Export template source from e33714a5ca38429d1bdb77a961a14461697944f5:examples/local-first/packages/language-reader-presentation
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import type { Layout, Range, Rect } from "./types.js";
|
||||
const sourceLines = new WeakMap<Layout, Layout["lines"]>();
|
||||
type SourceRect = Rect & Range;
|
||||
function rangeRows(layout: Layout, range: Range): SourceRect[] {
|
||||
// Layout source offsets are monotonic, including empty/blank lines. Search
|
||||
// the first candidate row rather than scanning a long document on every hover.
|
||||
let populated = sourceLines.get(layout);
|
||||
if (!populated) {
|
||||
populated = layout.lines.filter((line) => line.glyphs.length);
|
||||
sourceLines.set(layout, populated);
|
||||
}
|
||||
let low = 0,
|
||||
high = populated.length;
|
||||
while (low < high) {
|
||||
const middle = (low + high) >>> 1;
|
||||
if (populated[middle].glyphs.at(-1)!.end <= range.start) low = middle + 1;
|
||||
else high = middle;
|
||||
}
|
||||
const rects: SourceRect[] = [];
|
||||
for (let index = low; index < populated.length; index++) {
|
||||
const line = populated[index];
|
||||
if (line.glyphs[0].start >= range.end) break;
|
||||
const members = line.glyphs.filter((glyph) => glyph.start < range.end && glyph.end > range.start);
|
||||
if (!members.length || members.every((glyph) => /^\s*$/.test(glyph.text))) continue;
|
||||
rects.push({
|
||||
start: members[0].start,
|
||||
end: members.at(-1)!.end,
|
||||
x: members[0].x,
|
||||
y: Math.min(...members.map((glyph) => glyph.y)),
|
||||
width: members.at(-1)!.x + members.at(-1)!.width - members[0].x,
|
||||
height: Math.max(...members.map((glyph) => glyph.height)),
|
||||
});
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
export function rangeRects(layout: Layout, range: Range): Rect[] {
|
||||
return rangeRows(layout, range).map(({ start, end, ...rect }) => rect);
|
||||
}
|
||||
type Point = { x: number; y: number };
|
||||
function rounded(points: Point[], radius: number) {
|
||||
const corners = points.map((point, index) => {
|
||||
const before = points[(index + points.length - 1) % points.length],
|
||||
after = points[(index + 1) % points.length];
|
||||
const incoming = Math.hypot(point.x - before.x, point.y - before.y),
|
||||
outgoing = Math.hypot(after.x - point.x, after.y - point.y);
|
||||
const distance = Math.min(radius, incoming / 2, outgoing / 2);
|
||||
return {
|
||||
point,
|
||||
before: {
|
||||
x: point.x + ((before.x - point.x) * distance) / incoming,
|
||||
y: point.y + ((before.y - point.y) * distance) / incoming,
|
||||
},
|
||||
after: {
|
||||
x: point.x + ((after.x - point.x) * distance) / outgoing,
|
||||
y: point.y + ((after.y - point.y) * distance) / outgoing,
|
||||
},
|
||||
};
|
||||
});
|
||||
return (
|
||||
corners
|
||||
.map(
|
||||
({ point, before, after }, index) =>
|
||||
`${index ? "L" : "M"}${before.x},${before.y}Q${point.x},${point.y} ${after.x},${after.y}`,
|
||||
)
|
||||
.join("") + "Z"
|
||||
);
|
||||
}
|
||||
/** A broad band: only the first and last rows follow source edges. Paragraph
|
||||
* gaps and intermediate short rows stay inside one quiet, full-width outline. */
|
||||
export function rangeOutline(layout: Layout, range: Range, padding = 3): string {
|
||||
const rows = rangeRows(layout, range);
|
||||
if (!rows.length) return "";
|
||||
const first = rows[0],
|
||||
last = rows.at(-1)!;
|
||||
const left = 1,
|
||||
right = layout.width - 1;
|
||||
const firstLeft = Math.max(left, first.x - padding);
|
||||
const lastRight = Math.min(right, last.x + last.width + padding);
|
||||
const top = Math.max(1, first.y - padding),
|
||||
bottom = last.y + last.height + padding;
|
||||
let points: Point[];
|
||||
if (rows.length === 1) {
|
||||
points = [
|
||||
{ x: firstLeft, y: top },
|
||||
{ x: lastRight, y: top },
|
||||
{ x: lastRight, y: bottom },
|
||||
{ x: firstLeft, y: bottom },
|
||||
];
|
||||
} else {
|
||||
const firstJoin = first.y + first.height + padding;
|
||||
const lastJoin = last.y - padding;
|
||||
// Very tightly stacked rows get a plain box rather than a crossing contour.
|
||||
points =
|
||||
firstJoin >= lastJoin
|
||||
? [
|
||||
{ x: left, y: top },
|
||||
{ x: right, y: top },
|
||||
{ x: right, y: bottom },
|
||||
{ x: left, y: bottom },
|
||||
]
|
||||
: [
|
||||
{ x: firstLeft, y: top },
|
||||
{ x: right, y: top },
|
||||
{ x: right, y: lastJoin },
|
||||
{ x: lastRight, y: lastJoin },
|
||||
{ x: lastRight, y: bottom },
|
||||
{ x: left, y: bottom },
|
||||
{ x: left, y: firstJoin },
|
||||
{ x: firstLeft, y: firstJoin },
|
||||
];
|
||||
}
|
||||
points = points.filter((p, i) => {
|
||||
const before = points[(i + points.length - 1) % points.length],
|
||||
after = points[(i + 1) % points.length];
|
||||
return !((before.x === p.x && p.x === after.x) || (before.y === p.y && p.y === after.y));
|
||||
});
|
||||
return rounded(points, 4);
|
||||
}
|
||||
Reference in New Issue
Block a user