Kokkoja as of 2026-10-10: the teacher's requests, the Session window, notices, the QR code, the license
This commit is contained in:
1 parent
95c80d04c9
commit
1d087af633
32 files changed
+4917
-890
No files matched your search
+252
-62
@@ -8,21 +8,24 @@
|
||||
*/
|
||||
|
||||
/** A blank page is A4. */
|
||||
export const BLANK_PAGE_WIDTH = 2100;
|
||||
export const BLANK_PAGE_HEIGHT = 2970;
|
||||
// An A4 sheet in landscape (Andy, 2026-10-08; portrait until then).
|
||||
export const BLANK_PAGE_WIDTH = 2970;
|
||||
export const BLANK_PAGE_HEIGHT = 2100;
|
||||
|
||||
/**
|
||||
* The page grows to keep this much room around everything written on it,
|
||||
* in steps of the same amount so that it does not shift with every pen
|
||||
* point. 8 mm, Andy's choice (2026-10-03; 15 mm before).
|
||||
* point; "Shrink" leaves the same room. 5 mm, Andy's choice (2026-10-08;
|
||||
* 8 mm from 2026-10-03, 15 mm before that).
|
||||
*/
|
||||
export const WRITING_MARGIN = 80;
|
||||
export const WRITING_MARGIN = 50;
|
||||
|
||||
/**
|
||||
* An image is placed this far inside the sheet's edge. 8 mm, Andy's
|
||||
* choice; it is deliberately a separate setting from WRITING_MARGIN.
|
||||
* An image is placed this far inside the sheet's edge, and below the
|
||||
* image above. 5 mm, Andy's choice (2026-10-08; 8 mm before); it is
|
||||
* deliberately a separate setting from WRITING_MARGIN.
|
||||
*/
|
||||
export const IMAGE_MARGIN = 80;
|
||||
export const IMAGE_MARGIN = 50;
|
||||
|
||||
/**
|
||||
* @typedef {object} Stroke
|
||||
@@ -45,6 +48,11 @@ export const POINT_SIZE = 3;
|
||||
* @property {number} x
|
||||
* @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.
|
||||
* @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.
|
||||
@@ -163,17 +171,62 @@ export function cleanText(received) {
|
||||
!received.lines.every((/** @type {unknown} */ line) => typeof line === 'string') ||
|
||||
![received.x, received.y].every((v) => Number.isFinite(v) && Math.abs(v) <= 10 * MAX_PAGE_SIZE) ||
|
||||
(received.width !== null && !(received.width >= 10 && received.width <= MAX_PAGE_SIZE)) ||
|
||||
(received.height != null && !(received.height >= 10 && received.height <= 10 * MAX_PAGE_SIZE)) ||
|
||||
!(received.size >= 10 && received.size <= 400) ||
|
||||
!TEXT_FONTS.includes(received.font) ||
|
||||
!(received.boxWidth >= 0 && received.boxWidth <= MAX_PAGE_SIZE) ||
|
||||
!(received.boxHeight >= 0 && received.boxHeight <= 10 * MAX_PAGE_SIZE)) return null;
|
||||
return {
|
||||
id: received.id, x: received.x, y: received.y, width: received.width,
|
||||
height: received.height == null || received.width === null ? null : received.height,
|
||||
text: received.text, lines: received.lines.slice(), size: received.size, font: received.font,
|
||||
color: received.color, boxWidth: received.boxWidth, boxHeight: received.boxHeight,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rectangle a text block takes up: its own, grown to its lines, for a
|
||||
* text rectangle; the lines' box for free text.
|
||||
* @param {TextBlock} block
|
||||
* @returns {Rectangle}
|
||||
*/
|
||||
export function textBounds(block) {
|
||||
if (block.width !== null && block.height !== null) {
|
||||
return { left: block.x, top: block.y, width: block.width, height: Math.max(block.height, block.boxHeight) };
|
||||
}
|
||||
return { left: block.x, top: block.y, width: block.boxWidth, height: block.boxHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* A *Hide* rectangle: white, drawn above all writing, hiding what lies
|
||||
* beneath (Andy's teacher, 2026-10-08). A page object like an image,
|
||||
* made, moved and removed by those who may make pages, unaffected by
|
||||
* anyone hiding a layer on their own screen.
|
||||
* @typedef {object} HideRect
|
||||
* @property {string} id
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} received
|
||||
* @returns {HideRect | null}
|
||||
*/
|
||||
export function cleanHide(received) {
|
||||
if (!received || !validId(received.id) ||
|
||||
![received.x, received.y].every((v) => Number.isFinite(v) && Math.abs(v) <= MAX_PAGE_SIZE) ||
|
||||
![received.width, received.height].every((v) => v >= 1 && v <= MAX_PAGE_SIZE)) return null;
|
||||
return {
|
||||
id: received.id, x: Math.round(received.x), y: Math.round(received.y),
|
||||
width: Math.round(received.width), height: Math.round(received.height),
|
||||
};
|
||||
}
|
||||
|
||||
/** A page holds at most this many Hide rectangles. */
|
||||
export const MAX_PAGE_HIDES = 50;
|
||||
|
||||
export class Layer {
|
||||
/**
|
||||
* @param {string} id The participant's id.
|
||||
@@ -205,6 +258,9 @@ export class Layer {
|
||||
* @property {number} y
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {string} by The participant who added it ('' if unknown):
|
||||
* they may remove it even without the right to make pages (Andy,
|
||||
* 2026-10-08).
|
||||
* @property {Uint8Array<ArrayBuffer> | null} data Null until the bytes
|
||||
* have arrived from another participant.
|
||||
*/
|
||||
@@ -225,11 +281,17 @@ function withoutData(image) {
|
||||
* @typedef {object} PageDescriptor
|
||||
* @property {string} id
|
||||
* @property {number} number Counts the pages of a session from 1.
|
||||
* @property {number} left The sheet's left edge: 0 to begin with; less
|
||||
* once "Widen" has grown the sheet, which grows it equally on both
|
||||
* sides so that nothing on it moves; anything once "Shrink" has made
|
||||
* the sheet the contents' bounding box (Andy, 2026-10-08).
|
||||
* @property {number} top The sheet's top edge: 0 until "Shrink".
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {Omit<PageImage, 'data'>[]} images From the top of the page down.
|
||||
* @property {number} revision Counts the changes made to `images`, so
|
||||
* that of two lists the later one can be told.
|
||||
* @property {Omit<PageImage, 'data'>[]} images In stacking order, the last in front.
|
||||
* @property {HideRect[]} hides The Hide rectangles, in stacking order.
|
||||
* @property {number} revision Counts the changes made to `images`, `hides`
|
||||
* and the sheet, so that of two lists the later one can be told.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -262,46 +324,83 @@ export function cleanImage(received) {
|
||||
![received.width, received.height].every((v) => v >= 1 && v <= MAX_PAGE_SIZE)) return null;
|
||||
return {
|
||||
type: received.type, size: received.size, hash: received.hash,
|
||||
x: received.x, y: received.y, width: received.width, height: received.height, data: null,
|
||||
x: received.x, y: received.y, width: received.width, height: received.height,
|
||||
by: validId(received.by) ? received.by : '', data: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Where an image of a given natural size goes on a page: centred across
|
||||
* the A4 sheet, inside the margin (Andy, 2026-10-03: his teacher's scans
|
||||
* are often partial and wider than they are tall). The first image lies
|
||||
* at the top of the sheet; each later one lies below the lowest image
|
||||
* already there, the margin apart, so that a page can be made of a series
|
||||
* of images (Andy, 2026-10-05).
|
||||
* Where an image goes on the page, and the sheet that then holds it.
|
||||
*
|
||||
* An image wider than the sheet's width inside the margin is scaled down
|
||||
* to it, or, if `shrink` is false, keeps its natural size: it then begins
|
||||
* at the left margin and the sheet is widened to hold it (Andy,
|
||||
* 2026-10-05, as a choice to try beside the scaling).
|
||||
* Below the lowest image on the page, or at the page's top if there is
|
||||
* none, the margin apart, centred across the page's width, at its
|
||||
* natural size: one way for every image (Andy, 2026-10-08, dropping the
|
||||
* shrink-to-fit choice and composition mode, since an image can be moved
|
||||
* and scaled by hand). An image wider than the page widens the sheet
|
||||
* equally on both sides; one wider than a page can be is scaled to that.
|
||||
* @param {Page} page
|
||||
* @param {number} naturalWidth In page units.
|
||||
* @param {number} naturalHeight
|
||||
* @param {number} [below] The bottom edge of the lowest image already on
|
||||
* the page; 0 if there is none.
|
||||
* @param {boolean} [shrink]
|
||||
* @returns {{x: number, y: number, width: number, height: number,
|
||||
* pageWidth: number, pageHeight: number}}
|
||||
* The image's rectangle, and how large the sheet must be to hold it.
|
||||
* @returns {{x: number, y: number, width: number, height: number, sheet: Rectangle}}
|
||||
* The image's rectangle, and the sheet that holds it and everything
|
||||
* the sheet held before.
|
||||
*/
|
||||
export function placeImage(naturalWidth, naturalHeight, below = 0, shrink = true) {
|
||||
const room = BLANK_PAGE_WIDTH - 2 * IMAGE_MARGIN;
|
||||
// At its natural size an image may still be no wider than a page can be.
|
||||
const scale = Math.min(1, (shrink ? room : MAX_PAGE_SIZE - 2 * IMAGE_MARGIN) / naturalWidth);
|
||||
export function placeImage(page, naturalWidth, naturalHeight) {
|
||||
const scale = Math.min(1, (MAX_PAGE_SIZE - 2 * IMAGE_MARGIN) / naturalWidth);
|
||||
const width = Math.round(naturalWidth * scale);
|
||||
const height = Math.round(naturalHeight * scale);
|
||||
const x = width <= room ? Math.round((BLANK_PAGE_WIDTH - width) / 2) : IMAGE_MARGIN;
|
||||
const x = page.left + Math.round((page.width - width) / 2);
|
||||
const below = page.images.length ? page.imagesBottom() : page.top;
|
||||
const y = Math.round(below) + IMAGE_MARGIN;
|
||||
return {
|
||||
x, y, width, height,
|
||||
pageWidth: Math.max(BLANK_PAGE_WIDTH, x + width + IMAGE_MARGIN),
|
||||
pageHeight: Math.max(BLANK_PAGE_HEIGHT, y + height + IMAGE_MARGIN),
|
||||
};
|
||||
return { x, y, width, height, sheet: sheetHolding(page, { left: x, top: y, width, height }) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Which of some rectangles lies under a point, the last (frontmost) first,
|
||||
* and whether the point is within `reach` of one of its corners, which
|
||||
* counts before lying inside.
|
||||
* @param {Rectangle[]} rects
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} reach
|
||||
* @returns {{index: number, corner: [number, number] | null} | null}
|
||||
* The corner as (0 or 1, 0 or 1): left or right, top or bottom.
|
||||
*/
|
||||
export function rectAt(rects, x, y, reach) {
|
||||
for (let index = rects.length - 1; index >= 0; index -= 1) {
|
||||
const r = rects[index];
|
||||
for (const cx of [0, 1]) {
|
||||
for (const cy of [0, 1]) {
|
||||
if (Math.hypot(x - (r.left + cx * r.width), y - (r.top + cy * r.height)) <= reach) {
|
||||
return { index, corner: [cx, cy] };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (x >= r.left && x <= r.left + r.width && y >= r.top && y <= r.top + r.height) {
|
||||
return { index, corner: null };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sheet that holds what it holds now and the given rectangle with
|
||||
* the image margin around it.
|
||||
* @param {Page} page
|
||||
* @param {Rectangle} rect
|
||||
* @returns {Rectangle}
|
||||
*/
|
||||
export function sheetHolding(page, rect) {
|
||||
const left = Math.min(page.baseLeft, rect.left - IMAGE_MARGIN);
|
||||
const top = Math.min(page.baseTop, rect.top - IMAGE_MARGIN);
|
||||
const right = Math.max(page.baseLeft + page.baseWidth, rect.left + rect.width + IMAGE_MARGIN);
|
||||
const bottom = Math.max(page.baseTop + page.baseHeight, rect.top + rect.height + IMAGE_MARGIN);
|
||||
return { left, top, width: right - left, height: bottom - top };
|
||||
}
|
||||
|
||||
/** An image is never scaled below this, in page units (1 cm). */
|
||||
export const LEAST_IMAGE_SIDE = 100;
|
||||
|
||||
/**
|
||||
* Check a page descriptor that came from outside.
|
||||
* @param {any} received
|
||||
@@ -320,9 +419,20 @@ export function cleanPageDescriptor(received) {
|
||||
if (!image) return null;
|
||||
images.push(withoutData(image));
|
||||
}
|
||||
const listedHides = received.hides ?? [];
|
||||
if (!Array.isArray(listedHides) || listedHides.length > MAX_PAGE_HIDES) return null;
|
||||
const hides = [];
|
||||
for (const one of listedHides) {
|
||||
const hide = cleanHide(one);
|
||||
if (!hide) return null;
|
||||
hides.push(hide);
|
||||
}
|
||||
/** @param {any} value */
|
||||
const edge = (value) => (Number.isFinite(value) && Math.abs(value) <= MAX_PAGE_SIZE ? Math.round(value) : 0);
|
||||
return {
|
||||
id: received.id, number: received.number, width: received.width, height: received.height,
|
||||
images,
|
||||
id: received.id, number: received.number, left: edge(received.left), top: edge(received.top),
|
||||
width: received.width, height: received.height,
|
||||
images, hides,
|
||||
revision: Number.isInteger(received.revision) && received.revision >= 0 ? received.revision : 0,
|
||||
};
|
||||
}
|
||||
@@ -340,12 +450,16 @@ export class Page {
|
||||
* The size of the sheet before anything is written on it: A4, or taller
|
||||
* if its images need the room.
|
||||
*/
|
||||
this.baseLeft = 0;
|
||||
this.baseTop = 0;
|
||||
this.baseWidth = width;
|
||||
this.baseHeight = height;
|
||||
this.id = id;
|
||||
this.number = number;
|
||||
/** @type {PageImage[]} From the top of the page down. */
|
||||
this.images = [];
|
||||
/** @type {HideRect[]} In stacking order. */
|
||||
this.hides = [];
|
||||
/** Counts the changes made to `images`. */
|
||||
this.imagesRevision = 0;
|
||||
/** The page's present rectangle. See updateExtent(). */
|
||||
@@ -362,28 +476,47 @@ export class Page {
|
||||
/** @returns {PageDescriptor} */
|
||||
descriptor() {
|
||||
return {
|
||||
id: this.id, number: this.number, width: this.baseWidth, height: this.baseHeight,
|
||||
id: this.id, number: this.number, left: this.baseLeft, top: this.baseTop,
|
||||
width: this.baseWidth, height: this.baseHeight,
|
||||
images: this.images.map(withoutData),
|
||||
hides: this.hides.map((hide) => ({ ...hide })),
|
||||
revision: this.imagesRevision,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hide rectangle under a point, and whether the point is near one of
|
||||
* its corners, the frontmost first; as imageAt.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} reach
|
||||
* @returns {{index: number, corner: [number, number] | null} | null}
|
||||
*/
|
||||
hideAt(x, y, reach) {
|
||||
return rectAt(this.hides.map((hide) => ({ left: hide.x, top: hide.y, width: hide.width, height: hide.height })),
|
||||
x, y, reach);
|
||||
}
|
||||
|
||||
/**
|
||||
* The image under a point, and whether the point is near one of its
|
||||
* corners: the frontmost (last) image whose rectangle holds the point
|
||||
* or has a corner within `reach` of it.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} reach Page units.
|
||||
* @returns {{index: number, corner: [number, number] | null} | null}
|
||||
* The corner as (0 or 1, 0 or 1): left or right, top or bottom.
|
||||
*/
|
||||
imageAt(x, y, reach) {
|
||||
return rectAt(this.images.map((image) => ({ left: image.x, top: image.y, width: image.width, height: image.height })),
|
||||
x, y, reach);
|
||||
}
|
||||
|
||||
/** @returns {number} The bottom edge of the lowest image; 0 if there is none. */
|
||||
imagesBottom() {
|
||||
return Math.max(0, ...this.images.map((image) => image.y + image.height));
|
||||
}
|
||||
|
||||
/** @returns {number} How tall the sheet must be to hold its images: A4 at least. */
|
||||
sheetHeight() {
|
||||
return Math.max(BLANK_PAGE_HEIGHT, this.imagesBottom() + IMAGE_MARGIN);
|
||||
}
|
||||
|
||||
/** @returns {number} How wide the sheet must be to hold its images: A4 at least. */
|
||||
sheetWidth() {
|
||||
return Math.max(BLANK_PAGE_WIDTH,
|
||||
...this.images.map((image) => image.x + image.width + IMAGE_MARGIN));
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether its images have made the sheet larger than A4. */
|
||||
isLargerThanSheet() {
|
||||
return this.baseWidth > BLANK_PAGE_WIDTH || this.baseHeight > BLANK_PAGE_HEIGHT;
|
||||
@@ -407,7 +540,38 @@ export class Page {
|
||||
* @returns {Rectangle}
|
||||
*/
|
||||
baseExtent() {
|
||||
return { left: 0, top: 0, width: this.baseWidth, height: this.baseHeight };
|
||||
return { left: this.baseLeft, top: this.baseTop, width: this.baseWidth, height: this.baseHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* The smallest sheet that holds everything on the page with the margin
|
||||
* around it: what "Shrink" makes the sheet (Andy, 2026-10-08).
|
||||
* @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]) {
|
||||
if (!box) {
|
||||
box = { left: item.x, top: item.y, width: item.width, height: item.height };
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
box.width = right - box.left;
|
||||
box.height = bottom - box.top;
|
||||
}
|
||||
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).
|
||||
const margin = Math.max(WRITING_MARGIN, IMAGE_MARGIN);
|
||||
return {
|
||||
left: Math.floor(box.left - margin), top: Math.floor(box.top - margin),
|
||||
width: Math.ceil(box.left + box.width + margin) - Math.floor(box.left - margin),
|
||||
height: Math.ceil(box.top + box.height + margin) - Math.floor(box.top - margin),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,7 +604,8 @@ export class Page {
|
||||
* @param {TextBlock} block
|
||||
*/
|
||||
includeText(block) {
|
||||
const corners = [block.x, block.y, 0, block.x + block.boxWidth, block.y + block.boxHeight, 0];
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -453,6 +618,22 @@ export class Page {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a point of a stroke of the given nominal width lies where the
|
||||
* page would not grow to hold it: inside the edges by the margin and
|
||||
* the stroke's reach. With "Extend" off, writing stops there (Andy,
|
||||
* 2026-10-08), so that the page keeps its size on everyone's screen.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} width
|
||||
* @returns {boolean}
|
||||
*/
|
||||
holdsWriting(x, y, width) {
|
||||
const inset = WRITING_MARGIN + width * 2; // The reach of includeStroke.
|
||||
return x >= this.left + inset && x <= this.left + this.width - inset &&
|
||||
y >= this.top + inset && y <= this.top + this.height - inset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page's rectangle from what is on it: the base rectangle,
|
||||
* widened to hold every stroke with the margin around it, in steps of
|
||||
@@ -467,11 +648,17 @@ export class Page {
|
||||
let bottom = base.top + base.height;
|
||||
const b = this.strokeBounds;
|
||||
if (b) {
|
||||
// Writing that comes within the margin of a sheet's edge moves that
|
||||
// edge out in steps of the margin, counted from the sheet's edge
|
||||
// (not from a fixed grid, since 2026-10-08: a sheet that "Shrink"
|
||||
// made exact stays exact until writing reaches its margin).
|
||||
const step = WRITING_MARGIN;
|
||||
left = Math.min(left, Math.floor((b.left - WRITING_MARGIN) / step) * step);
|
||||
top = Math.min(top, Math.floor((b.top - WRITING_MARGIN) / step) * step);
|
||||
right = Math.max(right, Math.ceil((b.left + b.width + WRITING_MARGIN) / step) * step);
|
||||
bottom = Math.max(bottom, Math.ceil((b.top + b.height + WRITING_MARGIN) / step) * step);
|
||||
/** @param {number} short How far the writing's margin reaches past an edge. */
|
||||
const steps = (short) => (short > 0 ? Math.ceil(short / step) * step : 0);
|
||||
left -= steps(left - (b.left - WRITING_MARGIN));
|
||||
top -= steps(top - (b.top - WRITING_MARGIN));
|
||||
right += steps(b.left + b.width + WRITING_MARGIN - right);
|
||||
bottom += steps(b.top + b.height + WRITING_MARGIN - bottom);
|
||||
}
|
||||
const changed = left !== this.left || top !== this.top ||
|
||||
right - left !== this.width || bottom - top !== this.height;
|
||||
@@ -489,7 +676,10 @@ export class Page {
|
||||
*/
|
||||
static from(descriptor) {
|
||||
const page = new Page(descriptor.width, descriptor.height, descriptor.id, descriptor.number);
|
||||
page.baseLeft = descriptor.left;
|
||||
page.baseTop = descriptor.top;
|
||||
page.images = descriptor.images.map((image) => ({ ...image, data: null }));
|
||||
page.hides = descriptor.hides.map((hide) => ({ ...hide }));
|
||||
page.imagesRevision = descriptor.revision;
|
||||
page.updateExtent();
|
||||
return page;
|
||||
@@ -567,11 +757,12 @@ export function textsTouched(layer, x0, y0, x1, y1, radius) {
|
||||
const touched = [];
|
||||
const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0) / radius));
|
||||
for (const block of layer.texts.values()) {
|
||||
const b = textBounds(block);
|
||||
for (let s = 0; s <= steps; s++) {
|
||||
const x = x0 + (x1 - x0) * s / steps;
|
||||
const y = y0 + (y1 - y0) * s / steps;
|
||||
if (x >= block.x - radius && x <= block.x + block.boxWidth + radius &&
|
||||
y >= block.y - radius && y <= block.y + block.boxHeight + radius) {
|
||||
if (x >= b.left - radius && x <= b.left + b.width + radius &&
|
||||
y >= b.top - radius && y <= b.top + b.height + radius) {
|
||||
touched.push(block.id);
|
||||
break;
|
||||
}
|
||||
@@ -588,9 +779,8 @@ export function textsTouched(layer, x0, y0, x1, y1, radius) {
|
||||
*/
|
||||
export function textAt(layer, x, y) {
|
||||
for (const block of [...layer.texts.values()].reverse()) {
|
||||
if (x >= block.x && x <= block.x + block.boxWidth && y >= block.y && y <= block.y + block.boxHeight) {
|
||||
return block;
|
||||
}
|
||||
const b = textBounds(block); // A text rectangle counts whole, empty part included.
|
||||
if (x >= b.left && x <= b.left + b.width && y >= b.top && y <= b.top + b.height) return block;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user