mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-22 08:28:10 -07:00
global: opreturn part 3
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user