global: opreturn part 3

This commit is contained in:
nym21
2026-07-18 21:34:21 +02:00
parent 10c16335ed
commit 5161ae2012
16 changed files with 912 additions and 412 deletions
@@ -1,73 +1,95 @@
import { packCells } from "./pack.js";
const CALIBRATION_STEPS = 24;
/**
* @param {number} weight
* @param {number} capacity
* @param {number} columns
* @param {Float64Array} spans
* @param {number} scale
*/
function weightToSpan(weight, capacity, columns) {
const cellWeight = capacity / (columns * columns);
function scaledArea(spans, scale) {
let area = 0;
return Math.max(1, Math.round(Math.sqrt(weight / cellWeight)));
}
for (const span of spans) {
const resolved = Math.max(1, Math.round(span * scale));
/**
* @template {CapacityCell} Cell
* @param {readonly Cell[]} cells
* @param {number} capacity
* @param {number} columns
* @returns {ResolvedCapacityCell<Cell>[]}
*/
function resolveCapacityCells(cells, capacity, columns) {
const resolved = /** @type {ResolvedCapacityCell<Cell>[]} */ (cells);
for (const cell of resolved) {
cell.span = weightToSpan(cell.weight, capacity, columns);
area += resolved * resolved;
}
return resolved;
return area;
}
/**
* @template {CapacityCell} Cell
* @param {readonly Cell[]} cells
* @param {number} blockWeight
* @param {number} capacity
* @param {number} columns
*/
function fitCapacityCells(cells, capacity, columns) {
const resolvedCells = resolveCapacityCells(cells, capacity, columns);
let layouts = packCells(resolvedCells, columns, columns);
export function resolveCapacityCells(cells, blockWeight, capacity, columns) {
const gridArea = columns * columns;
const targetArea = Math.max(
cells.length,
Math.floor((gridArea * blockWeight) / capacity),
);
const idealSpans = new Float64Array(cells.length);
const resolved = /** @type {ResolvedCapacityCell<Cell>[]} */ (cells);
while (layouts === null) {
let largest = 0;
for (let index = 0; index < cells.length; index += 1) {
idealSpans[index] = Math.sqrt((cells[index].weight * gridArea) / capacity);
}
for (const { span } of resolvedCells) largest = Math.max(largest, span);
let lowerScale = 0;
let upperScale = 1;
if (largest <= 1) break;
while (scaledArea(idealSpans, upperScale) <= targetArea) {
upperScale *= 2;
}
for (const cell of resolvedCells) {
if (cell.span === largest) cell.span -= 1;
for (let step = 0; step < CALIBRATION_STEPS; step += 1) {
const scale = (lowerScale + upperScale) / 2;
if (scaledArea(idealSpans, scale) <= targetArea) {
lowerScale = scale;
} else {
upperScale = scale;
}
}
layouts = packCells(resolvedCells, columns, columns);
let area = 0;
let largestSpan = 1;
for (let index = 0; index < resolved.length; index += 1) {
const idealSpan = idealSpans[index];
const span = Math.max(1, Math.round(idealSpan * lowerScale));
resolved[index].span = span;
idealSpans[index] = (span + 0.5) / idealSpan;
largestSpan = Math.max(largestSpan, span);
area += span * span;
}
const candidates = Uint32Array.from(resolved, (_, index) => index);
candidates.sort((a, b) => {
return idealSpans[a] - idealSpans[b] || a - b;
});
for (const index of candidates) {
const cell = resolved[index];
const growth = 2 * cell.span + 1;
if (area + growth > targetArea) continue;
cell.span += 1;
largestSpan = Math.max(largestSpan, cell.span);
area += growth;
if (targetArea - area < 3) break;
}
return {
layouts: /** @type {NonNullable<typeof layouts>} */ (layouts),
resolvedCells,
resolvedCells: resolved,
rows: Math.max(largestSpan, Math.ceil(targetArea / columns)),
};
}
/**
* @template {CapacityCell} Cell
* @param {readonly Cell[]} cells
* @param {number} capacity
* @param {number} columns
*/
export function createSquareLayout(cells, capacity, columns) {
return { columns, ...fitCapacityCells(cells, capacity, columns) };
}
/**
* @typedef {Object} CapacityCell
* @property {number} weight
@@ -48,22 +48,28 @@ function drawHover(context, rect) {
* @param {DrawPreviewArgs} args
*/
export function drawPreview(args) {
const { context, disabledMask, filterState, inspected, previewMask, rects } = args;
const {
context,
hasHidden,
hiddenCounts,
inspected,
previewMembership,
rects,
start,
} = args;
let inspectedRect = /** @type {PreviewRect | null} */ (null);
if (filterState === null || (disabledMask === 0 && previewMask === null)) {
if (previewMembership === null && !hasHidden) {
for (const rect of rects) {
drawRect(context, 1, rect.color, rect);
if (rect.transaction === inspected) inspectedRect = rect;
}
} else {
const activeMask = previewMask ?? disabledMask;
for (const rect of rects) {
const mask = filterState.masks[rect.transaction.txIndex - filterState.start];
const alpha = previewMask === null
? (mask & activeMask ? MUTED_ALPHA : 1)
: (mask & activeMask ? 1 : MUTED_ALPHA);
const index = rect.transaction.txIndex - start;
const alpha = previewMembership === null
? (hiddenCounts[index] ? MUTED_ALPHA : 1)
: (previewMembership[index] ? 1 : MUTED_ALPHA);
drawRect(context, alpha, rect.color, rect);
if (rect.transaction === inspected) inspectedRect = rect;
@@ -77,13 +83,13 @@ export function drawPreview(args) {
/**
* @typedef {Object} DrawPreviewArgs
* @property {CanvasRenderingContext2D} context
* @property {number} disabledMask
* @property {BlockPreviewFilterState | null} filterState
* @property {boolean} hasHidden
* @property {Uint8Array} hiddenCounts
* @property {BlockPreviewTransaction | null} inspected
* @property {number | null} previewMask
* @property {Uint8Array | null} previewMembership
* @property {PreviewRect[]} rects
* @property {number} start
*/
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
/** @typedef {import("./geometry.js").PreviewRect} PreviewRect */
@@ -1,4 +1,5 @@
import { FEE_RATE_PERCENTILES } from "../../fee-rates.js";
import { packCells } from "./pack.js";
/**
* @param {number[]} feeRates
@@ -25,3 +26,20 @@ export function orderTransactions(weights, feeRates) {
return order.sort((a, b) => feeRates[b] - feeRates[a] || weights[b] - weights[a]);
}
/**
* @template {{ span: number }} Cell
* @param {readonly Cell[]} cells
* @param {number} columns
* @param {number} rows
*/
export function packTransactions(cells, columns, rows) {
let layouts = packCells(cells, columns, rows);
while (layouts === null) {
rows += 1;
layouts = packCells(cells, columns, rows);
}
return { layouts, resolvedCells: cells };
}
@@ -1,11 +1,11 @@
import { MAX_BLOCK_WEIGHT } from "../../format.js";
import { createPreviewFeeRange, orderTransactions } from "./fees.js";
import { createSquareLayout } from "./capacity.js";
import { createSquareLayout } from "./layout.js";
import { getCanvasFeeRateColor } from "./color.js";
import { createPreviewGeometry, hitTest } from "./geometry.js";
import { drawPreview } from "./draw.js";
const COLUMNS = 84;
const COLUMNS = 86;
const VISIBLE_CELLS = COLUMNS * COLUMNS;
/**
@@ -56,15 +56,20 @@ export function createBlockPreviewHeatmap(data, options = {}) {
const order = orderTransactions(data.weights, data.feeRates);
const ranges = createPreviewFeeRange(data.feeRates, order);
const cells = createCapacityCells(data, order, ranges);
const square = createSquareLayout(cells, MAX_BLOCK_WEIGHT, COLUMNS);
let disabledMask = 0;
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
const square = createSquareLayout(
cells,
data.blockWeight,
MAX_BLOCK_WEIGHT,
COLUMNS,
);
const hiddenCounts = new Uint8Array(data.range.end - data.range.start);
let hiddenFilterCount = 0;
let frame = 0;
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
let inspectFrame = 0;
let inspectPoint = /** @type {BlockPreviewPointer | null} */ (null);
let bounds = /** @type {DOMRectReadOnly | null} */ (null);
let previewMask = /** @type {number | null} */ (null);
let previewMembership = /** @type {Uint8Array | null} */ (null);
let geometry = /** @type {PreviewGeometry | null} */ (null);
let rectWidth = 0;
let capturedPointer = /** @type {number | null} */ (null);
@@ -99,11 +104,12 @@ export function createBlockPreviewHeatmap(data, options = {}) {
context.clearRect(0, 0, width, width);
drawPreview({
context,
disabledMask,
filterState,
hasHidden: hiddenFilterCount > 0,
hiddenCounts,
inspected,
previewMask,
previewMembership,
rects: geometry?.rects ?? [],
start: data.range.start,
});
}
@@ -242,23 +248,25 @@ export function createBlockPreviewHeatmap(data, options = {}) {
window.removeEventListener("blur", clearInspect);
observer.disconnect();
},
/** @param {number | null} mask */
setPreviewMask(mask) {
if (previewMask === mask) return;
/** @param {Uint8Array | null} membership */
setPreviewMembership(membership) {
if (previewMembership === membership) return;
previewMask = mask;
previewMembership = membership;
scheduleDraw();
},
/** @param {number} mask */
setDisabledMask(mask) {
if (disabledMask === mask) return;
/**
* @param {Uint8Array} membership
* @param {boolean} hidden
*/
setFilterHidden(membership, hidden) {
const change = hidden ? 1 : -1;
hiddenFilterCount += change;
for (let index = 0; index < hiddenCounts.length; index += 1) {
if (membership[index]) hiddenCounts[index] += change;
}
disabledMask = mask;
scheduleDraw();
},
/** @param {BlockPreviewFilterState} state */
setFilterState(state) {
filterState = state;
scheduleDraw();
},
});
@@ -266,7 +274,6 @@ export function createBlockPreviewHeatmap(data, options = {}) {
/** @typedef {import("../data.js").BlockPreviewData} BlockPreviewData */
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
/**
* @typedef {Object} BlockPreviewPointer
@@ -0,0 +1,25 @@
import { resolveCapacityCells } from "./capacity.js";
import { packTransactions } from "./fees.js";
/**
* @template {{ weight: number }} Cell
* @param {readonly Cell[]} cells
* @param {number} blockWeight
* @param {number} capacity
* @param {number} columns
*/
export function createSquareLayout(cells, blockWeight, capacity, columns) {
const capacityLayout = resolveCapacityCells(
cells,
blockWeight,
capacity,
columns,
);
const packed = packTransactions(
capacityLayout.resolvedCells,
columns,
capacityLayout.rows,
);
return { columns, ...packed };
}
@@ -6,15 +6,23 @@
*/
export function packCells(cells, columns, rows) {
const occupied = new Uint8Array(columns * rows);
const cursors = new Uint32Array(columns + 1);
const layouts = [];
for (const cell of cells) {
const span = Math.min(cell.span, columns);
const position = findPosition(occupied, columns, rows, span);
const position = findPosition(
occupied,
columns,
rows,
span,
cursors[span],
);
if (position === null) return null;
fillCells(occupied, columns, position.x, position.y, span);
cursors[span] = position.index + span;
layouts.push({ x: position.x, y: position.y, span });
}
@@ -26,14 +34,22 @@ export function packCells(cells, columns, rows) {
* @param {number} columns
* @param {number} rows
* @param {number} span
* @returns {{ x: number, y: number } | null}
* @param {number} start
* @returns {{ index: number, x: number, y: number } | null}
*/
function findPosition(occupied, columns, rows, span) {
function findPosition(occupied, columns, rows, span, start) {
const lastRow = rows - span;
const lastColumn = columns - span;
const startRow = Math.floor(start / columns);
const startColumn = start % columns;
for (let y = 0; y <= lastRow; y += 1) {
for (let x = 0; x <= columns - span; x += 1) {
if (canPlace(occupied, columns, x, y, span)) return { x, y };
for (let y = startRow; y <= lastRow; y += 1) {
const firstColumn = y === startRow ? startColumn : 0;
for (let x = firstColumn; x <= lastColumn; x += 1) {
if (canPlace(occupied, columns, x, y, span)) {
return { index: y * columns + x, x, y };
}
}
}