Skip to content
Open
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
5 changes: 3 additions & 2 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:
- Undo and redo for strokes, erasing, containers, text edits, and transformations
- Versioned ZIP-based `.wboard` documents with an embedded `preview.png`. Explorer shows that picture as the file thumbnail in the released install. The VS Code extension in `vscode/sqlbi-whiteboard` opens the same picture instead of the ZIP.
- Markdown `.wimport` recipes that build image and text containers from headings
- **File → Export** to PowerPoint or PDF: the board is cut into areas where it is empty, one slide or page per area, with an overview first. A deck carries the text containers in the speaker notes, or, as Editable slides, puts images and text containers on the slide as objects with the ink over them; a PDF has a bookmark per page, can put the whole board on one page to zoom into, and as Vector pages carries the ink as paths and the text as selectable text. The dialog shows the areas numbered on the board and updates as the settings change. [docs/export.md](docs/export.md) explains how areas are chosen
- **File → Export** to PowerPoint or PDF: the board is cut into areas where it is empty, one slide or page per area, with an overview first. A deck carries the text containers in the speaker notes, or, as Editable slides, puts images and text containers on the slide as objects with the ink over them; a PDF has a bookmark per page, can put the whole board on one page to zoom into, and as Vector pages carries the ink as paths and the text as selectable text. The dialog shows the areas numbered on the board and updates as the settings change. **View → Frame** draws a slide on the board by hand: whatever sits inside a frame is that slide, and the rest of the board is cut automatically. [docs/export.md](docs/export.md) explains how areas are chosen
- 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, mouse drawing, the pen button, snippet format order, laser trail timing and weight, toolbar position and layout, keeping the Eraser on the toolbar for a pen that has no reverse end, and (except Store installs) a daily new-version check
Expand Down Expand Up @@ -196,7 +196,8 @@ Use **Copy settings** after finding a useful combination so the exact values can
| Ctrl+Z / Ctrl+Y | Undo / redo |
| Ctrl+C | Copy the selection. Copying a LiveView copies its last frame as a bitmap |
| Ctrl+V | Paste prefers an image (including a file on the clipboard) over text. Otherwise create a text container from plain text |
| F2 | Edit the selected text container |
| F2 | Edit the selected text container, or rename the selected frame |
| View > Frame | Add a frame the size of the screen: a slide drawn on the board, selected by its edge or its tab, which Export takes as it is |
| Language chip | Choose Plain text, DAX, or SQL Server on a selected text container |
| F6 | Format DAX or SQL on the selected text container. In F2, formats in place |
| Ctrl+Enter | Commit the F2 edit, including an F6 format done in that session, and return to display mode |
Expand Down
24 changes: 24 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,30 @@ later, because the driver, not the application, asks for the file name.

---

## 27. A frame is a slide drawn on the board, and it is not a container

**Implemented.** Phase E5 of [export.md](export.md); the smaller calls are under E5 in
[export-decisions.md](export-decisions.md).

The automatic cut (decision 26) serves a board that was drawn without a deck in mind.
Someone preparing a board for a deck wants to say where the slides are, and the way to
say it on a whiteboard is to draw a rectangle. **View → Frame** adds one the size of the
screen; it can be moved, resized, renamed, and reordered like anything else selectable,
and Export takes it as it is: whatever sits inside a frame is that slide, frames come
first, and the rest of the board is cut automatically.

A frame is deliberately not a container. The one rule the board has about containers is
that a stroke touching exactly one of them is linked to it, and a frame around a picture
would break that rule for every stroke on the picture. So a frame links nothing, is
selected only by its edge or its title tab, and moving it moves nothing inside it: it is
a label over content, read at export time.

Frames are the first object to change the file format since 1.0, and the change is kept
as small as it can be: a board is written as version 6 only when it holds a frame, so a
board without one still opens in every release since format 5.

---

## Open questions

- arm64 is not built; add it if Surface devices matter for a pen application.
Expand Down
33 changes: 33 additions & 0 deletions docs/export-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,36 @@ which phase made it and why.
a future language service names falls back to Segoe UI until it is added to the
resolver.
- **The overview page stays a picture** in both modes; it is a map, not content.

## E5 — Frames

- **A frame is not a container.** It implements no container interface, so strokes never
link to it and it never takes part in the single-container test: a frame drawn around
a picture does not stop ink from linking to the picture. Deleting a frame deletes only
the frame.
- **A frame is selected by its edge or its title tab, never by its inside**, so
everything inside it stays reachable to the pen and the mouse. The band is 8 screen
pixels either side of the edge and the tab 160 by 22 screen pixels at the top-left
corner, both divided by the zoom so they feel the same at any magnification.
- **View → Frame** (mnemonic A) adds a frame the size of the visible board, inset by 8%
so its edge is under the pen and not under the window's edge, titled "Slide n", and
selects it. F2 on a selected frame renames it through a small dialog. There is no
shortcut for adding one; the command strip is reachable with Alt+V.
- **Moving or resizing a frame moves nothing inside it.** A frame is a label over
content, and the export reads what it covers at export time. Resizing keeps the
aspect, as the resize gesture does for everything else, which for a frame created at
the screen's shape means it stays slide-shaped.
- **Frames are drawn over everything on screen** as a dashed outline with a filled tab,
and left out of every export picture, the overview, and `preview.png`.
- **Frames win in the partitioner.** Every unit whose center lies inside a frame belongs
to that frame, frames come first (creation order under Drawing order, top-left to
bottom-right under Reading order), the frame's own rectangle is the area, its title is
the slide title when it has one, and whatever is left is partitioned as before. A frame
is never an object of an area, so it never appears in an export.
- **Bring to front and Send to back work on a frame**, and since drawing order sorts
frames by z-index, they are also how the slide order is changed.
- **The archive version moves to 6 only for a board that holds a frame.** A board
without one is still written as version 5 and keeps opening in every release since
format 5; a board with frames asks for this release. The frame's title travels in the
DTO's text title field rather than a new one.
- **A frame is not copied** by Ctrl+C: there is nothing on the clipboard a frame could be.
14 changes: 8 additions & 6 deletions docs/export.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Export: PowerPoint, PDF, and Replay

**Proposed. Nothing below is implemented.** This document is the plan for three ways of
getting a board out of the application: a PowerPoint deck, a PDF, and a replay of the
session. The first two share almost everything and are planned to be built; the third is
sketched at the level of modules and file format so that it can be judged and scheduled,
not started. When any part is accepted it becomes a numbered entry in
[decisions.md](decisions.md), and this document is kept for the alternatives.
**PowerPoint and PDF are implemented (phases E1 to E5); Replay is not.** This document is
the plan for three ways of getting a board out of the application: a PowerPoint deck, a
PDF, and a replay of the session. The first two are built as described here, with the
recommendations below taken as decisions: decisions 26 and 27 in
[decisions.md](decisions.md) are the summary, and [export-decisions.md](export-decisions.md)
lists every smaller call made while implementing. The third is sketched at the level of
modules and file format so that it can be judged and scheduled, not started. This document
is kept for the reasoning and the alternatives.

Background: [README.md](../README.md) for what the application does, and
[decisions.md](decisions.md) for the constraints a new dependency has to meet — decision 21
Expand Down
1 change: 1 addition & 0 deletions site/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ <h3>Drawing with a mouse</h3>
<h3>Containers</h3>
<p>Images, LiveViews, and text blocks are containers. With a mouse, click to move, drag the circular handle to resize while keeping aspect ratio — hold <kbd>Ctrl</kbd> too with <span class="ui">Mouse drawing</span> on. Releasing the mouse returns to the drawing tool you had. Imported images accept PNG, JPEG, BMP, GIF, and SVG, including clipboard paste and Explorer drag-and-drop. An SVG is kept as its markup and redrawn at every size, so enlarging one costs it nothing.</p>
<p><span class="ui">View → Bring to front</span> and <span class="ui">View → Send to back</span> reorder the selected container and its linked strokes. Strokes cannot be reordered on their own.</p>
<p><span class="ui">View → Frame</span> adds a frame the size of the screen: a slide drawn on the board. A frame is selected by its dashed edge or by its title tab, never by its inside, so everything in it stays reachable; drag it to move it, drag the handle to resize it, press <kbd>F2</kbd> to rename it, and <span class="ui">Delete</span> removes the frame alone. Moving a frame moves nothing inside it: it is a label over content. <span class="ui">Export</span> takes frames as they are, first and in the order they were added, and cuts the rest of the board automatically. Frames never appear in an export or in a preview.</p>

<h3>Text, SQL, and DAX</h3>
<p>Paste prefers an image when the clipboard has one, including a file SnagIt or another capture tool left on the clipboard. Paste plain text to create a selected text container.</p>
Expand Down
4 changes: 2 additions & 2 deletions site/shortcuts.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,14 @@ <h2><span class="secico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"
<div class="keyrow"><span><kbd>Shift</kbd><span class="plus">+</span><kbd>F12</kbd></span><span class="act">Save As. Commits an active text edit first.</span></div>
<div class="keyrow"><span><kbd>Ctrl</kbd><span class="plus">+</span><kbd>E</kbd></span><span class="act">Export the board to PowerPoint or PDF. Commits an active text edit first.</span></div>
<div class="keyrow"><span><kbd>Delete</kbd></span><span class="act">Delete the selected container and its linked strokes.</span></div>
<div class="keyrow"><span><kbd>F2</kbd></span><span class="act">Edit the selected text container.</span></div>
<div class="keyrow"><span><kbd>F2</kbd></span><span class="act">Edit the selected text container, or rename the selected frame.</span></div>
<div class="keyrow"><span><kbd>F6</kbd></span><span class="act">Format DAX or SQL Server on the selected text container. In <kbd>F2</kbd>, formats in place; <kbd>Ctrl</kbd><span class="plus">+</span><kbd>Enter</kbd> then commits.</span></div>
<div class="keyrow"><span><kbd>Ctrl</kbd><span class="plus">+</span><kbd>Enter</kbd></span><span class="act">Commit the F2 edit, including an F6 format done in that session, and return to display mode.</span></div>
<div class="keyrow"><span><kbd>Escape</kbd></span><span class="act">Cancel the F2 edit (including an uncommitted F6), close the command strip, or leave full screen or canvas only.</span></div>
<div class="keyrow"><span><kbd>Alt</kbd><span class="plus">+</span><kbd>L</kbd></span><span class="act">Laser pointer.</span></div>
<div class="keyrow"><span><kbd>F11</kbd></span><span class="act">Fill the current monitor and hide the title and tabs. Press again or <kbd>Escape</kbd> to leave.</span></div>
<div class="keyrow"><span><kbd>Ctrl</kbd><span class="plus">+</span><kbd>F11</kbd></span><span class="act">Canvas only: hide the title and tabs, keep this window’s size and place. Press again or <kbd>Escape</kbd> to leave.</span></div>
<div class="keyrow"><span><kbd>Alt</kbd><span class="plus">+</span><kbd>F</kbd> <span class="plus">/</span> <kbd>E</kbd> <span class="plus">/</span> <kbd>V</kbd> <span class="plus">/</span> <kbd>H</kbd></span><span class="act">Open or switch the File, Edit, View, or Help strip. Alt always selects a tab. With a strip open, letters without Alt run a command. File: N New, O Open, S Save, A Save As, E Export. Edit: Z Undo, Y Redo, C Copy, V Paste. View: F Full screen, C Canvas only, B Bring to front, S Send to back, L LiveView, Z Freeze, D Disconnect, R Reconnect. Help: P Preferences, A About. Left, right, Home, and End move between commands.</span></div>
<div class="keyrow"><span><kbd>Alt</kbd><span class="plus">+</span><kbd>F</kbd> <span class="plus">/</span> <kbd>E</kbd> <span class="plus">/</span> <kbd>V</kbd> <span class="plus">/</span> <kbd>H</kbd></span><span class="act">Open or switch the File, Edit, View, or Help strip. Alt always selects a tab. With a strip open, letters without Alt run a command. File: N New, O Open, S Save, A Save As, E Export. Edit: Z Undo, Y Redo, C Copy, V Paste. View: F Full screen, C Canvas only, B Bring to front, S Send to back, A Frame, L LiveView, Z Freeze, D Disconnect, R Reconnect. Help: P Preferences, A About. Left, right, Home, and End move between commands.</span></div>
</div>
</section>

Expand Down
39 changes: 32 additions & 7 deletions src/SQLBI.Whiteboard.Core/Export/BoardPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,31 @@ public static IReadOnlyList<ExportArea> Partition(
options ??= ExportLayoutOptions.Default;

var units = BuildUnits(document.Objects);
var areas = new List<ExportArea>();

// Frames win: whatever sits inside one belongs to it, and frames come
// first, in the order they were drawn or in reading order.
IEnumerable<FrameBoardObject> frames = options.Order == AreaOrder.Reading
? document.Frames.OrderBy(frame => frame.Bounds.Top).ThenBy(frame => frame.Bounds.Left)
: document.Frames;
foreach (var frame in frames)
{
var inside = units.Where(unit => frame.Bounds.Contains(unit.Bounds.Center)).ToArray();
units.RemoveAll(inside.Contains);
var objects = inside.SelectMany(unit => unit.Objects).OrderBy(item => item.ZIndex).ToArray();
areas.Add(new ExportArea(
areas.Count + 1,
frame.Bounds,
objects,
string.IsNullOrWhiteSpace(frame.Title)
? ResolveTitle(document, objects, titleResolver)
: frame.Title.Trim(),
TextScaleFor(frame.Bounds, options)));
}

if (units.Count == 0)
{
return [];
return areas;
}

var leaves = new List<Region>();
Expand All @@ -33,14 +55,17 @@ public static IReadOnlyList<ExportArea> Partition(
? leaves.OrderBy(region => region.MinZIndex)
: leaves;

return ordered
.Select((region, index) => new ExportArea(
index + 1,
foreach (var region in ordered)
{
areas.Add(new ExportArea(
areas.Count + 1,
region.Bounds,
region.Objects,
ResolveTitle(document, region.Objects, titleResolver),
TextScaleFor(region.Bounds, options)))
.ToArray();
TextScaleFor(region.Bounds, options)));
}

return areas;
}

/// <summary>
Expand Down Expand Up @@ -119,7 +144,7 @@ private static List<Unit> BuildUnits(IReadOnlyList<BoardObject> objects)
{
units.Add(Unit.Of([stroke]));
}
else if (item is not InkStrokeObject && item is not IBoardContainer)
else if (item is not InkStrokeObject && item is not IBoardContainer && item is not FrameBoardObject)
{
units.Add(Unit.Of([item]));
}
Expand Down
12 changes: 10 additions & 2 deletions src/SQLBI.Whiteboard.Core/Model/BoardDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,19 @@ public IEnumerable<BoardObject> Query(RectD worldBounds) =>
.OrderByDescending(image => image.ZIndex)
.FirstOrDefault();

public BoardObject? HitTestTopContainer(PointD worldPoint) =>
_objects.Where(item => item is IBoardContainer && item.Bounds.Contains(worldPoint))
/// <summary>
/// The topmost thing a select gesture lands on: a container anywhere inside
/// it, or a frame by its edge or tab. The zoom sizes the frame's band.
/// </summary>
public BoardObject? HitTestTopContainer(PointD worldPoint, double zoom = 1) =>
_objects.Where(item =>
(item is IBoardContainer && item.Bounds.Contains(worldPoint)) ||
(item is FrameBoardObject frame && frame.HitTest(worldPoint, zoom)))
.OrderByDescending(item => item.ZIndex)
.FirstOrDefault();

public IEnumerable<FrameBoardObject> Frames => _objects.OfType<FrameBoardObject>();

public BoardObject? FindSingleTouchedContainer(InkStrokeObject stroke)
{
BoardObject? match = null;
Expand Down
39 changes: 39 additions & 0 deletions src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,45 @@ public sealed record LiveViewBoardObject(
bool CaptureCursor = false,
bool IsFrozen = false) : BoardObject(Id, ZIndex, Bounds), IBoardContainer;

/// <summary>
/// A rectangle the author draws to say "this is a slide". It is not a container:
/// strokes never link to it, and it never takes part in the single-container
/// test, so a frame around a picture does not stop ink from linking to the
/// picture. It is selected by its edge or its title tab, not by its inside,
/// which is how the things inside it stay reachable.
/// </summary>
public sealed record FrameBoardObject(
Guid Id,
int ZIndex,
RectD Bounds,
string Title) : BoardObject(Id, ZIndex, Bounds)
{
/// <summary>
/// Screen pixels, divided by the zoom when hit testing, so the band and the
/// tab stay the same size under the pen whatever the zoom.
/// </summary>
public const double EdgeBand = 8;
public const double TabWidth = 160;
public const double TabHeight = 22;

public RectD TabRect(double zoom) => new(
Bounds.Left,
Bounds.Top,
Math.Min(Bounds.Width, TabWidth / Math.Max(zoom, 0.000001)),
Math.Min(Bounds.Height, TabHeight / Math.Max(zoom, 0.000001)));

public bool HitTest(PointD worldPoint, double zoom)
{
var band = EdgeBand / Math.Max(zoom, 0.000001);
if (!Bounds.Inflate(band).Contains(worldPoint))
{
return false;
}

return !Bounds.Inflate(-band).Contains(worldPoint) || TabRect(zoom).Contains(worldPoint);
}
}

public sealed record BoardAsset(
string Id,
string OriginalFileName,
Expand Down
Loading