The Shape tool, implied rectangles, and the palette floating again
A Shapes button right of Text draws a rectangle, an ellipse, a line of taps or an arrow in the pen's colour and width, with a fill colour of its own; a hold, a secondary press or two quick clicks open its menu. Text and Hide rectangles draw nothing on the page and show blue corners while the pointer hovers over them; free text's red corner mark is gone. Arrange moves and resizes shapes. The colour palette floats below "+" again instead of reflowing the toolbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
46a82bf3b7
commit
03907da8c9
12 files changed
+1502
-297
No files matched your search
+62
-10
@@ -35,8 +35,24 @@ export const IMAGE_MARGIN = 50;
|
||||
* @property {number[]} points x, y, pressure for each point, in sequence.
|
||||
* Pressure is 0 to 1.
|
||||
* @property {boolean} done False while the stroke is being drawn.
|
||||
* @property {boolean} filled The points are the outline of a closed
|
||||
* polygon filled with the colour, rather than a line (the Shape
|
||||
* tool's arrow, 2026-10-14); the width then matters only to the
|
||||
* eraser's and the page's reckoning of the stroke's reach.
|
||||
* @property {string | null} fill A colour the closed polygon of the
|
||||
* points is filled with beneath the line (the Shape tool's fill
|
||||
* colour, Andy, 2026-10-14), or null.
|
||||
* @property {ShapeKind | null} shape Which shape of the Shape tool the
|
||||
* stroke is, so that Arrange can move and resize it, or null for a
|
||||
* stroke of the pen.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The shapes of the Shape tool (shapes.js), as a stroke records its kind.
|
||||
* @typedef {'rectangle' | 'ellipse' | 'line' | 'arrow'} ShapeKind
|
||||
*/
|
||||
export const SHAPE_KINDS = /** @type {const} */ (['rectangle', 'ellipse', 'line', 'arrow']);
|
||||
|
||||
/** Numbers per point in Stroke.points. */
|
||||
export const POINT_SIZE = 3;
|
||||
|
||||
@@ -49,10 +65,13 @@ export const POINT_SIZE = 3;
|
||||
* @property {number} y
|
||||
* @property {number | null} width Page units, or null for no wrapping.
|
||||
* @property {number | null} height With a width: the block is a *text
|
||||
* rectangle* (Andy's teacher, 2026-10-08), drawn with a red frame on
|
||||
* a white ground that hides the image beneath, wrapping inside and
|
||||
* growing downward by lines when the text needs more. Null for free
|
||||
* text, which is only as large as its lines.
|
||||
* rectangle* (Andy's teacher, 2026-10-08), wrapping inside and
|
||||
* growing downward by lines when the text needs more; an *implied*
|
||||
* rectangle (Andy, 2026-10-14), marked on screen by blue corners and
|
||||
* drawing nothing on the page (a red frame on a white ground until
|
||||
* then, from a misreading of the teacher's rectangles, which are
|
||||
* marks of their own: the Shape tool). Null for free text, which is
|
||||
* only as large as its lines.
|
||||
* @property {string} text As typed, with the writer's line breaks.
|
||||
* @property {string[]} lines As shown, laid out by the writer's client.
|
||||
* @property {number} size Font size in page units.
|
||||
@@ -153,12 +172,44 @@ export function cleanStroke(received) {
|
||||
!(received.width > 0 && received.width <= MAX_STROKE_WIDTH)) return null;
|
||||
const points = received.points ?? [];
|
||||
if (!validPoints(points)) return null;
|
||||
const fill = typeof received.fill === 'string' && COLOR_PATTERN.test(received.fill) ? received.fill : null;
|
||||
const shape = SHAPE_KINDS.find((kind) => kind === received.shape) ?? null;
|
||||
return {
|
||||
id: received.id, color: received.color, width: received.width,
|
||||
points: points.slice(), done: received.done !== false,
|
||||
points: points.slice(), done: received.done !== false, filled: received.filled === true, fill, shape,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rectangle around a stroke's points (not its line's width).
|
||||
* @param {Stroke} stroke
|
||||
* @returns {Rectangle | null} Null for a stroke without points.
|
||||
*/
|
||||
export function strokeBox(stroke) {
|
||||
const p = stroke.points;
|
||||
if (p.length < POINT_SIZE) return null;
|
||||
let left = Infinity;
|
||||
let top = Infinity;
|
||||
let right = -Infinity;
|
||||
let bottom = -Infinity;
|
||||
for (let i = 0; i < p.length; i += POINT_SIZE) {
|
||||
left = Math.min(left, p[i]);
|
||||
right = Math.max(right, p[i]);
|
||||
top = Math.min(top, p[i + 1]);
|
||||
bottom = Math.max(bottom, p[i + 1]);
|
||||
}
|
||||
return { left, top, width: right - left, height: bottom - top };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Stroke} stroke
|
||||
* @returns {boolean} Whether a stroke of a line shape ends where it began.
|
||||
*/
|
||||
export function isClosed(stroke) {
|
||||
const p = stroke.points;
|
||||
return p.length >= 2 * POINT_SIZE && p[0] === p[p.length - POINT_SIZE] && p[1] === p[p.length - POINT_SIZE + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a text block that came from outside.
|
||||
* @param {any} received
|
||||
@@ -563,8 +614,7 @@ export class Page {
|
||||
contentSheet() {
|
||||
// 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
|
||||
// the image's box, which counts). 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. */
|
||||
@@ -585,13 +635,15 @@ export class Page {
|
||||
};
|
||||
for (const layer of this.layers.values()) {
|
||||
for (const stroke of layer.strokes.values()) {
|
||||
if (isPaperWhite(stroke.color)) continue;
|
||||
if (isPaperWhite(stroke.color) && (stroke.fill === null || isPaperWhite(stroke.fill))) 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;
|
||||
// A text rectangle draws nothing of its own since 2026-10-14, so
|
||||
// white text in one is as invisible as white free text.
|
||||
if (isPaperWhite(block.color)) continue;
|
||||
const b = textBounds(block);
|
||||
add(b.left, b.top, b.width, b.height);
|
||||
}
|
||||
@@ -641,7 +693,7 @@ export class Page {
|
||||
includeText(block) {
|
||||
const bounds = textBounds(block);
|
||||
const corners = [bounds.left, bounds.top, 0, bounds.left + bounds.width, bounds.top + bounds.height, 0];
|
||||
this.includeStroke({ id: '', color: '', width: 0, points: corners, done: true });
|
||||
this.includeStroke({ id: '', color: '', width: 0, points: corners, done: true, filled: false, fill: null, shape: null });
|
||||
}
|
||||
|
||||
/** Find where strokes and text lie again, after some were removed or replaced. */
|
||||
|
||||
Reference in new issue
Block a user