Commit Graph
4844 Commits
Author SHA1 Message Date
alehelandClaude Opus 5.5 a0dfbecd4c fix(ui): a frame's last release is kept past the cap on its edges
Found in review (CodeRabbit): `PagePointer` keeps a frame's first eight primary presses and
releases, and the camera's click was located at the last release among those. After a stall
with more presses and releases than that, the click was located at an earlier release, so the
quadrilateral could go to the wrong page. The frame's last release is now read from all its
events (`PagePointer::released`), however many came before it.

Test: `a_frames_last_release_is_kept_past_the_cap_on_its_edges`, seen failing with the release
read from the capped edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:16:00 +01:00
KenM76andClaude Opus 5.5 16ffab9b03 M12: add a 3D model (U3D or PRC) to a page with a poster (#1297)
A new `model_add` automation tool and `Edit::AddModel3d` embed a U3D or
PRC file as a 3D annotation (ISO 32000-2 13.6.2) in a chosen rectangle:

- The model goes into a Flate-compressed 3D stream (13.6.3) with /Type /3D
  and /Subtype U3D or PRC, read from the file's header (`U3D` and a zero
  byte, or `PRC`); anything else is refused before the document changes.
  Models over 256 MiB (MAX_MODEL_BYTES) are refused. model_add checks the
  file's size (from the file system) and its header before reading the
  rest, so a large file that is not a model is never loaded; the path goes
  through the read root, and `read_model` joins the helpers the
  file-access test knows take a resolved path.
- /3DD references the stream, so the annotation has its own instance.
- /3DA states /A: click (XA, the standard's default and the tool's),
  page_open (PO) or page_visible (PV).
- /AP /N is the poster readers print and show before activation: a chosen
  image or PDF page fitted inside the box keeping its shape and centred (the
  custom-stamp picture path: `mark_source`, `fit_picture`, and the stamp's
  placement matrix now shared as `appearance::picture_place`), or, with no
  picture, a light frame labelled "3D model". It is drawn in user space and
  not counter-rotated, so on a page with /Rotate it turns with the page, as
  the model does.
- /Contents carries the name (the file stem by default) as the alternate
  description (12.5.2).
- A document older than the model needs gets /Version in its catalog
  (7.7.2): 1.6 for U3D, 1.7 for PRC, as the optimizer does for JBIG2.
  Adobe's extension level for PRC in 1.7 is not written.
- The 3D rectangle has its own MAX_MODEL_SIDE (14400 pt).

The edit is undoable ("Add 3D model"), needs the comments permission and
rebuilds only the comments view. A new parity entry, misc.3d-add, is
partial: no poster rendered from the model, no views (/VA, /3DV), no
replacing the poster, no UI command, no /Extensions. misc.3d-view is
unchanged. ROADMAP.md progress log line.

Adapted from pdfcer (MIT, same author), commit 1ca5ece, rewritten to this
repository's types and rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 16:09:08 -05:00
alehelandClaude Opus 5.5 bdd2ff0ee6 fix(ui): a camera click is located where it came up
Found in review (CodeRabbit): the last fix located a camera click by the pointer, which can have
moved on by the end of the release's frame. A click pressed on page 2 and released on page 1,
with the pointer back on page 2 by then, moved the quadrilateral to page 2. `PagePointer` keeps
each release's position, so `PagePointer::released` gives where the click came up, and the click
is located there.

Test: `a_camera_click_released_off_its_page_moves_nothing`, seen failing without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:04:19 +01:00
alehelandClaude Opus 5.5 b77903012d fix(ui): a camera click on another page moves the quadrilateral there
Found in review (CodeRabbit): with the camera tool, a press nobody holds on a page without the
quadrilateral moves it there, but a click was located by its press's origin, which egui has
cleared by the release frame. So only a drag moved it, and a click did nothing. This was so on
`main` too. A click is now located where it came up; `PagePress::Free` already says its press
began on this page.

Test: `a_camera_click_on_another_page_moves_the_quadrilateral_there`, seen failing without the
fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:51:52 +01:00
KenM76andClaude Opus 5.5 315e83b88c perf(cos): an object that can't be read is searched for once, not on every read
Document::try_get kept only objects that loaded. An object whose entry
points at something that doesn't parse falls back to a whole-file
search (scan_for); that failed too, nothing was kept, and the next read
searched the whole file again. A caller that reads such an object
repeatedly, such as the window title of a file whose document
information dictionary is damaged and that asks for its title to be
shown, paid one whole-file search per read, several per frame.

The cache now keeps the load result, a failure as well. It is the same
map with the same bounds, clears and per-reader privacy, so every place
that drops loaded objects (authentication, reconstruction, private scan
readers, eviction) drops remembered failures too. Edits are read from
the overlay before the cache, as before, so a set or freed number still
answers for itself. A failure is kept in nested loads too, as a success
already is; only a poisoned lock is never kept. Failures count towards
the cache's object caps and hold no bytes of their own. Sharing the map
between clones stays sound because entries, data and the stream limit
don't change after open.

Test: an_unreadable_object_is_searched_for_once counts whole-file
searches through a test-only counter (like SECTION_READS): one for
eleven reads and a clone's read. With failures not kept it counts 22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 15:28:12 -05:00
KenM76andClaude Opus 5.5 ebd09e6649 M12: PRC reader: map the prc-rs licence for license-check (#1297)
The Double code table's attribution pointed its licence file at the
table's own source. `cargo xtask license-check` requires every asset's
licence file to be a LICENSES.toml document, so the gate failed.

Add prc-rs's LICENSE at the same commit as `licenses/prc-rs-LICENSE.txt`,
fetched unchanged, with its origin and hash in LICENSES.toml, and point
the attribution entry at it. The MIT notice stays in the table's header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 15:03:39 -05:00
alehelandClaude Opus 5.5 58bb5ba3d0 test(ui): the page guard knows any router method, and its readers are tested
Found in review:
- **The router guard** listed the router's methods by name, so a method added later
  (`view.router.holds(i)`) would pass every check. A use of the router by a method neither list
  names now fails outside the functions allowed to write or read it. The comments' point test
  called by path (`PageCx::comment_at(cx, p)`) is caught too.
- **The guard's own readers** were proven only by planted builds. Reading the lines of code (the
  function each is in, a call rustfmt split) and finding hit tests imported under another name now
  work on a source string, and a unit test pins them with `hit_test_calls` and `declares`.
- **Docs.** Fill & Sign's preview hides wherever a press would be taken (a mark, its handles, a
  Prepare field), and the hits under the pointer before the tools act are taken on the frames that
  need them, not "once".

Tests: the guard's readers, seen failing with the line joining planted out. A planted router
method no list names, and a planted `PageCx::comment_at` by path, each fail the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:59 +01:00
alehelandClaude Opus 5.5 568ea94f48 fix(ui): a right-click between the pages anchors no page's menu; the guard finds router readers
Found in review:
- **A right-click off the pages.** Between the pages or beside one, no page took the
  right-click, but the page menu still opened over the page view, on the last anchor: Delete
  page, Paste and Copy Image acted on a page right-clicked long before. This was so on `main`
  too. The anchor is now cleared on every right-click before the pages set it, so off the pages
  the menu acts on the current page.
- **The guard** took a file as a page module only by marks a router read doesn't need. A file
  that reads `.router.` is one too, so its router reads are checked.
- **Docs.** egui ends a press's drag when any button is released, not when a second goes down.
  `link_ui::hit` takes the tip from the link it found rather than looking it up again.

Tests: a right-click between the pages clears the anchor, seen failing without the fix. A
planted router read in an unlisted file fails the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:56 +01:00
alehelandClaude Opus 5.5 6752b36807 fix(ui): a mark being moved keeps Fill & Sign's presses; the guard sees calls by path
Found in review:
- **Fill & Sign during a mark's move.** `main` placed no Fill & Sign mark while a comment's move,
  resize or reshape was live on the page, whoever held the press. That guard was lost: with a
  move left over (its release never seen), the next click placed a new mark. A live gesture on
  the page now counts as over a mark again (`comments::moving_on`, which the comments' own
  dispatch uses too).
- **The guard** missed the router's and the page view's hits called by path
  (`PageRouter::owner(&view.router, i)`), and its doc said the `.at(` it matches was confined to
  `page_input.rs`.
- `measure_ui::Clicks` derived traits nothing uses.

Tests: a Fill & Sign click while a mark's move is live places nothing, seen failing without the
fix. A planted router read by path fails the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:51 +01:00
alehelandClaude Opus 5.5 6756ab0ed4 fix(ui): a right-click where two pages touch is one page's; the guard reads split calls
Found in review:
- **A right-click where two pages touch.** With the page gaps hidden, a right-click on the row
  two pages share ran on both: the first page selected its comment, the second unselected it,
  and the second page's anchor for the menu stood. A right-click is now the first page's, as a
  press has been since the last fix. This was so on `main` too.
- **The guard** matched one line at a time, so a call rustfmt splits across lines
  (`view` / `.router` / `.owner(i)`) got past it. A continued line is now read with the one it
  continues. It also refuses a hit test imported under a name that isn't a hit test's
  (`use …::hit as under`), which the checks by name would miss.
- **Docs.** Links still take a right-click while the primary button is held, as before, and when
  Escape comes in a press's own frame every page is `Elsewhere`, not `Free`.

Tests: a stamp clicked where two pages touch lands on the first, a click pressed on one page and
released on the next places nothing, a right-click where two pages touch is the first page's,
Prepare keeps its selection on a click that began off the page, and the Note tool starts no note
on added text with the Edit panel open. Each was seen failing with its fix, or the rule it pins,
reverted. A planted split router read and a planted aliased hit test fail the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:41 +01:00
alehelandClaude Opus 5.5 2a4032b2fc fix(ui): a press where two pages touch is one page's; the guard sees PageHits::hits
Found in review:
- **A press where two pages touch.** With the page gaps hidden, pages can share a row of pixels,
  and a press there was claimed by both: the first page acted on it, then the second took it
  over, and the text layer could count it twice. Only a press just begun is claimed now, by the
  first page that does. This was so on `main` too.
- **The guard** listed who asks `PageHits::at` and `context_at`, but not `PageHits::hits`, which
  they wrap. A new call of it in `page_input.rs` now fails, and the guard's doc says what it
  still can't see.
- **Docs.** `Hit::declines_drag` (a link's drag with the Link tool), `over_mark`, and when the
  Select tool still unselects under another tool's frame.

Tests: two pages claiming one press, seen failing without the fix. A planted call of
`PageHits::hits` fails the guard. The router's unit tests now begin a press before claiming it,
as `draw_pages` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:38 +01:00
alehelandClaude Opus 5.5 c1d34fc9ea fix(ui): commenting tools leave a frame another tool took; Escape keeps the menu's anchor true
Found in review:
- **A double-click another tool took finished a polygon.** Round 5 kept commenting tools off a
  press another tool had taken, but a polygon, polyline or cloud finishes on a double-click or
  Enter whoever has the press. With Prepare open, a double-click on a field opened its properties
  and committed the polygon being clicked out. Added text opened for editing did the same with
  the Edit panel open. The commenting tools now skip such a frame entirely, as `main`'s consumed
  chain did. The comments' own presses and a click's unselecting still run.
- **The menu's anchor after Escape.** While a press Escape cancelled is held, the page's tools
  are skipped, and a right-click left the page menu on the page right-clicked before. It now
  anchors where it is, as every other right-click does.
- **The guard.** It now lists who asks the page view's hits at a point (`PageHits::at` and
  `context_at`), so a new place in `page_input.rs` can't test a point again. It takes a file
  that uses the comments' page context (`PageCx`) for a page module. Its doc of the point tests
  it doesn't list is now true.
- **Docs and visibility.** True docs for `PagePress::Free`, where a press is off the pages, and
  the camera's hit. `link_ui::screen_rect` is private.

Tests: a polygon left unfinished by a double-click on a Prepare field, and a right-click after
Escape anchoring the menu. Each was seen failing without its fix. A planted ask of the page
view's hits, and a planted unlisted file using `PageCx`, each fail the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:35 +01:00
alehelandClaude Opus 5.5 f0caaf9259 fix(ui): a press Prepare or added content took draws no comment; menus open where right-clicked
Found in review:
- **Comments acted on presses another tool had taken.** Comments run every frame now, and a
  commenting tool acted on any press nobody held, so with Prepare open a Rectangle drag drew a
  rectangle as well as Prepare's selection rectangle. Added content, with the Edit panel open,
  could likewise open a note composer under the Note tool. `main` skipped comments when another
  module took the frame. They are now told (`taken`), and their drawing and placing tools leave
  such a press alone. A click there still unselects.
- **The page menu's anchor went stale.** A right-click while the primary button was held skipped
  the call that also anchors the page menu, so Delete page, Paste and Copy Image acted on the page
  right-clicked before. Every right-click on a page now anchors the menu there, whoever takes it,
  and only the selections wait for the release. That includes the right-clicks Add text, Edit
  text, the Link tool and a field tool take, which used to keep the previous anchor.
- **Edit text's right-clicks.** Edit text took right-clicks on added content too. It now takes
  those on its paragraphs, pictures and the objects selected together, and comments see a
  right-click on added text, as before.
- **Camera corners.** A camera corner at the page's edge showed its grab cursor past the edge,
  where a press is no page's. Its hit now stops at the page, as a link's does.
- One source for a comment's layer, dead code in Add text's right-click, and true docs for
  `PagePress::Elsewhere` and the "Page presses" rule.

Tests: a shape-tool drag with Prepare open, a right-click with the button held on another page,
a right-click on added text with Edit text, the menu anchored by an Edit text right-click, and a
camera corner at the page's edge. Each was seen failing with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:32 +01:00
alehelandClaude Opus 5.5 aa415811d7 fix(ui): right-clicks leave a held press's selections alone; guard finds more page modules
Found in review:
- **Right-clicks during a press.** A comment being moved kept its selection only while its
  gesture state was live, so a gesture left over (a release the pages never saw) blocked every
  later right-click. Comments and Prepare now leave their selection alone while the primary
  button is held. egui ends the press's own interaction (and its drag) when another button goes
  down, so the button is what tells. A touch's long press, which egui reports as a right-click
  with the finger still down, still opens its menu.
- **A click that began beside the page** unselected the selected comment, though the Link tool and
  Prepare now leave theirs alone for such a click. Comments do the same.
- **The guard** took a file as a page module only if it named `PagePointer`; a file using
  `PageToolContext`, `PagePress` or the router's hits or owners is one too. Its message about
  tooltips and release checks now points at the page view's own hits, and its doc of the router's
  readers names link following.

Tests: a right-click with a gesture left over, a click that began beside the page, and a long
press on a comment. Each was seen failing with its fix
reverted. A planted page-router name in a module the guard didn't list fails the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:29 +01:00
alehelandClaude Opus 5.5 47ece03a54 fix(ui): the Link tool's click told too; a moved comment keeps its selection
Found in review:
- **The Link tool's click** acted on any press: with Prepare open, a click on a field also
  unselected the selected link, and a click that began off the page did too. It now acts only on
  a press on a link, or one nobody holds.
- **A right-click while a comment is moved** selected the comment under it mid-drag, though the
  release still moved the first. Comments now keep their selection during a move, resize or
  reshape, as Prepare does.
- The hits under the pointer before the tools act are taken only on the pointer's page, and the
  Link tool's right-click reuses them.
- The router has one source for the press's page (`Capture` no longer carries its own).
- The guard lists which functions may ask which comment lies under a point (`comment_at`); the
  guard finds hit tests by name, and its docs say so.
- Docs made true: what `PagePress::Mine` promises, what `tool_input` gates, and that a dialog does
  not cancel the pages' press (only Escape and automation's cancel do).

Tests: the two fixes, and ruling 1 for added text (a click on it unselects the comment), each seen
failing with its fix reverted; the drawing-tool right-click test now checks the comment's menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:25 +01:00
alehelandClaude Opus 5.5 ae62b66304 fix(ui): right-clicks with the Link tool and the camera as before; true docs
Routing right-clicks changed a few more cases beyond the rulings, found in review:
- **The Link tool.** A right-click on a link also unselected the selected comment and moved the
  page menu's anchor; the Link tool used to take it whole. On a document it can't change, its
  right-click on a note over a link opened the plain page menu instead of the note's. Now the Link
  tool's right-click is what its press there would take, on a document it can change, and on a
  link it leaves comments alone.
- **The camera.** A corner, on the handle layer, took right-clicks from a comment under it,
  though it has no menu. Corners are no longer right-click targets.
- **The Link tool's hand** showed in the 2 px margin past the page's edge, where a press is no
  page's. `link_ui::hit` now finds nothing off the page.

Docs made true: what a comment's press is (`comments::page_input`), what reaches the free-press
gate (`page_tool_input`), and when every page is `PagePress::Free`.

Tests: those three, and two that pin right-clicks kept as before: Edit text's on a paragraph
under a comment, and a Prepare field tool's. Each seen failing with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:22 +01:00
alehelandClaude Opus 5.5 6557d6edc5 test(ui): the page guard holds told modules to what they are told
The modules that are now told what a press is to their page (forms, Prepare, comments, the Link
tool, the camera, the stamps and the other free-press tools) left the press-handler list, whose
check was only that each read `router.owner(` and called no hit test of its own. Nothing kept
them from reading the router again, though CLAUDE.md said the guard enforced it.

- Rule (e) now: `router.press_for(`, `router.owner(` and `router.keeps_drag(` are read only by
  `ROUTER_READERS`: the page view, and the Edit text group and text layer until 4b tells them
  too. An unused entry fails the test, so 4b removes them one by one.
- A hit test called as a method counts as a call: `cx.hit(origin)` or `self.foo_hit(p)` in a
  press handler passed. `PageCx::hit`, which finds a comment and makes no `Hit`, is renamed
  `comment_at`.
- An entry of `PAGE_HIT_CALLS` that no page module calls any more fails the test.

Each was seen: a `view.router.owner(page)` and a `cx.hit(origin)` planted in forms' `page_input`
pass the guard before and fail it after, and a stale `PAGE_HIT_CALLS` entry fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:19 +01:00
alehelandClaude Opus 5.5 ce90c81ffb fix(ui): a drag seen past its page's edge still draws on that page
egui reports a drag once the pointer has moved a few points from the press. A press near a page's
bottom edge, flicked onto the next page in one frame, is seen as a drag only with the pointer on
the next page. Redact (and the area Highlight), the Link tool and Prepare returned before their
drag start unless the pointer was on their own page, and the next page is `Elsewhere`, so the
press drew nothing anywhere. (On main the next page took it, with the box's start on the page
before.)

They now start the gesture from where the press began on their own page, as zoom, snapshot, crop
and Sign area already did, so the box, link rectangle, marquee, field drawn or field moved belongs
to the page the press began on. Tests: one per tool, each seen failing with the fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:16 +01:00
alehelandClaude Opus 5.5 b12d091496 fix(ui): hover painting beside the open palette, and one hit test under the pointer
The page's hover (the top hit under the pointer) was only worked out when nothing was drawn over
the pages, so with the palette open a field beside it lost its tooltip and its hover outline,
and comments their outline. The palette keeps the page's cursor and tips away, which is what
that gate is for; hover now ignores it, as the outlines and the field tooltip did before.

The hits under the pointer were also asked up to four times a page each frame, each pass running
every module's hit test: for a click's release, for Fill & Sign's preview, for a right-click, and
for the cursor. A click's release, Fill & Sign's preview and a right-click's tool checks now share
one pass, taken before the tools act and only on frames that need it. A right-click's own pass
(comments by `context_hit`) stays, and the cursor and hover keep theirs, after the tools act.

A unit test pins that cancelling the pages' gestures lets go of a camera corner. The kittest for
Escape mid corner drag passes on `main` too, because Escape leaves the camera tool first.

Tests: `a_fields_tooltip_shows_beside_the_open_palette`,
`cancelling_pointer_gestures_lets_go_of_a_camera_corner`; each seen failing with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:13 +01:00
alehelandClaude Opus 5.5 b1a0657c42 fix(ui): right-clicks with Prepare, the Link tool and Add text as before
Routing right-clicks by what lies on top under the pointer changed three cases beyond the
rulings:
- **Prepare.** A right-click while a field was dragged selected the field under the pointer, and
  the release then moved that one. Prepare used to ignore right-clicks during a drag; it does
  again (`prepare::context` returns while a grab is held).
- **The Link tool.** A note over a link took the right-click, though the Link tool's press there
  is the link's. The Link tool's right-click on a link is the link's again, whatever lies over it.
- **Add text.** Comments took right-clicks away from paragraphs and added items. Add text used to
  take every right-click on a page it can change; it does again.

Tests: those three, and two that pin ruling 6's right-click half with the Select tool: no link
menu on a link with no action, and none on a form field over a link. Each was seen failing with
its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:10 +01:00
alehelandClaude Opus 5.5 734cb4c13a test(ui): the page guards read past a test module in the middle of a file
The page guards (pointer reads, cursor writes, and presses) left a file's tests out by cutting it
at its first `#[cfg(test)] mod`. `edit_text_ui/image.rs` has a test module at line 231, so its
lines 245-1116 were never read: `crop_input`, `image_input` and the picture hit tests among
them. A hit test called there, a router write or a raw pointer read passed every guard.

`outside_test_modules` now leaves out each test module up to its closing brace, and the guards
read the rest. Nothing in the newly read code breaks a rule. A unit test pins the scanner, and a
bare `crop_hit(` planted in `image_input` now fails the press guard (it passed before).

The guard's messages and docs, CLAUDE.md's "Page presses" line, and the docs of
`page_tool_input`, `PageHits::hits` and `PageRouter::cancel` now say what the code does: modules
are told what a press is to their page (`PagePress`), and `PageHits::hits` is the one function
that asks the hit tests. `comments::context_hit(` joins the hit tests a press handler must not
call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:07 +01:00
alehelandClaude Opus 5.5 ed8a10e83e refactor(ui): tell forms and Prepare whether the press is theirs
Forms and Prepare read the router themselves, and forms tested the pointer twice more: for a
field's tooltip, and for whether a click came up over the widget it was pressed on. Following a
link tested the pointer again too.

Both are now told the page's `PagePress`. Prepare's Select tool draws a selection rectangle, or
unselects, only on a press nobody holds, and a field tool draws only on such a press. Whether a
click came up over what its press began on is answered once, from the hits under the pointer
(`released_over`), for fields and for following links. The field tooltip (`forms_ui::tooltip`)
and the field hover outline come from the page's hover, the top hit under the pointer.

`PageHits::hits` is now the only function that calls a hit test, and the press-handler check no
longer lists forms or Prepare.

Behaviour changes: a field's tooltip shows only where a press would take the field, not under
a comment drawn over it (the user's ruling, 2026-10-31); a field's hover outline follows the
same rule, so it shows only with the Select tool, which fills fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:04 +01:00
alehelandClaude Opus 5.5 951ef7e362 refactor(ui): one gate for the presses nobody holds
The tools that act wherever on the page each decided for themselves whether a press was
theirs: Sign area, zoom and snapshot, crop, redact and the stamps asked the router, and Measure,
Fill & Sign, the camera and the Link tool's rectangle acted on any press. Stamp placement lived
inline in the page view.

`tool_input` is now the one gate. Measure, the Stamp tool and custom stamps, Fill & Sign, Sign
area, zoom and snapshot, the camera and crop are each told whether the press is the tool's (it
began on their page and no hit won it), and act only then; their cursors and gestures run as
before. Redact's boxes and the Link tool's rectangle are told the same where they are called.
Stamp placement moves to `stamps_ui::place` and `place_custom`, beside custom stamps. Measure
is told its clicks (`measure_ui::Clicks`), Sign area takes a `PageToolContext`.

The camera's corners are hits (`camera_ui::hit`, `Owner::CameraCorner` on the handle layer), so
a corner's drag is an owned press like any other: it starts on the corner's own press, ends on
`drag_stopped() || !primary_down`, and `cancel_pointer_gestures` drops it. Until now only the
order of two key handlers kept Escape mid-drag from leaving a corner following the pointer:
Escape leaves the camera tool before the pages see it. The kittest for Escape mid-drag can't
fail with that order; a unit test of the cancel comes later in this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:56 +01:00
alehelandClaude Opus 5.5 84fe5e54b3 refactor(ui): tell comments and Fill & Sign whether the press is theirs
Comments ran last in the page view's chain of `||`, so only when no earlier module had taken the
frame, and asked the router themselves (`grabs`, `fill_grabs`, `draw_grabs`), with a hit test of
their own for hover. A link, a field or added text taking the frame first hid a comment's
gesture end, its Escape and the click that unselects it.

Comments now run on every frame and are told the page's `PagePress`:
- a press on a comment (its body, a handle or a point) goes to `select_input` with the part,
  with the Select tool, a shape tool (the selected shape's handles) or Fill & Sign (its marks);
- the drawing tools act only on a press nobody holds (`Gestures::free`);
- a click on anything but a comment leaves comments unselected.

`grabs`, `fill_grabs`, `draw_grabs` and `cancelled_drag` (with the raw button read in `keys` it
needed) are gone; `grabs_points` is `reaches_points`. Fill & Sign leaves the press to a mark
when the press is someone's, or, with no button down, when a mark is the top hit under the
pointer. The page's hover is that top hit (`PageInput.hover`), and the comments' hover outline
follows it, so it no longer shows under a field or added text that would take a press.

Behaviour change (the user's ruling, 2026-10-31): a click on a form field, or on added text,
leaves the selected comment unselected, as a click on a link or the page already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:52 +01:00
alehelandClaude Opus 5.5 1780f33350 refactor(ui): tell each page whether the press is its own
Each module asked the router `owner(page).is_none()` to learn whether a press was the active
tool's. That reads a press held on another page, or begun between the pages, as the tool's
too.

The router now remembers the page a press began on (`begin` on every new press, then `press`
on the page it lands on), and `press_for(page)` answers what a page has to do with it:
- `Mine(owner)`: the press began on this page and the owner's hit won it;
- `Free`: no button is down, or the press began on this page and no hit won it, so it is the
  active tool's;
- `Elsewhere`: the press began on another page, or between the pages.

The page view asks once per page, and the stamps and the Link tool use the answer. The other
modules move to it in the next commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:43 +01:00
alehelandClaude Opus 5.5 bab615fed1 refactor(ui): route right-clicks by what lies on top under the pointer
Each right-click handler tested the pointer itself, and the chain's order decided which ran:
comments only when no earlier module took the frame, Prepare by its own widget scan.

A right-click now goes to what lies on top under the pointer (`PageHits::context_at`), by the
layer ranking a press uses, with two differences. Comments count with every tool, as a
comment's menu opens whatever tool is active (`comments::context_hit`). A highlight over a link
is the highlight's: a left click there follows the link, a right-click opens the highlight's
menu. On every right-click the page's menu is anchored at the pointer, and the comment it is
on is selected, or none (`comments::context`); a Prepare field is selected (`prepare::context`);
links take theirs from the same answer. Edit text's own paragraphs and pictures, and a Prepare
field tool, keep their right-clicks as before.

Behaviour change (the user's ruling, 2026-10-31): a right-click on a highlight over a link opens
the highlight's menu, not the link's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:40 +01:00
alehelandClaude Opus 5.5 2b256ab0c7 refactor(ui): one link test for the Select tool, the Link tool and right-clicks
Three link tests disagreed. The Select tool's hand and click (`link_hit`) took the topmost link
the renderer lists, with no margin, by its place in that list. The Link tool's selection and
Select's right-click took the bottom one of every link the engine lists, 2 px around each, by
`/Annots` index. Comments had a fourth (`on_link`).

Now `link_ui::hit` is the one test, and `Owner::Link` carries the `/Annots` index:
- the Link tool finds every link the engine lists, 2 px around each, and takes the topmost; a
  drag from a link still draws a new one (the hit declines the drag);
- every other tool finds the links the renderer can follow, with no margin (so Select shows no
  hand over a link with no action, as before), and takes the topmost;
- following a link looks its target up by that index;
- `comments::hit` is told whether a link is under the point.

The Link tool is told the link its press is on, and the one a right-click is on, instead of
testing the pointer itself. A right-click goes to the top hit under the pointer, so with the
Select tool a right-click on a note over a link selects the note and opens its menu, and one
just beside a link no longer opens the link's.

Behaviour changes (the user's rulings, 2026-10-31): the Link tool takes the topmost of stacked
links; Select's right-click has no margin; a right-click on a note over a link is the note's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:37 +01:00
alehelandClaude Opus 5.5 71d222cacf refactor(ui): move page input out of canvas.rs into canvas/page_input.rs
What the tools do with the pointer on one page now lives in its own file: `PageInput`,
`PageToolContext` and `page_tool_input`; `PageHits`, `edit_inventory`, `link_hit` and
`cancel_on_escape`; the text layer, `text_layer` to `finish_text_pointer_gesture`; and
`page_overlays`, which follows links and picks images. `cached_edit_images` stays in canvas.rs,
where painting and the menus use it too. No change in behaviour. canvas.rs goes from 4,073 to
3,265 lines.

The guard's page modules gain `canvas/page_input.rs`, and its allowlist entries move with the
functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:28 +01:00
alehelandClaude Opus 5.5 0cbd61d611 test(ui): one source-scan guard for the Page presses rule
#5426 took the page pointer router from #5300 at 11bc8c56, before #5300 added its guard. The
guard on main checks seven named press handlers, eight hit tests called by path and one
`router.press(` caller. Planted inside `#[cfg(any())]`, it caught one of five violations: a
second `link_hit` in `page_tool_input`, a bare `hit(` in comments' `page_after_text`,
`view.router.cancel()` and `press_cancelled = true` in redact_ui all passed.

This brings in #5300's rules beside main's press-handler check, in one test,
`page_modules_act_on_a_press_only_through_the_router` (from aef301d82):
- only `page_tool_input`, `draw_pages` and `cancel_pointer_gestures` press, settle or cancel
  the router;
- only `cancel_on_escape` ignores a press until the button is up;
- every `hit` and `…_hit` call, bare or by path, is in a function allowed to make one, with why;
- a `Hit` is made only in a hit test;
- an allowlist entry that is no longer used fails the test.

It caught all five planted violations, and a stale ROUTER_WRITERS entry. CLAUDE.md's "Page
presses" line names the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:26 +01:00
KenM76andClaude Opus 5.5 cb317560fe M12: PRC reader: answer review (#1297)
- Schema programs draw on one step allowance per reading pass:
  MAX_SCHEMA_STEPS (2^22) moves into the BitReader's budget next to the
  decoded-element allowance, and the tree walk carries both from reader
  to reader. A per-call ceiling let a cheap entity repeated many times
  run ~2^22 steps each.
- The vertex-colour pre-check charges the minimum encoding (one full
  colour, then one reuse bit each), so a run of reused colours may end a
  section.
- placements() reports unreadable globals when an entity reference, not
  only an item, names a reference coordinate system.
- chunks_exact -> as_chunks for clippy 1.99 (chunks_exact_to_as_chunks).

Tests: programs_on_one_stream_share_the_step_allowance,
one_step_allowance_covers_the_whole_walk,
reused_vertex_colours_may_be_the_last_data,
unreadable_globals_are_an_error_when_a_reference_names_a_coordinate_system;
each fails with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 14:38:13 -05:00
alehelandClaude Opus 5.5 12e17a1f64 refactor(ui): crop mode's hit is a hit test of its own
Crop mode owns every press on its page, the controls beside its frame included (0595d4d39).
`PageHits::at` made that hit itself. A `Hit` comes from a module's hit test, named `hit` or
`…_hit`, so the cursor, the tip and the press agree, and the Page presses guard holds the page
modules to it. The picture module's `crop_hit` makes it now, and `PageHits::at` asks it first,
as before. No change in behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:38:09 +01:00
KenM76andClaude Opus 5.5 5eb484c235 M12: PRC reader: a test for each input guard (#1297)
Disabling each input guard in turn left several with no failing test.
Each now has one that pins the guard's own refusal, not a later error:

- a zero Huffman code-length width, and a zero code length;
- a boolean array longer than the data, refused before any bit is read;
- a character array count past the data;
- no file structures, or more than MAX_FILE_STRUCTURES;
- a schema entry count past the data;
- normal angles wider than 16 bits;
- more texture sets than a face may carry (now by message);
- representation sets nested past the ceiling (and at it, accepted);
- a prototype chain past the ceiling;
- the element allowance, at each place it is charged: an entity's
  counted array, uncompressed character and short arrays, a schema
  program, and across one walk's readers (walk_within takes the
  allowance so a test can set it; an allowance that fits one file
  structure does not fit two).

Each fails with its guard disabled (the texture-set one by aborting on
the allocation the guard prevents).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:52:20 -05:00
vangel76andClaude Sonnet 5.5 bd569b81af Merge upstream/main into feat/redact-mark-regex
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 19:47:42 +01:00
vangel76andClaude Sonnet 5.5 330ca712ae Merge upstream/main into feat/optimize-target-bytes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 19:44:30 +01:00
KenM76andClaude Opus 5.5 56578a6006 M3: test what moves an OCR edit on top: clipped, two images, form (#3973)
Review asked for text under and above a scan (already pinned), an image
clipped away entirely, two images, and a form holding the image. The
clipped case fails when the hit test uses the image's box instead of
its visible box; the negative cases fail when any later XObject counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:42:16 -05:00
KenM76andClaude Opus 5.5 d69d5cc832 Merge origin/main into fix/ocr-edit-under-scan
Conflicts: images.rs (main folded image_transform_scope into
image_scopes; kept only painted_over) and text/interp.rs (gs arm now
reads params via dict_in_place, then records the soft-mask anchor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:36:38 -05:00
vangel76andClaude Sonnet 5.5 72db15ac6c Merge upstream/main into feat/crop-toolbar-5330
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 19:28:21 +01:00
KenM76andClaude Opus 5.5 a7cc7586d1 M12: PRC reader: cap decoded array elements per reading pass (#1297)
The inflation cap bounds bytes, but one bit can decode to a 4-8 byte
element, so a 512 MiB section could ask for about 16 GiB. Every array
count is now charged against MAX_DECODED_ELEMENTS (2^24), shared by
all readers of one tree walk; over the allowance is TooManyElements.
Huffman pre-sizing is capped at 1024.

decoded_elements_are_charged_against_one_allowance fails with the
charge in BitReader::take_elements removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:27:05 -05:00
KenM76andClaude Opus 5.5 e93e8d50ba fix(edit): keep a soft mask anchored when the edit is lifted over the scan (#3973)
A soft mask is placed by the CTM in force when its `gs` runs (ISO 32000-2
11.6.5.2). The paragraph lifted over the scan re-selects the graphics state
after its own `cm`, so a mask selected before a translation landed
translated with it and could hide the new text. A re-emitted state that
carries a dictionary /SMask now records its anchor CTM and is selected
there: `cm` to the anchor, `gs`, `cm` back.

The test edits the same paragraph with and without a mask and requires the
same ink; it fails with the re-anchoring disabled (the mask lands 100 pt
right and the text vanishes). Both rewrite paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:15:43 -05:00
KenM76andClaude Opus 5.5 ee12dc684c Merge origin/main into perf/png-predictor-bytes
main took vendor patches 29-34; the PNG predictor patch is now 35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:01:22 -05:00
KenM76andClaude Opus 5.5 c566ca6144 M12: PRC reader: tree markups and views read past (#1297)
CAD 3D files carrying PMI failed model_tree() and placements() with
"tree markups (PMI) is not supported": the tree section has no length
prefix, so MarkupData and views had to be parsed to be skipped.

Now read past, not modelled (new tree/markup.rs):
- MarkupData [WD 7.3.10.4]: markup linked items (204), leaders (503),
  markups (502), annotation items/sets/references (504-506); sets
  nest under the MAX_DEPTH ceiling.
- Views (501) [WD 7.7.3], with their annotation plane (SURF_Plane 86),
  optional scene parameters, and from 8137 their flags, linked items
  and filters.
- Scene lights (731-734) and clipping planes, so
  SceneDisplayParameters no longer refuses them.
ReferenceData and ReferenceOnPRCBase are shared readers now, and skip
their schema-added fields.

Field orders the working draft leaves open follow prc-rs's format
description (MIT); each such reader carries a `prc-rs:` marker.

Tests: one synthetic test per entity kind (each reads to a sentinel
and every truncation errors), nesting ceiling, wrong annotation type,
and a tree with markups and views on a part and an occurrence that
still places its mesh. Each shown failing with its reader reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:44 -05:00
KenM76andClaude Opus 5.5 c571b13469 M12: PRC reader: product tree and styles (#1297)
Walk the PRC product tree into placed, coloured tessellations.

- PrcFile::placements / placements_with(StyleAlpha, EntityOverrides):
  every drawable tessellation reached from the model file's roots, placed
  by its occurrence path's locations and then the representation item's
  local coordinate systems (a 4x4 Matrix) [WD 7.3.10.1, 7.6.3.2].
- Colours: styles and materials from the file structure globals, the
  father/son inheritance bits [WD 7.2.4.2], style transparency combined
  with material alpha by StyleAlpha, per-face styles and topology
  references via Placement::triangle_colours. A prototype's part is
  coloured by its own structure's styles. A colour index counts the colour
  array's doubles (pdf-issues #816).
- Occurrences' entity references: colour, hide or relocate the entity
  they name, in the occurrence's subtree, everywhere, or not at all.
- PrcFile::model_tree: the assembly tree as a model-tree panel lists it,
  with name sources, visibility and each node's placement range.
- TriangleMesh::transformed applies a Matrix: positions, cofactor-mapped
  normals, winding flipped under a mirror.

Never panics: occurrence and prototype cycles are Malformed; depth 64,
2^20 visits, MAX_PLACEMENTS and a face-override ceiling bound the walk;
counts are checked against the remaining data before allocating.
Markups, views, scene lights and clipping planes are Unsupported;
textures are read past.

Field orders the working draft leaves open follow prc-rs (MIT); each is
marked `// prc-rs:`, as are the step-1 container, Double and schema
choices that did.

Adapted from pdfcer (MIT, same author), crates/pdfcer-3d/src/tree and
vec3.rs at 0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:44 -05:00
KenM76andClaude Opus 5.5 7f30266790 M12: PRC reader: tessellation and arrays (#1297)
Read the tessellation section of a PRC file structure:
FileStructure::tessellations() returns each entity as a Tessellation.

- TESS_3D becomes a TriangleMesh: positions, triangles from the
  triangle/fan/strip blocks (one-normal and textured variants included),
  one triangle range per face, stored normals and texture coordinates
  with per-corner indices, and per-triangle line-attribute styles.
- TESS_3D_Wire becomes polylines (closing and continuous flags); vertex
  colours are read past, sized by points (closing point included).
- TESS_Markup is read past.
- TESS_3D_Compressed is read field by field to its end so the entities
  after it decode; its triangles are not rebuilt (later step).
- The compressed Character/Short/Integer/Indice arrays and the Huffman
  container they use (WD 11.6-11.9, 12.2) live in arrays.rs; the base
  fields every entity shares (attributes, name, user data) in entity.rs.

Never-panic hardening beyond the source this is adapted from:
- Huffman symbol width must be 1..=16; the leaf table and element count
  are bounded by the payload before anything is allocated.
- A face may carry at most 8 texture-coordinate sets (an unbounded
  count allocated per set and padded per triangle).
- Every count is checked against the remaining bits; indice sums and
  deltas use checked arithmetic.

A few reading choices (Huffman bit order and code-length width, indice
running sums, 64-bit attribute order, three version gates, the
is_segment_color bit, the compressed field order) follow prc-rs (MIT);
each is marked `// prc-rs:` in the source.

Tests are synthetic, written with the crate's test bit writer. Each
fix was reverted and its test seen failing (17 reversions).

Adapted from pdfcer (MIT, same author), pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
KenM76andClaude Opus 5.5 ab552430a7 M12: PRC reader: the Double code table from prc-rs (MIT) (#1297)
Every PRC Double decodes through a fixed 2077-row prefix-code table
(working draft 11.17.2). The rows come from prc-rs src/double.rs at
287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d under its MIT licence, whose
notice stays in the file header; ATTRIBUTION.toml records it.

The table is a complete prefix code, so the empty-table stub test is
replaced by a truncation test. The exponent lookup masks the 11 exponent
bits, because the last exponent row's upper word is 0x7ff80000.

Revert proof: with the table emptied, eight Double tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
KenM76andClaude Opus 5.5 83b230ae73 M12: PRC reader crate: container, bit primitives, schema (#1297)
New standalone L0 crate pdfcraft-prc: the PRC container walk (file
header, file-structure descriptions, per-section zlib inflation under a
512 MiB ceiling), the bit-level primitives of the compressed sections,
and the schema interpreter a newer producer uses to describe fields it
added (step and nesting ceilings).

The Double prefix-code table is not in this commit: Double reads
return UnknownDoubleCode, and the tests that need the table are
ignored with that reason.

Adapted from pdfcer (MIT, same author), crates/pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
FredApps 168a3cb91e Merge pull request #5740 from Combiler01/fix/panel-resize-scrollbar-5730
fix(ui): prevent panel resize handle collision with scrollbar (#5730)
2026-11-02 17:47:35 +01:00
FredApps 8f1441a3f2 fix(ui): keep pane resize edges right of the border, off the page scrollbar (#5730)
The centred 4 pt strip still covered the right 3 pt of the page's
scrollbar beside a right-docked pane. Register the host's resize edge
again from the border rightwards, on the host's own layer, and leave an
inert edge inert. The test now hovers and drags both scrollbars beside a
right-docked Tools pane (CodeRabbit) and checks the width is unchanged.
2026-11-02 16:59:26 +01:00
FredApps 1997b8ed94 Merge remote-tracking branch 'origin/main' into pr5740 2026-11-02 16:29:14 +01:00
FredApps e261518727 Merge pull request #5569 from Piohrun/feat/jbig2-run-allowance
M2.6: the images of a run, and a page's JBIG2 images, share one allowance of decoding
2026-11-02 15:11:32 +01:00
FredApps 1303684851 Merge pull request #5639 from RiceDee/fix/rail-page-zoom-fields
fix(ui): the rail's page box fits, and the zoom field looks like a field (#5636)
2026-11-02 14:46:11 +01:00