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
+35
-36
@@ -16,6 +16,13 @@
|
||||
* stroke="#1f4fd8" ...>
|
||||
* <path d="M... Q..." stroke-width="6.9"/> what is shown
|
||||
* </g>
|
||||
* <g data-stroke=".." data-filled="" data-shape="arrow" ...> a filled shape (an arrow)
|
||||
* <path d="M... L... Z" fill="#1f4fd8"/>
|
||||
* </g>
|
||||
* <g data-stroke=".." data-shape="rectangle" data-fill="#fdd835" ...>
|
||||
* <path d="M... L... Z" fill="#fdd835" stroke="none"/> the fill beneath the line
|
||||
* <path d="M... Q..." stroke-width="7"/>
|
||||
* </g>
|
||||
* <g data-text="<text id>" data-x=".." data-y=".." data-width=".." ...>
|
||||
* <desc>the text as typed</desc>
|
||||
* <text ...><tspan>one line as shown</tspan>...</text>
|
||||
@@ -28,7 +35,7 @@
|
||||
|
||||
import { fromBase64, toBase64 } from './crypto.js';
|
||||
import {
|
||||
POINT_SIZE, Page, cleanHide, cleanImage, cleanName, cleanPageDescriptor, cleanStroke, cleanText, pointCount, textBounds, validId,
|
||||
POINT_SIZE, Page, cleanHide, cleanImage, cleanName, cleanPageDescriptor, cleanStroke, cleanText, isClosed, pointCount, validId,
|
||||
} from './page_model.js';
|
||||
import { partAt } from './stroke_geometry.js';
|
||||
import { LEFT_GAP, baselineOf } from './text_layout.js';
|
||||
@@ -36,14 +43,11 @@ import { LEFT_GAP, baselineOf } from './text_layout.js';
|
||||
/** @typedef {import('./page_model.js').Stroke} Stroke */
|
||||
|
||||
const FORMAT_VERSION = '1';
|
||||
/** A text rectangle's frame: the palette's red, 0.2 mm (Andy's teacher, 2026-10-08). */
|
||||
export const TEXT_FRAME_COLOR = '#e53935';
|
||||
export const TEXT_FRAME_WIDTH = 2;
|
||||
/**
|
||||
* Free text is marked by an L at its top-left corner, two segments this
|
||||
* long (5 mm; 3 mm first), in the frame's red (Andy, 2026-10-09).
|
||||
*/
|
||||
export const TEXT_MARK = 50;
|
||||
// (Until 2026-10-14 free text was marked by a red L of 5 mm at its
|
||||
// top-left corner, page content, and until 2026-10-14 a text rectangle
|
||||
// by a red frame on a white ground; a text block now draws only its
|
||||
// text, its extent shown by blue corners on screen while the pointer
|
||||
// hovers over it. An old file's marks are read as nothing.)
|
||||
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||
|
||||
/**
|
||||
@@ -97,20 +101,11 @@ function textElements(block) {
|
||||
const lines = block.lines.map((line, i) =>
|
||||
`<tspan x="${number(block.x + block.size * LEFT_GAP)}"` +
|
||||
` y="${number(block.y + baselineOf(block.size, block.font, i))}">${escapedContent(line)}</tspan>`);
|
||||
const bounds = textBounds(block);
|
||||
// A text rectangle's red frame, or free text's corner mark, is part of
|
||||
// the page (a rectangle's white ground is written beneath all layers:
|
||||
// see pageToSvg).
|
||||
const frame = block.height === null
|
||||
? `<path d="M${block.x} ${block.y + TEXT_MARK}V${block.y}H${block.x + TEXT_MARK}"` +
|
||||
` fill="none" stroke="${TEXT_FRAME_COLOR}" stroke-width="${TEXT_FRAME_WIDTH}"/>`
|
||||
: `<rect x="${bounds.left}" y="${bounds.top}" width="${bounds.width}" height="${bounds.height}"` +
|
||||
` fill="none" stroke="${TEXT_FRAME_COLOR}" stroke-width="${TEXT_FRAME_WIDTH}"/>`;
|
||||
return `<g data-text="${escaped(block.id)}" data-x="${block.x}" data-y="${block.y}"` +
|
||||
` data-width="${block.width ?? ''}" data-height="${block.height ?? ''}"` +
|
||||
` data-size="${block.size}" data-font="${block.font}"` +
|
||||
` data-box="${block.boxWidth} ${block.boxHeight}">` +
|
||||
`<desc>${escapedContent(block.text)}</desc>${frame}` +
|
||||
`<desc>${escapedContent(block.text)}</desc>` +
|
||||
`<text font-family="${block.font}" font-size="${block.size}" fill="${block.color}"` +
|
||||
` xml:space="preserve">${lines.join('')}</text></g>`;
|
||||
}
|
||||
@@ -134,12 +129,22 @@ function number(value) {
|
||||
function strokeShapes(stroke) {
|
||||
const count = pointCount(stroke);
|
||||
if (count === 0) return '';
|
||||
/** The closed polygon of the points, filled with a colour. */
|
||||
const polygon = (/** @type {string} */ color) => {
|
||||
const p = stroke.points;
|
||||
let path = '';
|
||||
for (let i = 0; i < p.length; i += POINT_SIZE) path += `${i ? 'L' : 'M'}${number(p[i])} ${number(p[i + 1])}`;
|
||||
return `<path d="${path}Z" fill="${color}" stroke="none"/>`;
|
||||
};
|
||||
// A filled stroke (the Shape tool's arrow): its outline, closed and filled.
|
||||
if (stroke.filled) return polygon(stroke.color);
|
||||
// A shape's fill colour lies beneath its line.
|
||||
let shapes = stroke.fill && (stroke.shape !== 'line' || isClosed(stroke)) ? polygon(stroke.fill) : '';
|
||||
if (count === 1) {
|
||||
const dot = partAt(stroke, 0);
|
||||
return `<circle cx="${number(dot.x)}" cy="${number(dot.y)}" r="${number(dot.width / 2)}"` +
|
||||
return shapes + `<circle cx="${number(dot.x)}" cy="${number(dot.y)}" r="${number(dot.width / 2)}"` +
|
||||
` fill="${stroke.color}" stroke="none"/>`;
|
||||
}
|
||||
let shapes = '';
|
||||
let path = '';
|
||||
let pathWidth = '';
|
||||
for (let k = 0; k < count; k++) {
|
||||
@@ -184,20 +189,9 @@ export function pageToSvg(page, { session, hostId, hidden }) {
|
||||
const shown = imageElement(image);
|
||||
if (shown) lines.push(shown);
|
||||
}
|
||||
// The white grounds of the text rectangles: above the images, beneath
|
||||
// every layer, so that they hide the image and never anyone's writing.
|
||||
const grounds = [];
|
||||
for (const id of page.layerOrder(hostId)) {
|
||||
const layer = page.layers.get(id);
|
||||
if (!layer || hidden?.has(id)) continue;
|
||||
for (const block of layer.texts.values()) {
|
||||
if (block.height === null) continue;
|
||||
const b = textBounds(block);
|
||||
grounds.push(`<rect data-ground="${escaped(block.id)}" x="${b.left}" y="${b.top}"` +
|
||||
` width="${b.width}" height="${b.height}" fill="#ffffff"/>`);
|
||||
}
|
||||
}
|
||||
if (grounds.length) lines.push('<g data-grounds="">', ...grounds, '</g>');
|
||||
// (Until 2026-10-14 a <g data-grounds> of white rectangles stood here,
|
||||
// one beneath each text rectangle; a file that has one is read as if
|
||||
// it had none.)
|
||||
for (const id of page.layerOrder(hostId)) {
|
||||
const layer = page.layers.get(id);
|
||||
if (!layer || layer.isEmpty() || hidden?.has(id)) continue;
|
||||
@@ -206,7 +200,9 @@ export function pageToSvg(page, { session, hostId, hidden }) {
|
||||
if (pointCount(stroke) === 0) continue;
|
||||
lines.push(
|
||||
`<g data-stroke="${escaped(stroke.id)}" data-width="${stroke.width}"` +
|
||||
` data-points="${stroke.points.join(' ')}" stroke="${stroke.color}" fill="none"` +
|
||||
` data-points="${stroke.points.join(' ')}"${stroke.filled ? ' data-filled=""' : ''}` +
|
||||
`${stroke.fill ? ` data-fill="${stroke.fill}"` : ''}${stroke.shape ? ` data-shape="${stroke.shape}"` : ''}` +
|
||||
` stroke="${stroke.color}" fill="none"` +
|
||||
` stroke-linecap="round" stroke-linejoin="round">${strokeShapes(stroke)}</g>`);
|
||||
}
|
||||
for (const block of layer.texts.values()) lines.push(textElements(block));
|
||||
@@ -289,6 +285,9 @@ export function pageFromSvg(text) {
|
||||
width: Number(element.getAttribute('data-width')),
|
||||
points,
|
||||
done: true,
|
||||
filled: element.hasAttribute('data-filled'),
|
||||
fill: element.getAttribute('data-fill'),
|
||||
shape: element.getAttribute('data-shape'),
|
||||
});
|
||||
if (stroke && points.length >= POINT_SIZE) layer.strokes.set(stroke.id, stroke);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user