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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
to this line rather than an edit in a pipeline variable group.
-->
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.1.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ How the project is developed and shipped is documented separately:
- Markdown `.wimport` recipes that build image and text containers from headings
- An intentionally small floating toolbar
- A File / Edit / View / Help tab strip. Click a tab for a one-row command strip over the canvas
- Preferences for the startup monitor, full-screen start, finger drawing, snippet format order, laser trail timing and weight, toolbar position and layout, and (except Store installs) a daily new-version check
- Preferences for the startup monitor, full-screen start, finger drawing, the pen button, snippet format order, laser trail timing and weight, toolbar position and layout, and (except Store installs) a daily new-version check
- About, with version and channel

## Build and run
Expand Down Expand Up @@ -174,14 +174,15 @@ Use **Copy settings** after finding a useful combination so the exact values can
| Input | Behavior |
| --- | --- |
| Pen tip | Current tool; Pen is selected at startup |
| Shift + pen tip | Constrain the stroke to horizontal or vertical, whichever is nearer, at a uniform width. Press or release mid-stroke to start or end the constraint from that point. The barrel button does the same when assigned to Straight line |
| Pen hover | Show the small red pointer dot and hide the arrow |
| Pen contact | Hide both the pointer dot and arrow |
| Physical mouse movement | Show the normal arrow |
| Left mouse | Temporarily select/move/resize a container; return to the previous drawing tool on release |
| Double-click container | Center and fit the image, text, or LiveView to the canvas |
| Double-click empty canvas | Center and fit all board content, or reset an empty board |
| Pen eraser | Erase complete strokes |
| Pen barrel | Hold the lower barrel button for the laser; release returns to the previous tool |
| Pen eraser | Erase complete strokes. The upper side button erases too: Windows reports it the same way as a pen turned round |
| Pen barrel | Hold the barrel button for the action assigned in Preferences: Laser (default) or Straight line. Laser returns to the previous tool on release |
| One finger | Pan. With Finger drawing on, uses the current tool instead |
| Two fingers | Pan and pinch zoom. Cancels an in-progress finger stroke when Finger drawing is on |
| Mouse wheel | Zoom at the pointer. Shift+wheel zooms more slowly |
Expand All @@ -201,7 +202,7 @@ Use **Copy settings** after finding a useful combination so the exact values can
| Delete | Delete the selected container and its linked strokes |
| Alt+L | Laser pointer |
| File / Edit / View / Help | Tab strip. Click a tab for a one-row command strip over the canvas. Click the canvas to hide it |
| Help > Preferences | Searchable settings: startup monitor, full screen, finger drawing, snippet format order, laser trail, toolbar, update checks |
| Help > Preferences | Searchable settings: startup monitor, full screen, finger drawing, pen button, snippet format order, laser trail, toolbar, update checks |
| View > Bring to front / Send to back | Reorder the selected image, text, or LiveView (and its linked strokes) |
| Help > About | Version, channel, license, the product site, and a download link when a newer release is known |
| View > LiveView | Capture, freeze, disconnect, or reconnect a window or display |
Expand Down Expand Up @@ -246,13 +247,21 @@ is installed. The source is `vscode/sqlbi-whiteboard`.
- `SQLBI.Whiteboard.Dax` contains the framework-neutral DAX lexer, parser, classifier, and deterministic formatter adapted from Prompt Assistant.
- `SQLBI.Whiteboard.SqlServer` contains the framework-neutral SQL Server 2025 adapter over Microsoft's ScriptDOM parser and script generator.
- `vscode/sqlbi-whiteboard` is a VS Code custom editor that shows `preview.png` from a `.wboard` ZIP. It is not part of the desktop installer.
- `SQLBI.Whiteboard` is the WPF shell. `InkCanvas` supplies system-managed wet ink, while `BoardSurface` renders completed ink, images, text, and selection on a white canvas in camera space. A transient AvalonEdit surface is overlaid only while a text container is being edited; language services translate parser classifications into WPF text styles.
- `SQLBI.Whiteboard` is the WPF shell. `TouchInkCanvas` supplies system-managed wet ink for the finger, while `BoardSurface` renders completed ink, images, text, and selection on a white canvas in camera space, plus the pen's own wet stroke. A transient AvalonEdit surface is overlaid only while a text container is being edited; language services translate parser classifications into WPF text styles.
- `SQLBI.Whiteboard/LiveView` owns Windows Graphics Capture and the Direct3D-to-WPF bridge. Capture retains one GPU frame per active LiveView; CPU bitmap conversion occurs only when copying or saving a snapshot.
- `SQLBI.Whiteboard.Core.SmokeTests` is a package-free executable test harness for camera anchoring, commands, hit testing, and archive round trips.

The current document query is deliberately linear. A spatial index can be introduced behind `BoardDocument.Query` when profiling demonstrates a need, without changing input, tools, persistence, or rendering call sites.

Live ink uses a transparent WPF `InkCanvas` above the retained scene. WPF renders wet ink on its dedicated dynamic-rendering thread. On stroke completion, pressure points are transformed from screen space into the unbounded world model; camera movement is suspended while the pen is in contact.
Live ink uses a transparent WPF `TouchInkCanvas` above the retained scene, and it collects
the finger's strokes only: WPF renders those on its dedicated dynamic-rendering thread.
Pen ink is read straight from the pen's packets by `MainWindow.AppendPenInk`, which owns
the contact, the straight-line constraint, and the calligraphy dynamics, and draws the wet
stroke through `BoardSurface.PendingStroke`. A barrel button tears the WPF contact in two
every time it is pressed or released — see [TODO.md](TODO.md) — so no stroke built on that
bookkeeping could behave like the Shift key. In both paths pressure points are transformed
from screen space into the unbounded world model on completion; camera movement is
suspended while the pen is in contact.

## Wacom Cintiq Pro validation

Expand Down
48 changes: 47 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis
pre-release to GitHub Releases, and one approval promotes that same build to a release.
<https://whiteboard.sqlbi.com> reads its download links from the release manifest
deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props`
(1.0.3). Identity version for the Store package is `VersionPrefix.0` (`1.0.3.0`).
(1.1.1). Identity version for the Store package is `VersionPrefix.0` (`1.1.1.0`).

Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0
was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code
Expand All @@ -34,6 +34,52 @@ walked in full for 1.0.0, which was the last part of the chain that had only eve
reasoned about. winget is the one piece still waiting, below. All of it is described in
[docs/release-management.md](docs/release-management.md).

## Pen buttons: what was settled, and what is left

The barrel button is the only assignable one, and it takes Laser or Straight line. Adding
an action means an entry in `PenButtonAction`, a choice in `SettingsCatalog`, and — if it
swaps the tool rather than acting as a modifier — a case in `MainWindow.BarrelToolFor`.
Nothing else needs to know.

Erasing is not assignable. The reverse end of the pen erases, and so does the upper side
button, because they cannot be told apart:

- **The upper button and a reversed pen are the same signal.** A trace from the
development pen (`PenTrace`, enabled by pointing `SQLBI_WHITEBOARD_PENTRACE` at a file)
settles what several rounds of inference could not. The device exposes exactly two
buttons, `Tip Switch` and `Barrel Switch` — no eraser button, no secondary tip button.
Clicking the upper side button and turning the pen round produce identical events:
`Inverted` goes true, both switches stay up, pressure stays zero, and when either one
lands the same tip switch closes. So an inversion is the eraser, full stop. A device
that reports a real `SecondaryTipButton` would be distinguishable, and supporting one
would mean re-introducing a second slot — worth doing only if such a device turns up.

- **The barrel switch masks the tip switch, and the ink there is recovered by hand.** A
barrel press and a barrel release each arrive as a stylus up with `InAir` true. After a
release the pen keeps reporting `Tip Switch=Up` and `InAir` until the button is pressed
again - while the tip is still on the glass, and while the packets still carry its real
pressure (0.54 rising to 0.69 across one such gap in the trace). WPF delivers those as
in-air moves, so the InkCanvas collects nothing and the ink drawn in between was lost.
`AccumulateMaskedTipInk` keeps them instead and commits them as a freehand stroke when
the gap ends. Two consequences worth knowing: a barrel transition splits the line into
separate stroke objects, which shows as a seam where a highlighter overlaps itself and
as several undo steps; and the recovered stretch appears when the gap closes rather
than under the tip, because there is no wet-ink path for points the InkCanvas never
sees. Giving it one means drawing a provisional stroke on the scene surface.

- **The straight-line constraint cannot start mid-stroke from a button on this pen.** It
can from Shift, and from the barrel button, because both are reported while the tip is
down. Anything reported only through `Inverted` is not, since Invert and Tip are
mutually exclusive on this device.

- **Two constants stand in for signals the hardware does not give.** `AppendPenInk` calls
four consecutive weightless packets a lift rather than a dropped reading — no digitizer
misses four readings in a row. `DefaultActivationDistance` is 24 px: how far the pen
must travel before the axis is settled. It was 8 px, which let a few milliseconds of
the previous direction pick the axis; a trace of real strokes is the way to revisit it.
Once settled the axis is kept for the whole segment, however far off it the hand
drifts — turning a corner instead was tried and produced a staircase out of a diagonal.

## Waiting on the first winget submission

Not work, but the reason winget is not finished yet.
Expand Down
38 changes: 38 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,44 @@ outbound request.

---

## 22. Pen ink is collected from the pen, not from the InkCanvas

**Implemented.**

`InkCanvas` collects a stroke between a stylus down and the matching up. On a pen whose
barrel switch shares the report with the tip switch — the development Cintiq, and the
reason this was found — pressing or releasing that button fabricates a stylus up followed
by a stylus down, and between a release and the next press the driver reports the tip as
open while it is still pressing. WPF therefore reports the pen in the air for as long as
the button is held, and the contact is torn in two on every click.

Every attempt to repair that inside the InkCanvas moved the fault rather than removing it:
strokes joined across stretches the pen never drew, ink was lost between a release and the
next press, and a modifier held down could not be released. Meanwhile the pen's own packet
stream never breaks — position and pressure arrive continuously, in contact or not, which
a trace of a real session established (`PenTrace`, enabled by pointing
`SQLBI_WHITEBOARD_PENTRACE` at a file).

So the window reads that stream directly. `MainWindow.AppendPenInk` owns the contact — it
begins at the first pressured packet and ends after a run of weightless ones — and applies
the straight-line constraint and the calligraphy dynamics to each point as it arrives. The
wet stroke is drawn by `BoardSurface.PendingStroke` rather than by WPF's dynamic renderer.
The straight-line constraint is then one boolean read per point, which is what makes the
barrel button behave exactly like the Shift key: neither has any opinion about whether WPF
thinks the pen is down.

`TouchInkCanvas` keeps the InkCanvas for finger ink, where nothing tears the contact, and
hosts the laser sampler and the hover tracker. It collects no pen ink; strokes the
InkCanvas still opens for a pen are discarded on arrival.

The cost is that pen wet ink is drawn on the UI thread rather than WPF's dedicated
dynamic-rendering thread. Reverting is not attractive: the machinery this replaced —
a stylus plug-in for the constraint, recovery of ink from in-air packets, splitting a
collected stroke back into contacts, and a second stroke lifecycle inside the renderer —
was several hundred lines and never converged.

---

## Open questions

- arm64 is not built; add it if Surface devices matter for a pen application.
Expand Down
6 changes: 3 additions & 3 deletions site/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ <h3 id="ink">Ink and tools</h3>
<div class="toolrow">
<svg class="glyph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m15.87 2.67 4.97 4.97c.88.88.88 2.3 0 3.18l-8.68 8.68h6.1c.37 0 .69.28.74.65v.1c0 .38-.28.7-.64.74l-.1.01H9.83a2.24 2.24 0 0 1-1.71-.65l-4.97-4.97a2.25 2.25 0 0 1 0-3.18l9.53-9.53c.88-.88 2.3-.88 3.18 0Zm-10.16 9.1-1.49 1.49c-.3.29-.3.76 0 1.06l4.97 4.96c.15.15.34.22.53.22h.07a.75.75 0 0 0 .46-.22l1.49-1.48-6.03-6.03Zm8.04-8.04L6.77 10.7l6.03 6.03 6.98-6.98c.29-.3.29-.77 0-1.06L14.8 3.73a.75.75 0 0 0-1.06 0Z"/></svg>
<span class="name">Eraser</span>
<span class="role">Removes whole strokes. The eraser end of a pen does the same, and hovering with it draws a dashed square around what a tap would clear.</span>
<span class="role">Removes whole strokes. The eraser end of a pen does the same — and so does the upper side button, because Windows reports the two identically — and hovering with it draws a dashed square around what a tap would clear.</span>
</div>
<div class="toolrow">
<svg class="glyph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.28 6.03c-.3.3-.77.3-1.06 0l-1.47-1.47v3.69a.75.75 0 0 1-1.5 0V4.56L9.78 6.03a.75.75 0 0 1-1.06-1.06l2.75-2.75a.75.75 0 0 1 1.06 0l2.75 2.75c.3.3.3.77 0 1.06Zm-9.25 8.19a.75.75 0 1 1-1.06 1.06l-2.75-2.75a.75.75 0 0 1 0-1.06l2.75-2.75a.75.75 0 0 1 1.06 1.06l-1.47 1.47h3.69a.75.75 0 0 1 0 1.5H4.56l1.47 1.47Zm11.94 1.06a.75.75 0 0 1 0-1.06l1.47-1.47h-3.69a.75.75 0 0 1 0-1.5h3.69l-1.47-1.47a.75.75 0 0 1 1.06-1.06l2.75 2.75a.75.75 0 0 1 0 1.06l-2.75 2.75c-.3.3-.77.3-1.06 0Zm-2.69 2.69a.75.75 0 0 0-1.06 0l-1.47 1.47v-3.69a.75.75 0 0 0-1.5 0v3.69l-1.47-1.47a.75.75 0 0 0-1.06 1.06l2.75 2.75a.75.75 0 0 0 1.06 0l2.75-2.75c.3-.3.3-.77 0-1.06Z"/></svg>
Expand Down Expand Up @@ -902,11 +902,11 @@ <h3>The command strip</h3>
</figure>

<h3>Preferences</h3>
<p><span class="ui">Help → Preferences</span> is a searchable list: which monitor to open on, start full screen, finger drawing, snippet format order, laser trail timing and weight, toolbar placement, and the daily new-version check. <span class="ui">Help → About</span> shows the version. New settings are more rows in Preferences, not a new dialog.</p>
<p><span class="ui">Help → Preferences</span> is a searchable list: which monitor to open on, start full screen, finger drawing, what the pen's barrel button does, snippet format order, laser trail timing and weight, toolbar placement and layout, and the daily new-version check. Settings about how something looks — the trail weight, the barrel button, where the toolbar sits and how it is laid out — draw their options rather than naming them, so the choice is made by looking. <span class="ui">Help → About</span> shows the version. New settings are more rows in Preferences, not a new dialog.</p>

<figure class="fig">
<div class="figbox">
<svg viewBox="0 0 700 340" role="img" aria-label="The Preferences dialog: a search box across the top, the four categories down the left, and the settings of the selected category as rows on the right.">
<svg viewBox="0 0 700 340" role="img" aria-label="The Preferences dialog: a search box across the top, the categories down the left, and the settings of the selected category as rows on the right.">
<title>The Preferences dialog</title>
<rect width="700" height="340" rx="10" fill="var(--fig-ground)"/>
<rect x="20" y="14" width="660" height="312" rx="12" fill="var(--fig-surface)" stroke="var(--fig-line)"/>
Expand Down
Loading