Shrink ignores what cannot be seen; blank lines at a block's end go

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 01:48:56 +02:00
1 parent bb41047e85
commit 56ab259cd4
5 files changed
+158 -11

No files matched your search

+43 -9
View File
@@ -184,6 +184,17 @@ export function cleanText(received) {
};
}
/** The paper's colour, in which a mark cannot be seen. */
export const PAPER_WHITE = '#ffffff';
/**
* @param {string} color
* @returns {boolean} Whether a mark of this colour is invisible on the paper.
*/
export function isPaperWhite(color) {
return color.trim().toLowerCase() === PAPER_WHITE;
}
/**
* The rectangle a text block takes up: its own, grown to its lines, for a
* text rectangle; the lines' box for free text.
@@ -550,20 +561,43 @@ export class Page {
* @returns {Rectangle | null} Null if the page is blank.
*/
contentSheet() {
/** @type {Rectangle | null} */
let box = this.strokeBounds ? { ...this.strokeBounds } : null;
for (const item of [...this.images, ...this.hides]) {
// What can be seen (Andy, 2026-10-14): a stroke or free text in the
// paper's white is left out (over an image it shows, but lies within
// the image's box, which counts); a text rectangle counts whole, its
// red frame being visible. The record that grows the page while
// writing (strokeBounds) is not used here, so a white stroke still
// gets room as it is written.
/** @type {{box: Rectangle | null}} A holder, since the closure's assignments are not seen by the type checker. */
const found = { box: null };
/** @param {number} left @param {number} top @param {number} width @param {number} height */
const add = (left, top, width, height) => {
const box = found.box;
if (!box) {
box = { left: item.x, top: item.y, width: item.width, height: item.height };
continue;
found.box = { left, top, width, height };
return;
}
const right = Math.max(box.left + box.width, item.x + item.width);
const bottom = Math.max(box.top + box.height, item.y + item.height);
box.left = Math.min(box.left, item.x);
box.top = Math.min(box.top, item.y);
const right = Math.max(box.left + box.width, left + width);
const bottom = Math.max(box.top + box.height, top + height);
box.left = Math.min(box.left, left);
box.top = Math.min(box.top, top);
box.width = right - box.left;
box.height = bottom - box.top;
};
for (const layer of this.layers.values()) {
for (const stroke of layer.strokes.values()) {
if (isPaperWhite(stroke.color)) continue;
const reach = stroke.width * 2; // As includeStroke reckons it.
const p = stroke.points;
for (let i = 0; i < p.length; i += POINT_SIZE) add(p[i] - reach, p[i + 1] - reach, 2 * reach, 2 * reach);
}
for (const block of layer.texts.values()) {
if (block.height === null && isPaperWhite(block.color)) continue;
const b = textBounds(block);
add(b.left, b.top, b.width, b.height);
}
}
for (const item of [...this.images, ...this.hides]) add(item.x, item.y, item.width, item.height);
const box = found.box;
if (!box) return null;
// The greater of the two margins (Andy, 2026-10-08), exactly, the same
// on all four sides (a grid made them uneven, Andy, 2026-10-08).
+10
View File
@@ -17,6 +17,7 @@ import {
MAX_PAGE_HIDES,
} from './page_model.js';
import { RelayConnection } from './relay_connection.js';
import { trimTrailingBlankLines } from './text_layout.js';
/** @typedef {import('./crypto.js').Identity} Identity */
/** @typedef {import('./page_model.js').Layer} Layer */
@@ -525,6 +526,15 @@ export class Session {
shrinkPage() {
const page = this.page;
if (!this.mayMakePages() || page.number === 0) return false;
// The blank lines at the end of one's own text blocks go first (Andy,
// 2026-10-14); another's blocks are theirs, trimmed when they finish
// typing. Each trim is a gesture of its own to undo; a block being
// typed in is left to the editor.
for (const block of this.ownLayer.texts.values()) {
if (!this.beginTextEdit(block.id, 'shrink')) continue;
if (trimTrailingBlankLines(block)) this.setText(block);
this.endTextEdit(block.id, 'shrink');
}
const sheet = page.contentSheet();
if (!sheet || (sheet.left === page.baseLeft && sheet.top === page.baseTop &&
sheet.width === page.baseWidth && sheet.height === page.baseHeight)) return false;
+6 -2
View File
@@ -14,7 +14,7 @@ import { randomId } from './crypto.js';
import { textBounds } from './page_model.js';
import { TEXT_FRAME_WIDTH, TEXT_MARK } from './page_file.js';
import { HOLD_MS, HOLD_SLACK_PX } from './pen_input.js';
import { LEFT_GAP, LINE_HEIGHT, TOP_GAP, baselineOf, fontMetrics, layoutText } from './text_layout.js';
import { LEFT_GAP, LINE_HEIGHT, TOP_GAP, baselineOf, fontMetrics, layoutText, trimTrailingBlankLines } from './text_layout.js';
/** @typedef {import('./page_model.js').TextBlock} TextBlock */
/** @typedef {import('./page_model.js').TextFont} TextFont */
@@ -206,7 +206,10 @@ export class TextEditor {
this.field.setSelectionRange(block.text.length, block.text.length);
}
/** Finish editing. An empty block is removed. */
/**
* Finish editing. The blank lines at the end go (Andy, 2026-10-14),
* and an empty block is removed.
*/
stop() {
const block = this.block;
if (!block) return;
@@ -215,6 +218,7 @@ export class TextEditor {
this.field.hidden = true;
this.mark.hidden = true;
this.field.value = '';
trimTrailingBlankLines(block);
if (block.text.trim() === '') this.target.removeText(block.id);
else this.target.setText(block);
this.target.endEdit?.(block.id);
+20
View File
@@ -146,3 +146,23 @@ export function layoutText(text, width, size, family) {
boxHeight: Math.ceil(size * TOP_GAP + lines.length * size * LINE_HEIGHT),
};
}
/**
* Remove the blank lines at the end of a block's text (Andy, 2026-10-14:
* they cannot be seen, and Shrink should keep no room for them) and lay
* the block out again. A text rectangle that is exactly as tall as its
* lines, which is what a box grown by typing is, shrinks back to the
* lines that remain; one dragged taller than its lines keeps its height,
* since that room was asked for.
* @param {import('./page_model.js').TextBlock} block
* @returns {boolean} Whether anything changed.
*/
export function trimTrailingBlankLines(block) {
const trimmed = block.text.replace(/(?:\n[ \t]*)+$/, '');
if (trimmed === block.text) return false;
const grown = block.height !== null && block.height === Math.ceil(block.boxHeight);
block.text = trimmed;
Object.assign(block, layoutText(block.text, block.width, block.size, block.font));
if (grown) block.height = Math.ceil(block.boxHeight);
return true;
}