@jbrowse/render-core 5.0.0-beta.1 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/RenderLifecycleMixin.d.ts +5 -5
- package/esm/RenderLifecycleMixin.js +5 -5
- package/esm/contractReports.d.ts +54 -0
- package/esm/contractReports.js +112 -0
- package/esm/displayPhase.d.ts +10 -1
- package/esm/displayPhase.js +2 -1
- package/esm/installUpload.js +4 -3
- package/esm/regionDataMap.js +23 -21
- package/esm/shaders/hpmath.js.generated.d.ts +1 -0
- package/esm/shaders/hpmath.js.generated.js +4 -1
- package/esm/shaders/rowRect.generated.d.ts +0 -1
- package/esm/shaders/rowRect.generated.js +0 -1
- package/package.json +2 -1
- package/src/shaders/hpmath.js.generated.ts +5 -1
- package/src/shaders/hpmath.slang +22 -9
- package/src/shaders/rowRect.generated.ts +0 -2
- package/src/shaders/rowRect.slang +26 -36
|
@@ -95,11 +95,11 @@ export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree
|
|
|
95
95
|
* never called and `canvasDrawn` can never flip).
|
|
96
96
|
*
|
|
97
97
|
* Lives here, beside `canvasDrawn`, because every consumer of "has this
|
|
98
|
-
* display painted" needs the pair
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
98
|
+
* display painted" needs the pair. Both LGV fetch foundations fill it
|
|
99
|
+
* with `!fetchInert`, so a display there states its placeholder once,
|
|
100
|
+
* through that hook; the raw default reaches only a display composing
|
|
101
|
+
* this mixin outside them. See `painted` below for the reader that was
|
|
102
|
+
* missed while the two were separate overrides.
|
|
103
103
|
*/
|
|
104
104
|
readonly rendersCanvas: boolean;
|
|
105
105
|
/**
|
|
@@ -104,11 +104,11 @@ export function RenderLifecycleMixin() {
|
|
|
104
104
|
* never called and `canvasDrawn` can never flip).
|
|
105
105
|
*
|
|
106
106
|
* Lives here, beside `canvasDrawn`, because every consumer of "has this
|
|
107
|
-
* display painted" needs the pair
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
107
|
+
* display painted" needs the pair. Both LGV fetch foundations fill it
|
|
108
|
+
* with `!fetchInert`, so a display there states its placeholder once,
|
|
109
|
+
* through that hook; the raw default reaches only a display composing
|
|
110
|
+
* this mixin outside them. See `painted` below for the reader that was
|
|
111
|
+
* missed while the two were separate overrides.
|
|
112
112
|
*/
|
|
113
113
|
get rendersCanvas() {
|
|
114
114
|
return true;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A contract family — the `<family>` in the `[jbrowse <family> contract]`
|
|
3
|
+
* prefix every contract check in the tree reports under.
|
|
4
|
+
* `config/jest/console.js` matches `\w+`, so a new one reaches the test gate
|
|
5
|
+
* without touching it.
|
|
6
|
+
*/
|
|
7
|
+
export type ContractFamily = 'display' | 'session' | 'figure';
|
|
8
|
+
export interface ContractReport {
|
|
9
|
+
family: ContractFamily;
|
|
10
|
+
message: string;
|
|
11
|
+
/** what armed the channel, so a host can say why it is showing this */
|
|
12
|
+
armedBy: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether the checks should run at all.
|
|
16
|
+
*
|
|
17
|
+
* Read it where a check costs something to run — the `MutationObserver` a live
|
|
18
|
+
* figure installs, the per-frame payload walk in `installUpload`. A check whose
|
|
19
|
+
* cost is a `WeakSet` lookup should run unconditionally and report through
|
|
20
|
+
* {@link reportContractViolation} instead, so that arming the channel late
|
|
21
|
+
* still surfaces what it found.
|
|
22
|
+
*/
|
|
23
|
+
export declare function contractReportsOn(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Turn the channel on in a production build, saying what turned it on.
|
|
26
|
+
*
|
|
27
|
+
* The reason is not decoration: a notification that appears in an app the
|
|
28
|
+
* reader did not build has to answer "why am I seeing this" in its own text, or
|
|
29
|
+
* it reads as the app being broken rather than as a plugin being under
|
|
30
|
+
* development. First caller wins, so the earliest evidence is the one quoted.
|
|
31
|
+
*/
|
|
32
|
+
export declare function enableContractReports(reason: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Where a host puts contract violations that reach a user — a session
|
|
35
|
+
* notification in the apps. Installing one drains whatever was found before the
|
|
36
|
+
* host existed, and the disposer only clears the sink it installed, so a
|
|
37
|
+
* session torn down after its replacement was built does not take the live
|
|
38
|
+
* one's sink with it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function setContractReportSink(fn: (report: ContractReport) => void): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Report a violation of an ordering contract no type can state.
|
|
43
|
+
*
|
|
44
|
+
* In tree this is the `console.error` it has always been, and the jest gate is
|
|
45
|
+
* what listens. Out of tree it is the only thing that reaches a plugin author
|
|
46
|
+
* at all: their display runs in a production build of somebody's app, which
|
|
47
|
+
* strips nothing here, and a violation surfaces the moment anything arms the
|
|
48
|
+
* channel — a `localStorage` flag, a plugin loaded from localhost, or a site
|
|
49
|
+
* whose config asks for it.
|
|
50
|
+
*
|
|
51
|
+
* `agent-docs/reference/ARCHITECTURAL_LIMITS.md` §"Ordering is the contract" is
|
|
52
|
+
* the register of what reports here.
|
|
53
|
+
*/
|
|
54
|
+
export declare function reportContractViolation(family: ContractFamily, message: string): void;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const DEVELOPER_MODE_KEY = 'jbrowseDeveloperMode';
|
|
2
|
+
// The flag a plugin author can set on any deployed JBrowse without owning it or
|
|
3
|
+
// asking anyone, which is the population this whole channel is for.
|
|
4
|
+
function storageFlag() {
|
|
5
|
+
try {
|
|
6
|
+
return typeof localStorage === 'undefined' ||
|
|
7
|
+
localStorage.getItem(DEVELOPER_MODE_KEY) === null
|
|
8
|
+
? undefined
|
|
9
|
+
: `localStorage.${DEVELOPER_MODE_KEY} is set`;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// storage can be denied outright, and a diagnostic is not the thing to take
|
|
13
|
+
// the app down with
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Module scope for the same reason `numberGrouping` is: a check is called from
|
|
18
|
+
// a mixin factory, an MST action and a React effect that cannot share a
|
|
19
|
+
// closure, and each realm resolves this once at startup rather than through an
|
|
20
|
+
// observable a worker could not see.
|
|
21
|
+
let armedBy = process.env.NODE_ENV === 'production'
|
|
22
|
+
? storageFlag()
|
|
23
|
+
: 'this is a development build';
|
|
24
|
+
let sink;
|
|
25
|
+
const unlogged = [];
|
|
26
|
+
const undelivered = [];
|
|
27
|
+
const PENDING_LIMIT = 20;
|
|
28
|
+
function remember(queue, found) {
|
|
29
|
+
if (queue.length < PENDING_LIMIT) {
|
|
30
|
+
queue.push(found);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the checks should run at all.
|
|
35
|
+
*
|
|
36
|
+
* Read it where a check costs something to run — the `MutationObserver` a live
|
|
37
|
+
* figure installs, the per-frame payload walk in `installUpload`. A check whose
|
|
38
|
+
* cost is a `WeakSet` lookup should run unconditionally and report through
|
|
39
|
+
* {@link reportContractViolation} instead, so that arming the channel late
|
|
40
|
+
* still surfaces what it found.
|
|
41
|
+
*/
|
|
42
|
+
export function contractReportsOn() {
|
|
43
|
+
return armedBy !== undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Turn the channel on in a production build, saying what turned it on.
|
|
47
|
+
*
|
|
48
|
+
* The reason is not decoration: a notification that appears in an app the
|
|
49
|
+
* reader did not build has to answer "why am I seeing this" in its own text, or
|
|
50
|
+
* it reads as the app being broken rather than as a plugin being under
|
|
51
|
+
* development. First caller wins, so the earliest evidence is the one quoted.
|
|
52
|
+
*/
|
|
53
|
+
export function enableContractReports(reason) {
|
|
54
|
+
armedBy ??= reason;
|
|
55
|
+
for (const found of unlogged.splice(0)) {
|
|
56
|
+
emit(found, armedBy);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Where a host puts contract violations that reach a user — a session
|
|
61
|
+
* notification in the apps. Installing one drains whatever was found before the
|
|
62
|
+
* host existed, and the disposer only clears the sink it installed, so a
|
|
63
|
+
* session torn down after its replacement was built does not take the live
|
|
64
|
+
* one's sink with it.
|
|
65
|
+
*/
|
|
66
|
+
export function setContractReportSink(fn) {
|
|
67
|
+
sink = fn;
|
|
68
|
+
if (armedBy !== undefined) {
|
|
69
|
+
for (const found of undelivered.splice(0)) {
|
|
70
|
+
fn({ ...found, armedBy });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return () => {
|
|
74
|
+
if (sink === fn) {
|
|
75
|
+
sink = undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function emit(found, armed) {
|
|
80
|
+
// `console.error`, never a throw: an error escaping `afterAttach` is read by
|
|
81
|
+
// the session loader as an invalid track and the display is silently dropped,
|
|
82
|
+
// which would hide the very violation being reported. The prefix is what
|
|
83
|
+
// `config/jest/console.js` buffers and `contractGate.js` fails a test on.
|
|
84
|
+
console.error(`[jbrowse ${found.family} contract] ${found.message}`);
|
|
85
|
+
if (sink === undefined) {
|
|
86
|
+
remember(undelivered, found);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
sink({ ...found, armedBy: armed });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Report a violation of an ordering contract no type can state.
|
|
94
|
+
*
|
|
95
|
+
* In tree this is the `console.error` it has always been, and the jest gate is
|
|
96
|
+
* what listens. Out of tree it is the only thing that reaches a plugin author
|
|
97
|
+
* at all: their display runs in a production build of somebody's app, which
|
|
98
|
+
* strips nothing here, and a violation surfaces the moment anything arms the
|
|
99
|
+
* channel — a `localStorage` flag, a plugin loaded from localhost, or a site
|
|
100
|
+
* whose config asks for it.
|
|
101
|
+
*
|
|
102
|
+
* `agent-docs/reference/ARCHITECTURAL_LIMITS.md` §"Ordering is the contract" is
|
|
103
|
+
* the register of what reports here.
|
|
104
|
+
*/
|
|
105
|
+
export function reportContractViolation(family, message) {
|
|
106
|
+
if (armedBy === undefined) {
|
|
107
|
+
remember(unlogged, { family, message });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
emit({ family, message }, armedBy);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/esm/displayPhase.d.ts
CHANGED
|
@@ -83,6 +83,15 @@ export interface DisplayLoadingInputs {
|
|
|
83
83
|
* click.
|
|
84
84
|
*/
|
|
85
85
|
isLoadingOrCanceled: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* A load this display depends on beyond its primary fetch has not landed for
|
|
88
|
+
* the first time, so the frame the primary fetch calls current is still
|
|
89
|
+
* missing something (multi-way synteny's lane genes and links). A loading
|
|
90
|
+
* term rather than a `dataSuperseded` fold: that flag holds the export
|
|
91
|
+
* through every later refetch too, and a scrim over lanes that are already
|
|
92
|
+
* drawn is the thing a display saying this wants to avoid.
|
|
93
|
+
*/
|
|
94
|
+
awaitingDependentData: boolean;
|
|
86
95
|
/**
|
|
87
96
|
* Whether this display paints a canvas in its current configuration. Gates
|
|
88
97
|
* the pre-first-paint term alone, because a display that is showing a
|
|
@@ -142,7 +151,7 @@ export interface DisplayLoadingInputs {
|
|
|
142
151
|
* IntersectionObserver's first callback, which is exactly when a display must
|
|
143
152
|
* not flash `ready`. Only the paint it will never do is excused.
|
|
144
153
|
*/
|
|
145
|
-
export declare function computeLoadingTerm({ fetchInert, viewportEmpty, isLoadingOrCanceled, rendersCanvas, canvasDrawn, }: DisplayLoadingInputs, viewportCurrent: () => boolean, hostMounted?: () => boolean): boolean;
|
|
154
|
+
export declare function computeLoadingTerm({ fetchInert, viewportEmpty, isLoadingOrCanceled, awaitingDependentData, rendersCanvas, canvasDrawn, }: DisplayLoadingInputs, viewportCurrent: () => boolean, hostMounted?: () => boolean): boolean;
|
|
146
155
|
/**
|
|
147
156
|
* The same ranking minus `renderError`, for a display with no rendering backend
|
|
148
157
|
* to fail (arc's main-thread SVG). `computeDisplayPhase` delegates here rather
|
package/esm/displayPhase.js
CHANGED
|
@@ -70,10 +70,11 @@ export function computeDisplayPhase({ renderError, regionTooLarge, error }, load
|
|
|
70
70
|
* IntersectionObserver's first callback, which is exactly when a display must
|
|
71
71
|
* not flash `ready`. Only the paint it will never do is excused.
|
|
72
72
|
*/
|
|
73
|
-
export function computeLoadingTerm({ fetchInert, viewportEmpty, isLoadingOrCanceled, rendersCanvas, canvasDrawn, }, viewportCurrent, hostMounted = () => true) {
|
|
73
|
+
export function computeLoadingTerm({ fetchInert, viewportEmpty, isLoadingOrCanceled, awaitingDependentData, rendersCanvas, canvasDrawn, }, viewportCurrent, hostMounted = () => true) {
|
|
74
74
|
return (!fetchInert &&
|
|
75
75
|
!viewportEmpty &&
|
|
76
76
|
(isLoadingOrCanceled ||
|
|
77
|
+
awaitingDependentData ||
|
|
77
78
|
(rendersCanvas && hostMounted() && !canvasDrawn) ||
|
|
78
79
|
!viewportCurrent()));
|
|
79
80
|
}
|
package/esm/installUpload.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getType } from '@jbrowse/mobx-state-tree';
|
|
2
2
|
import { computed } from 'mobx';
|
|
3
|
+
import { contractReportsOn, reportContractViolation, } from './contractReports.js';
|
|
3
4
|
import { createMapUploadSync } from './mapUploadSync.js';
|
|
4
5
|
import { isCheckedAtTheStore } from './regionDataMap.js';
|
|
5
6
|
/**
|
|
@@ -29,7 +30,7 @@ function checkPayloads(self, cells) {
|
|
|
29
30
|
}
|
|
30
31
|
if (!named.has(key)) {
|
|
31
32
|
named.add(key);
|
|
32
|
-
|
|
33
|
+
reportContractViolation('display', `${getType(self).name}: cell ${String(key)} ` +
|
|
33
34
|
`handed to installUpload is \`${String(value)}\`. A fetch answers ` +
|
|
34
35
|
'a payload or nothing; leave the key out until it has one.');
|
|
35
36
|
}
|
|
@@ -51,7 +52,7 @@ export function installUpload(self, backend, { cells, inputs, encode, render })
|
|
|
51
52
|
return {
|
|
52
53
|
upload: b => {
|
|
53
54
|
const current = own();
|
|
54
|
-
if (
|
|
55
|
+
if (contractReportsOn()) {
|
|
55
56
|
checkPayloads(self, current);
|
|
56
57
|
}
|
|
57
58
|
return sync(b, current);
|
|
@@ -66,7 +67,7 @@ export function installUpload(self, backend, { cells, inputs, encode, render })
|
|
|
66
67
|
return {
|
|
67
68
|
upload: b => {
|
|
68
69
|
const current = cells();
|
|
69
|
-
if (
|
|
70
|
+
if (contractReportsOn()) {
|
|
70
71
|
checkPayloads(self, current);
|
|
71
72
|
}
|
|
72
73
|
const p = props ? props.get() : undefined;
|
package/esm/regionDataMap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { observable } from 'mobx';
|
|
2
|
+
import { reportContractViolation } from './contractReports.js';
|
|
2
3
|
// Maps built here check themselves at the `set`, so the upload seam skips them
|
|
3
4
|
// and a bad entry is reported once, with the field named.
|
|
4
5
|
//
|
|
@@ -73,26 +74,27 @@ export function regionDataMap(
|
|
|
73
74
|
// reached by forgetting rather than by deciding.
|
|
74
75
|
name) {
|
|
75
76
|
const map = observable.map(undefined, { deep: false, name });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
77
|
+
checkedAtTheStore.add(map);
|
|
78
|
+
const set = map.set.bind(map);
|
|
79
|
+
// one `typeof` per stored region, which is why it is not gated on the channel
|
|
80
|
+
// being armed: a violation found in a production build is buffered until
|
|
81
|
+
// something arms it, where a gate here would have thrown the evidence away
|
|
82
|
+
map.set = (key, value) => {
|
|
83
|
+
// as `unknown`, because the claim that this is a `T` is the thing in
|
|
84
|
+
// doubt — checking it against its own declared type narrows to `never`
|
|
85
|
+
// and checks nothing
|
|
86
|
+
const stored = value;
|
|
87
|
+
if (typeof stored !== 'object' || stored === null) {
|
|
88
|
+
// reported and never thrown: the store runs inside the fetch's own result
|
|
89
|
+
// handler, where a throw is caught and reported as a failed region —
|
|
90
|
+
// which would hide the violation being reported.
|
|
91
|
+
reportContractViolation('display', `${name}: region ${key} stored ` +
|
|
92
|
+
`\`${stored === null ? 'null' : typeof stored}\`, not a ` +
|
|
93
|
+
'payload. A fetch RPC answers a payload or a regionTooLarge ' +
|
|
94
|
+
'refusal; storing anything else leaves every reader of this map ' +
|
|
95
|
+
'reading through it.');
|
|
96
|
+
}
|
|
97
|
+
return set(key, value);
|
|
98
|
+
};
|
|
97
99
|
return map;
|
|
98
100
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function snapBoxHeightPx(heightPx: number): number;
|
|
2
|
+
export declare function snapBoxTopPx(topY: number, heightPx: number, scrollY: number): number;
|
|
2
3
|
export declare function snapBoxCenterYPx(centerY: number, heightPx: number, scrollY: number): number;
|
|
3
4
|
export declare function extendToMinWidthPx(x1: number, x2: number, minWidth: number): number;
|
|
@@ -26,8 +26,11 @@ export function snapBoxHeightPx(heightPx) {
|
|
|
26
26
|
}
|
|
27
27
|
return _t1;
|
|
28
28
|
}
|
|
29
|
+
export function snapBoxTopPx(topY, heightPx, scrollY) {
|
|
30
|
+
return Math.floor(((((topY + (heightPx * 0.5)) - (snapBoxHeightPx(heightPx) * 0.5)) - scrollY) + 0.5));
|
|
31
|
+
}
|
|
29
32
|
export function snapBoxCenterYPx(centerY, heightPx, scrollY) {
|
|
30
|
-
return ((
|
|
33
|
+
return ((snapBoxTopPx((centerY - (heightPx * 0.5)), heightPx, scrollY) + Math.floor((snapBoxHeightPx(heightPx) * 0.5))) + 0.5);
|
|
31
34
|
}
|
|
32
35
|
export function extendToMinWidthPx(x1, x2, minWidth) {
|
|
33
36
|
let dx = (x2 - x1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/render-core",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "GPU/Canvas2D rendering primitives for JBrowse displays: the HAL, the draw-lifecycle mixin, per-region/global backend bases, and the React backend hooks",
|
|
6
6
|
"keywords": [
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"./canvas2dUtils": "./esm/canvas2dUtils.js",
|
|
32
32
|
"./canvasContext": "./esm/canvasContext.js",
|
|
33
33
|
"./colorRampLut": "./esm/colorRampLut.js",
|
|
34
|
+
"./contractReports": "./esm/contractReports.js",
|
|
34
35
|
"./coverageBand": "./esm/coverageBand.js",
|
|
35
36
|
"./coverageBandBuffers": "./esm/coverageBandBuffers.js",
|
|
36
37
|
"./createRenderingBackend": "./esm/createRenderingBackend.js",
|
|
@@ -26,8 +26,12 @@ export function snapBoxHeightPx(heightPx: number): number {
|
|
|
26
26
|
return _t1
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function snapBoxTopPx(topY: number, heightPx: number, scrollY: number): number {
|
|
30
|
+
return Math.floor(((((topY + (heightPx * 0.5)) - (snapBoxHeightPx(heightPx) * 0.5)) - scrollY) + 0.5))
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
export function snapBoxCenterYPx(centerY: number, heightPx: number, scrollY: number): number {
|
|
30
|
-
return ((
|
|
34
|
+
return ((snapBoxTopPx((centerY - (heightPx * 0.5)), heightPx, scrollY) + Math.floor((snapBoxHeightPx(heightPx) * 0.5))) + 0.5)
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export function extendToMinWidthPx(x1: number, x2: number, minWidth: number): number {
|
package/src/shaders/hpmath.slang
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// must make identically to the GPU — which pixel row a glyph centers on, how a
|
|
9
9
|
// sub-pixel span is widened. `pnpm gen:shaders` transliterates them into
|
|
10
10
|
// hpmath.js.generated.ts; nothing downstream re-derives them by hand.
|
|
11
|
-
//! js-export: snapBoxHeightPx, snapBoxCenterYPx, extendToMinWidthPx
|
|
11
|
+
//! js-export: snapBoxHeightPx, snapBoxTopPx, snapBoxCenterYPx, extendToMinWidthPx
|
|
12
12
|
//! js-skip: hpSplitUint — the hi/lo float32 precision split exists because a GPU has no float64; the Canvas2D path just uses a number
|
|
13
13
|
//! js-skip: quadLocal — maps a vertex id to a quad corner — Canvas2D has no vertices, it calls fillRect
|
|
14
14
|
//! js-skip: yPxToClipY — clip space is the GPU coordinate system; the px-space decision it wraps is what gets exported (snapBoxCenterYPx and friends)
|
|
@@ -91,14 +91,27 @@ public float snapBoxHeightPx(float heightPx) {
|
|
|
91
91
|
return even && hPx >= 2.0 && hPx <= THIN_BOX_PX ? hPx + 1.0 : hPx;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// The pixel row a box's top edge lands on. Snapped about the box's CENTER, not
|
|
95
|
+
// its top: `snapBoxHeightPx` rounds the height and sometimes nudges it a whole
|
|
96
|
+
// pixel, and anchoring the top spends all of that off the BOTTOM edge. A box the
|
|
97
|
+
// layout deliberately centered inside its row then isn't — a UTR is
|
|
98
|
+
// center-shrunk to 0.65 of the transcript body, so at superCompact's 3px row its
|
|
99
|
+
// 1.95px box rounded up to 3px and drew a full pixel below the CDS beside it,
|
|
100
|
+
// while the intron line and strand arrow (which snap to the row's center row)
|
|
101
|
+
// sat on its top edge. Top-anchored and center-anchored agree wherever the
|
|
102
|
+
// height survives the snap unchanged, which is every full-height feature box.
|
|
103
|
+
public float snapBoxTopPx(float topY, float heightPx, float scrollY) {
|
|
104
|
+
float centerY = topY + heightPx * 0.5;
|
|
105
|
+
return floor(centerY - snapBoxHeightPx(heightPx) * 0.5 - scrollY + 0.5);
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
// Center a thin glyph on the *drawn* middle row of the box it annotates, given
|
|
95
|
-
// the box's real center `centerY` and height `heightPx`.
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// and stays consistent (just below the seam).
|
|
109
|
+
// the box's real center `centerY` and height `heightPx`. Reproducing the rect
|
|
110
|
+
// shader's own edge snapping (`snapBoxTopPx` + `snapBoxHeightPx`) lands the
|
|
111
|
+
// line/chevron exactly on the box's center row instead of the ~1px bias a plain
|
|
112
|
+
// pixel-center snap (`floor(centerY - scrollY + 0.5) + 0.5`) gives. Thin boxes
|
|
113
|
+
// are drawn at an odd height precisely so that center row exists; a taller even
|
|
114
|
+
// box still has none, and stays consistent (just below the seam).
|
|
102
115
|
//
|
|
103
116
|
// Split in two on purpose. The decision — which pixel row is the center — is
|
|
104
117
|
// resolution math the Canvas2D and SVG paths need in *screen px*; only the
|
|
@@ -107,7 +120,7 @@ public float snapBoxHeightPx(float heightPx) {
|
|
|
107
120
|
// re-deriving it (see adr-051). Keep new shared decisions in this shape: pure
|
|
108
121
|
// px-in/px-out below, the `yPxToClipY` wrapper above it.
|
|
109
122
|
public float snapBoxCenterYPx(float centerY, float heightPx, float scrollY) {
|
|
110
|
-
float topPx =
|
|
123
|
+
float topPx = snapBoxTopPx(centerY - heightPx * 0.5, heightPx, scrollY);
|
|
111
124
|
return topPx + floor(snapBoxHeightPx(heightPx) * 0.5) + 0.5;
|
|
112
125
|
}
|
|
113
126
|
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
// Row placement comes from uniforms rather than the instance data, so a
|
|
12
12
|
// rowHeight / rowProportion change re-renders without a refetch.
|
|
13
13
|
//
|
|
14
|
-
// What the callers legitimately differ on is the min-width floor
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
14
|
+
// What the callers legitimately differ on is the min-width floor they write
|
|
15
|
+
// into `minCellPx` — a uniform-write decision, not a reason for two shaders.
|
|
16
|
+
// Whether to floor at all turns on whether the caller's marks TILE: MAF's cells
|
|
17
|
+
// do, so a sub-pixel one is read as part of the run around it and widening it
|
|
18
|
+
// paints ink the alignment does not contain (measured, and MAF now passes 0 —
|
|
19
|
+
// `agent-docs/reference/MAF_SUBPIXEL_CELLS.md`). The multi-row painter's features
|
|
20
|
+
// are sparse intervals with nothing beside them, so it floors.
|
|
21
|
+
//! export-consts: MIN_DRAWN_ROW_PX, MULTI_ROW_MIN_CELL_PX
|
|
21
22
|
//! js-export: drawnRowHeightPx, rowBandOffsetPx
|
|
22
23
|
module rowRect;
|
|
23
24
|
|
|
@@ -36,16 +37,13 @@ public struct RowRectUniforms {
|
|
|
36
37
|
public float canvasHeight; // CSS px
|
|
37
38
|
// The width `minCellPx` is measured against, and so the thing that decides
|
|
38
39
|
// what unit `minCellPx` IS. Named for that rather than for the viewport
|
|
39
|
-
// because the
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// That disagreement is live and parked, not settled —
|
|
43
|
-
// agent-docs/ideas/maf-subpixel-cells.md — so the name carries it to every
|
|
44
|
-
// call site instead of a comment forty lines away.
|
|
40
|
+
// because the callers that floor pass a CSS width. Pass a DEVICE width and
|
|
41
|
+
// the floor moves with the reader's monitor — MAF did, and rendered
|
|
42
|
+
// measurably differently on a retina screen until it stopped flooring.
|
|
45
43
|
public float minCellDenomPx;
|
|
46
|
-
// Narrowest a cell is painted, in whatever unit `minCellDenomPx` is in.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
44
|
+
// Narrowest a cell is painted, in whatever unit `minCellDenomPx` is in. Zero
|
|
45
|
+
// is a no-op and is what a tiling caller writes (MAF); the multi-row painter
|
|
46
|
+
// names its own width, MULTI_ROW_MIN_CELL_PX.
|
|
49
47
|
public float minCellPx;
|
|
50
48
|
public float zero; // always 0.0 — prevents hp-split collapse
|
|
51
49
|
public float rowHeight; // CSS px per row
|
|
@@ -62,24 +60,16 @@ public struct RowRectVsOut {
|
|
|
62
60
|
public float4 color : COLOR0;
|
|
63
61
|
};
|
|
64
62
|
|
|
65
|
-
// Thinnest a row is ever painted
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// thinner. Floored, neighbours overlap instead.
|
|
69
|
-
static const float MIN_DRAWN_ROW_PX = 1.0;
|
|
70
|
-
|
|
71
|
-
// Thinnest a cell can be painted and still be drawable at all, for the same
|
|
72
|
-
// reason as the row floor above on the other axis. A caller writes this into
|
|
73
|
-
// `minCellPx` when a cell being *visible* is not its problem, only a cell
|
|
74
|
-
// existing; one whose whole encoding is carried by sub-pixel cells asks for
|
|
75
|
-
// more (MULTI_ROW_MIN_CELL_PX).
|
|
63
|
+
// Thinnest a row is ever painted: a row thinner than a pixel can miss every
|
|
64
|
+
// pixel center and disappear, so without this rows drop out as they get denser
|
|
65
|
+
// instead of just getting thinner. Floored, neighbours overlap instead.
|
|
76
66
|
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
static const float
|
|
67
|
+
// There is no X-axis twin. A row is a container the reader is counting, so
|
|
68
|
+
// losing one loses a species; a cell is a sample of a tiling, so losing one to
|
|
69
|
+
// the blend is the honest answer and flooring it invents ink
|
|
70
|
+
// (`agent-docs/reference/MAF_SUBPIXEL_CELLS.md`). The caller that does want an X
|
|
71
|
+
// floor names its own width, below.
|
|
72
|
+
static const float MIN_DRAWN_ROW_PX = 1.0;
|
|
83
73
|
|
|
84
74
|
// What the multi-row painter ("chromosome painting") writes into `minCellPx`.
|
|
85
75
|
//
|
|
@@ -91,9 +81,9 @@ static const float MIN_DRAWN_CELL_PX = 1.0;
|
|
|
91
81
|
// across two, so neither reaches full opacity — the tick reads as a smudge, and
|
|
92
82
|
// the eye scanning for structure across the rows finds none.
|
|
93
83
|
//
|
|
94
|
-
//
|
|
95
|
-
// and only where the alternative was showing nothing legible:
|
|
96
|
-
// enough to matter is already past it.
|
|
84
|
+
// A floor distorts width; that is what a floor is. This is the only caller that
|
|
85
|
+
// takes the trade, and only where the alternative was showing nothing legible:
|
|
86
|
+
// a feature wide enough to matter is already past it.
|
|
97
87
|
static const float MULTI_ROW_MIN_CELL_PX = 2.0;
|
|
98
88
|
|
|
99
89
|
public struct RowBand {
|