dsh-fonttune 0.7.6 → 0.7.8
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/CHANGELOG.en.md +18 -0
- package/CHANGELOG.md +18 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +449 -37
- package/lib/shared.cjs +126 -19
- package/package.json +5 -12
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.7.8] - 2026-11-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **The font picker opened slowly**: the list now builds one screenful, and the names are no longer drawn in each family's own font, so opening and scrolling stay quick on a machine with many installed fonts.
|
|
9
|
+
- **A pause when switching presets**: a preset remembers the notches that were measured for it, so switching uses them directly instead of measuring again at that moment.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Only verified host versions are declared**: 0.2.0-rc.1 through 0.2.2-rc.1 are listed one by one; older or untested versions are no longer claimed.
|
|
13
|
+
|
|
14
|
+
## [0.7.7] - 2026-11-05
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **The stall when opening the font settings**: the weight notches are now measured in steps that stay out of the settings page's open and refresh, and a family is measured once — later opens read the result instead of measuring again.
|
|
18
|
+
- **A long unresponsive page with some very large font collections**: when the font file is exceptionally large the measurement falls back to coarse notches after a time limit instead of holding the interface for a long time.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **DSH 0.2.2-rc.1 is supported**: the compatibility table in the README and the plugin manifest now list 0.2.1-rc.3, 0.2.1-rc.5 and 0.2.2-rc.1.
|
|
22
|
+
|
|
5
23
|
## [0.7.6] - 2026-11-04
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.7.8] - 2026-11-05
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **字体选择列表打开较慢**:列表现在只列出首屏,并且不再逐个用各自字体显示名称;机器上安装的字体很多时,打开与滚动都更快。
|
|
9
|
+
- **切换配置方案时的停顿**:方案会记住已测好的档位,切换时直接使用,切换的那一瞬间不再重新测量。
|
|
10
|
+
|
|
11
|
+
### 变更
|
|
12
|
+
- **兼容范围只保留实测过的版本**:从 0.2.0-rc.1 到 0.2.2-rc.1 逐个列出,更早或未验证的版本不再声明支持。
|
|
13
|
+
|
|
14
|
+
## [0.7.7] - 2026-11-05
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
- **打开字体设置时的卡顿**:字体档位的测量改为分步进行,并避开设置页面的打开与刷新;同一套字体只需测量一次,之后打开设置不再重新计算。
|
|
18
|
+
- **部分超大字体集合下的长时间无响应**:字体文件特别大时,测量会在超时后改用粗略档位,不再长时间占住界面。
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
- **兼容 DSH 0.2.2-rc.1**:README 的兼容表与插件清单同步补上 0.2.1-rc.3、0.2.1-rc.5 与 0.2.2-rc.1。
|
|
22
|
+
|
|
5
23
|
## [0.7.6] - 2026-11-04
|
|
6
24
|
|
|
7
25
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -39,7 +39,7 @@ The floating tuning panel (left, with its "in force right now" readout) and the
|
|
|
39
39
|
|
|
40
40
|
| Plugin version | Supported DSH versions |
|
|
41
41
|
| --- | --- |
|
|
42
|
-
| **0.7.x** (latest, 0.7.
|
|
42
|
+
| **0.7.x** (latest, 0.7.7) | 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-rc.1, 0.2.1-rc.2, 0.2.1-rc.3, 0.2.1-rc.4, 0.2.1-rc.5, 0.2.2-rc.1|
|
|
43
43
|
| 0.6.x (0.6.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
|
|
44
44
|
| 0.5.x (0.5.0) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
|
45
45
|
| 0.4.x (0.4.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
| 插件版本 | 支持的 DSH 版本 |
|
|
41
41
|
| --- | --- |
|
|
42
|
-
| **0.7.x**(最新,0.7.
|
|
42
|
+
| **0.7.x**(最新,0.7.7) | 0.2.0-rc.1、0.2.0-rc.2、0.2.1-rc.1、0.2.1-rc.2、0.2.1-rc.3、0.2.1-rc.4、0.2.1-rc.5、0.2.2-rc.1|
|
|
43
43
|
| 0.6.x(0.6.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
|
|
44
44
|
| 0.5.x(0.5.0) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
|
45
45
|
| 0.4.x(0.4.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
|
package/lib/client.js
CHANGED
|
@@ -3476,6 +3476,70 @@ function balancedStackShape(entries, base, low, high, spread) {
|
|
|
3476
3476
|
return best;
|
|
3477
3477
|
}
|
|
3478
3478
|
|
|
3479
|
+
/**
|
|
3480
|
+
* The FIRST weight of an arithmetic grid whose rendering matches a signature.
|
|
3481
|
+
*
|
|
3482
|
+
* The probe grid is monotone: below a family's lightest face every weight renders
|
|
3483
|
+
* the same (the lightest face), above its heaviest face every weight renders the
|
|
3484
|
+
* same (the heaviest face), and in between it walks the faces. So "renders like the
|
|
3485
|
+
* ceiling" is a predicate that is false, then true, and its boundary is worth
|
|
3486
|
+
* `log2` samples instead of one render per grid point — which is what the two
|
|
3487
|
+
* horizon scans used to cost (eighty-one renders per family, issue #3).
|
|
3488
|
+
* @param {(weight: number) => string|number} sample - renders a probe at one weight.
|
|
3489
|
+
* @param {string|number} signature - the signature the boundary shares.
|
|
3490
|
+
* @param {number} low - first weight of the grid.
|
|
3491
|
+
* @param {number} high - last weight of the grid.
|
|
3492
|
+
* @param {number} step - spacing of the grid.
|
|
3493
|
+
* @param {number|null} fallback - returned when no grid weight matches.
|
|
3494
|
+
* @returns {number|null} the weight, or the fallback.
|
|
3495
|
+
*/
|
|
3496
|
+
function firstGridWeight(sample, signature, low, high, step, fallback) {
|
|
3497
|
+
var count = Math.floor((high - low) / step) + 1;
|
|
3498
|
+
if (count < 1 || !(step > 0)) return fallback;
|
|
3499
|
+
var at = function (index) {
|
|
3500
|
+
return low + index * step;
|
|
3501
|
+
};
|
|
3502
|
+
if (sample(at(count - 1)) !== signature) return fallback;
|
|
3503
|
+
var first = 0;
|
|
3504
|
+
var last = count - 1;
|
|
3505
|
+
while (first < last) {
|
|
3506
|
+
var middle = (first + last) >> 1;
|
|
3507
|
+
if (sample(at(middle)) === signature) last = middle;
|
|
3508
|
+
else first = middle + 1;
|
|
3509
|
+
}
|
|
3510
|
+
return at(first);
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
/**
|
|
3514
|
+
* The LAST weight of an arithmetic grid whose rendering matches a signature.
|
|
3515
|
+
*
|
|
3516
|
+
* The mirror of `firstGridWeight`: "renders like the floor" is true, then false,
|
|
3517
|
+
* so the boundary is found from the other end.
|
|
3518
|
+
* @param {(weight: number) => string|number} sample - renders a probe at one weight.
|
|
3519
|
+
* @param {string|number} signature - the signature the boundary shares.
|
|
3520
|
+
* @param {number} low - first weight of the grid.
|
|
3521
|
+
* @param {number} high - last weight of the grid.
|
|
3522
|
+
* @param {number} step - spacing of the grid.
|
|
3523
|
+
* @param {number|null} fallback - returned when no grid weight matches.
|
|
3524
|
+
* @returns {number|null} the weight, or the fallback.
|
|
3525
|
+
*/
|
|
3526
|
+
function lastGridWeight(sample, signature, low, high, step, fallback) {
|
|
3527
|
+
var count = Math.floor((high - low) / step) + 1;
|
|
3528
|
+
if (count < 1 || !(step > 0)) return fallback;
|
|
3529
|
+
var at = function (index) {
|
|
3530
|
+
return low + index * step;
|
|
3531
|
+
};
|
|
3532
|
+
if (sample(at(0)) !== signature) return fallback;
|
|
3533
|
+
var first = 0;
|
|
3534
|
+
var last = count - 1;
|
|
3535
|
+
while (first < last) {
|
|
3536
|
+
var middle = (first + last + 1) >> 1;
|
|
3537
|
+
if (sample(at(middle)) === signature) first = middle;
|
|
3538
|
+
else last = middle - 1;
|
|
3539
|
+
}
|
|
3540
|
+
return at(first);
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3479
3543
|
/**
|
|
3480
3544
|
* One slider shape for a stack whose text can land on more than one family.
|
|
3481
3545
|
*
|
|
@@ -3637,30 +3701,38 @@ function weightProfileFrom(sample, options) {
|
|
|
3637
3701
|
var top = typeof settings.top === "number" ? settings.top : WEIGHT_LADDER_STRONG;
|
|
3638
3702
|
var topSignature = sample(WEIGHT_PROBE_HIGH);
|
|
3639
3703
|
var bottomSignature = sample(WEIGHT_PROBE_LOW);
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3704
|
+
// The two horizons used to be found by walking the whole probe grid from one end
|
|
3705
|
+
// — eighty-one canvas renders per family, and on a font collection of a few
|
|
3706
|
+
// hundred megabytes each render is the browser rasterizing a new weight (issue
|
|
3707
|
+
// #3: seconds of blocked main thread). The grid is monotone in weight: everything
|
|
3708
|
+
// from a family's heaviest face UP renders the same, so the horizon is a
|
|
3709
|
+
// boundary, and a bisection finds it in seven samples instead of eighty-one.
|
|
3710
|
+
var heaviest = firstGridWeight(
|
|
3711
|
+
sample,
|
|
3712
|
+
topSignature,
|
|
3713
|
+
WEIGHT_PROBE_LOW,
|
|
3714
|
+
WEIGHT_PROBE_HIGH,
|
|
3715
|
+
WEIGHT_PROBE_STEP,
|
|
3716
|
+
WEIGHT_PROBE_HIGH
|
|
3717
|
+
);
|
|
3718
|
+
var lightest = lastGridWeight(
|
|
3719
|
+
sample,
|
|
3720
|
+
bottomSignature,
|
|
3721
|
+
WEIGHT_PROBE_LOW,
|
|
3722
|
+
WEIGHT_PROBE_HIGH,
|
|
3723
|
+
WEIGHT_PROBE_STEP,
|
|
3724
|
+
WEIGHT_PROBE_LOW
|
|
3725
|
+
);
|
|
3654
3726
|
// How far the offset can go before NO layer that must move can move any more:
|
|
3655
3727
|
// the widest horizon among those bases, since a layer that could still change
|
|
3656
3728
|
// must not be cut off by one that already sits at its heaviest face.
|
|
3657
3729
|
var headroom = Math.max(0, heaviest - WEIGHT_BASE);
|
|
3658
3730
|
if (top !== WEIGHT_BASE) {
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3731
|
+
// Same boundary, same bisection: the first weight at or above the heavy base
|
|
3732
|
+
// that already renders at the ceiling.
|
|
3733
|
+
var reached = firstGridWeight(sample, topSignature, top, WEIGHT_PROBE_HIGH, WEIGHT_PROBE_STEP, null);
|
|
3734
|
+
if (reached !== null) {
|
|
3735
|
+
headroom = Math.max(headroom, reached - top);
|
|
3664
3736
|
}
|
|
3665
3737
|
}
|
|
3666
3738
|
var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
|
|
@@ -4928,6 +5000,40 @@ function catalogRefreshDue(cache, now, retryMs) {
|
|
|
4928
5000
|
return now - (cache.at || 0) >= retryMs;
|
|
4929
5001
|
}
|
|
4930
5002
|
|
|
5003
|
+
/**
|
|
5004
|
+
* The rows one picker group actually renders.
|
|
5005
|
+
*
|
|
5006
|
+
* A machine with a huge font collection — or a Super TTC, which exposes every
|
|
5007
|
+
* weight and variant as its own family — can enumerate hundreds of names, and
|
|
5008
|
+
* building a row for each one is a visible stall when the list opens (issue #3:
|
|
5009
|
+
* the reporter's second symptom). The list therefore renders a screenful unless
|
|
5010
|
+
* the reader is searching, and a family that is already selected is always
|
|
5011
|
+
* rendered so its check mark never falls outside the window.
|
|
5012
|
+
* @param {string[]} families - the group's families, in display order.
|
|
5013
|
+
* @param {string} query - the search text (empty means "browse the whole list").
|
|
5014
|
+
* @param {Object<string, boolean>} selectedLower - lower-cased names already picked.
|
|
5015
|
+
* @param {number} [limit] - how many rows a browse renders.
|
|
5016
|
+
* @returns {string[]} the families to render.
|
|
5017
|
+
*/
|
|
5018
|
+
function pickerRows(families, query, selectedLower, limit) {
|
|
5019
|
+
var list = Array.isArray(families) ? families : [];
|
|
5020
|
+
var searching = typeof query === "string" && query.trim() !== "";
|
|
5021
|
+
if (searching) return list.slice();
|
|
5022
|
+
var cap = typeof limit === "number" && limit > 0 ? Math.floor(limit) : 60;
|
|
5023
|
+
var picked = selectedLower !== null && typeof selectedLower === "object" ? selectedLower : {};
|
|
5024
|
+
var rows = [];
|
|
5025
|
+
var shown = 0;
|
|
5026
|
+
for (var index = 0; index < list.length; index += 1) {
|
|
5027
|
+
var name = list[index];
|
|
5028
|
+
var isSelected = picked[String(name).toLowerCase()] === true;
|
|
5029
|
+
if (isSelected || shown < cap) {
|
|
5030
|
+
rows.push(name);
|
|
5031
|
+
shown += 1;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
return rows;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4931
5037
|
var shared = {
|
|
4932
5038
|
NAMESPACE: NAMESPACE,
|
|
4933
5039
|
SANS_FIELD: SANS_FIELD,
|
|
@@ -5075,6 +5181,7 @@ var shared = {
|
|
|
5075
5181
|
PRESETS: PRESETS,
|
|
5076
5182
|
PRESET_GROUPS: PRESET_GROUPS,
|
|
5077
5183
|
MAX_LOCAL_FONTS: MAX_LOCAL_FONTS,
|
|
5184
|
+
pickerRows: pickerRows,
|
|
5078
5185
|
pickerGroups: pickerGroups,
|
|
5079
5186
|
catalogRefreshDue: catalogRefreshDue,
|
|
5080
5187
|
MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
|
|
@@ -6483,6 +6590,18 @@ var weightProbeCanvas = null;
|
|
|
6483
6590
|
/** Cache of measured slider shapes, keyed by the resolved family stack. */
|
|
6484
6591
|
var weightProfileCache = {};
|
|
6485
6592
|
|
|
6593
|
+
/**
|
|
6594
|
+
* How slow one canvas render may be before the font is treated as too heavy to
|
|
6595
|
+
* sweep in detail (a few-hundred-megabyte collection answers in tens of ms).
|
|
6596
|
+
*/
|
|
6597
|
+
var SLOW_SAMPLE_MS = 40;
|
|
6598
|
+
|
|
6599
|
+
/** How long after load the code axis is measured (the other two go first). */
|
|
6600
|
+
var WARM_CODE_DELAY_MS = 2500;
|
|
6601
|
+
|
|
6602
|
+
/** Thrown by the guarded sampler to abandon the fine sweep on a slow font. */
|
|
6603
|
+
var WEIGHT_TOO_SLOW = { tooSlow: true };
|
|
6604
|
+
|
|
6486
6605
|
/**
|
|
6487
6606
|
* The families a stack can actually render text with.
|
|
6488
6607
|
*
|
|
@@ -6561,6 +6680,15 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6561
6680
|
return familyProfileCache[key];
|
|
6562
6681
|
}
|
|
6563
6682
|
var record = null;
|
|
6683
|
+
// A font collection of a few hundred megabytes makes EVERY render expensive (the
|
|
6684
|
+
// browser re-parses the collection for a weight it has not drawn yet), and the
|
|
6685
|
+
// sweep asks for a dozen of them in a row — measured at 20-50 ms each on such a
|
|
6686
|
+
// file, i.e. a second of blocked main thread per family (issue #3). One render
|
|
6687
|
+
// that costs more than SLOW_SAMPLE_MS is enough evidence: abandon the fine sweep
|
|
6688
|
+
// and answer with a coarse one, so the worst case stays bounded instead of
|
|
6689
|
+
// growing with the file size.
|
|
6690
|
+
var slow = false;
|
|
6691
|
+
var guarded = null;
|
|
6564
6692
|
try {
|
|
6565
6693
|
var target = '"' + (alias === "" ? family : alias) + '"';
|
|
6566
6694
|
var seen = {};
|
|
@@ -6570,10 +6698,16 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6570
6698
|
}
|
|
6571
6699
|
return seen[weight];
|
|
6572
6700
|
};
|
|
6701
|
+
guarded = function (weight) {
|
|
6702
|
+
var started = Date.now();
|
|
6703
|
+
var signature = sample(weight);
|
|
6704
|
+
if (Date.now() - started >= SLOW_SAMPLE_MS) throw WEIGHT_TOO_SLOW;
|
|
6705
|
+
return signature;
|
|
6706
|
+
};
|
|
6573
6707
|
var shape =
|
|
6574
6708
|
alias === ""
|
|
6575
|
-
? weightProfileFrom(
|
|
6576
|
-
: weightProfileFrom(
|
|
6709
|
+
? weightProfileFrom(guarded, { top: base })
|
|
6710
|
+
: weightProfileFrom(guarded, { top: base, candidates: shared.CONTINUOUS_CANDIDATES });
|
|
6577
6711
|
record = { min: shape.min, max: shape.max, step: shape.step, inkOf: null };
|
|
6578
6712
|
if (text !== null) {
|
|
6579
6713
|
var inks = {};
|
|
@@ -6585,7 +6719,31 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6585
6719
|
};
|
|
6586
6720
|
}
|
|
6587
6721
|
} catch (error) {
|
|
6588
|
-
|
|
6722
|
+
if (error !== WEIGHT_TOO_SLOW) {
|
|
6723
|
+
record = null;
|
|
6724
|
+
} else {
|
|
6725
|
+
// Too slow for the fine sweep: nine renders on the hundred grid describe the
|
|
6726
|
+
// family coarsely, which is still a control that moves in visible steps. No
|
|
6727
|
+
// ink comparison — that would be a second sweep of the same price.
|
|
6728
|
+
slow = true;
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
if (slow) {
|
|
6732
|
+
try {
|
|
6733
|
+
var coarseSeen = {};
|
|
6734
|
+
var coarse = weightProfileFrom(
|
|
6735
|
+
function (weight) {
|
|
6736
|
+
if (!Object.prototype.hasOwnProperty.call(coarseSeen, weight)) {
|
|
6737
|
+
coarseSeen[weight] = sampleWeightSignature(target, Math.round(weight / 100) * 100);
|
|
6738
|
+
}
|
|
6739
|
+
return coarseSeen[weight];
|
|
6740
|
+
},
|
|
6741
|
+
{ top: base, candidates: [100] }
|
|
6742
|
+
);
|
|
6743
|
+
record = { min: coarse.min, max: coarse.max, step: coarse.step, inkOf: null };
|
|
6744
|
+
} catch (error) {
|
|
6745
|
+
record = null;
|
|
6746
|
+
}
|
|
6589
6747
|
}
|
|
6590
6748
|
familyProfileCache[key] = record;
|
|
6591
6749
|
return record;
|
|
@@ -6965,6 +7123,123 @@ function inkOfSignature(signature) {
|
|
|
6965
7123
|
return isFinite(ink) ? ink : 0;
|
|
6966
7124
|
}
|
|
6967
7125
|
|
|
7126
|
+
/**
|
|
7127
|
+
* Where a measured slider shape survives a page reload.
|
|
7128
|
+
*
|
|
7129
|
+
* Measuring one axis is around eighty canvas renders per family, and the cost per
|
|
7130
|
+
* render is the browser rasterizing a weight it has never drawn (measured: ~3 ms
|
|
7131
|
+
* for an 18 MB family, ~6 ms for a 35 MB one, and seconds per render on a font
|
|
7132
|
+
* collection of a few hundred megabytes — issue #3). The answer only changes when
|
|
7133
|
+
* the reader changes the fonts, so it is kept: the first visit pays, every later
|
|
7134
|
+
* one opens instantly. The TTL bounds how long a font file that was replaced
|
|
7135
|
+
* under the same family name may go unnoticed.
|
|
7136
|
+
*/
|
|
7137
|
+
var SHAPE_STORE_KEY = "dsh-fonttune:weight-shapes:v1";
|
|
7138
|
+
var SHAPE_STORE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7139
|
+
var SHAPE_STORE_LIMIT = 32;
|
|
7140
|
+
|
|
7141
|
+
/**
|
|
7142
|
+
* What a slider shows while its measurement has not landed yet.
|
|
7143
|
+
*
|
|
7144
|
+
* The nominal offset window, unit one: the same answer the code already gives when
|
|
7145
|
+
* a family cannot be measured at all, so nothing new appears in the document and a
|
|
7146
|
+
* stored offset stays reachable. Rendering is what used to MEASURE (a cold cache at
|
|
7147
|
+
* open = eighty canvas renders on the main thread), which is the "opening the
|
|
7148
|
+
* settings freezes" of issue #3; the real counts replace these the moment the idle
|
|
7149
|
+
* measurement finishes and the card redraws.
|
|
7150
|
+
*/
|
|
7151
|
+
var PENDING_WEIGHT_SHAPE = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
7152
|
+
|
|
7153
|
+
/** The decoded store, or an empty one when storage is unavailable. */
|
|
7154
|
+
var shapeStore = null;
|
|
7155
|
+
|
|
7156
|
+
/**
|
|
7157
|
+
* The persistent shape store, read once.
|
|
7158
|
+
* @returns {Object} key -> `{min, max, step, at}`.
|
|
7159
|
+
*/
|
|
7160
|
+
function readShapeStore() {
|
|
7161
|
+
if (shapeStore !== null) return shapeStore;
|
|
7162
|
+
shapeStore = {};
|
|
7163
|
+
try {
|
|
7164
|
+
var raw = globalThis.localStorage.getItem(SHAPE_STORE_KEY);
|
|
7165
|
+
var parsed = raw === null ? null : JSON.parse(raw);
|
|
7166
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
7167
|
+
for (var key in parsed) {
|
|
7168
|
+
if (!Object.prototype.hasOwnProperty.call(parsed, key)) continue;
|
|
7169
|
+
var entry = parsed[key];
|
|
7170
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
7171
|
+
if (!isFinite(entry.min) || !isFinite(entry.max) || !isFinite(entry.step)) continue;
|
|
7172
|
+
if (!(entry.step > 0) || !isFinite(entry.at)) continue;
|
|
7173
|
+
shapeStore[key] = { min: entry.min, max: entry.max, step: entry.step, at: entry.at };
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
} catch (error) {
|
|
7177
|
+
// Private mode, a disabled store, a corrupted value: measuring still works.
|
|
7178
|
+
}
|
|
7179
|
+
return shapeStore;
|
|
7180
|
+
}
|
|
7181
|
+
|
|
7182
|
+
/**
|
|
7183
|
+
* The stored shape for one cache key, when it is still fresh.
|
|
7184
|
+
* @param {string} key - the same key the memory cache uses.
|
|
7185
|
+
* @returns {{min: number, max: number, step: number}|null} the shape, or null.
|
|
7186
|
+
*/
|
|
7187
|
+
function storedWeightShape(key) {
|
|
7188
|
+
var store = readShapeStore();
|
|
7189
|
+
if (!Object.prototype.hasOwnProperty.call(store, key)) return null;
|
|
7190
|
+
var entry = store[key];
|
|
7191
|
+
if (Date.now() - entry.at > SHAPE_STORE_TTL_MS) return null;
|
|
7192
|
+
return { min: entry.min, max: entry.max, step: entry.step };
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7195
|
+
/**
|
|
7196
|
+
* Keep one measured shape for the next page load.
|
|
7197
|
+
*
|
|
7198
|
+
* Writes the whole (small) map back, dropping the oldest entries past the limit:
|
|
7199
|
+
* a reader who tries many browsing fonts must not grow this without bound.
|
|
7200
|
+
* @param {string} key - the same key the memory cache uses.
|
|
7201
|
+
* @param {{min: number, max: number, step: number}} profile - what was measured.
|
|
7202
|
+
*/
|
|
7203
|
+
function rememberWeightShape(key, profile) {
|
|
7204
|
+
var store = readShapeStore();
|
|
7205
|
+
store[key] = { min: profile.min, max: profile.max, step: profile.step, at: Date.now() };
|
|
7206
|
+
var keys = Object.keys(store);
|
|
7207
|
+
if (keys.length > SHAPE_STORE_LIMIT) {
|
|
7208
|
+
keys.sort(function (left, right) {
|
|
7209
|
+
return store[left].at - store[right].at;
|
|
7210
|
+
});
|
|
7211
|
+
for (var index = 0; index < keys.length - SHAPE_STORE_LIMIT; index += 1) {
|
|
7212
|
+
delete store[keys[index]];
|
|
7213
|
+
}
|
|
7214
|
+
}
|
|
7215
|
+
try {
|
|
7216
|
+
globalThis.localStorage.setItem(SHAPE_STORE_KEY, JSON.stringify(store));
|
|
7217
|
+
} catch (error) {
|
|
7218
|
+
// A full or disabled store only costs the next load the measurement again.
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
|
|
7222
|
+
/**
|
|
7223
|
+
* What to redraw when a measurement lands after the first paint.
|
|
7224
|
+
*
|
|
7225
|
+
* The card renders the count of positions, which is a property of the font: a
|
|
7226
|
+
* shape that arrives later (the idle warm-up, a slower family) has to reach the
|
|
7227
|
+
* mounted cards or they would keep showing the provisional counts.
|
|
7228
|
+
*/
|
|
7229
|
+
var shapeListeners = [];
|
|
7230
|
+
|
|
7231
|
+
/** Ask every mounted card to re-read the shapes. */
|
|
7232
|
+
function notifyShapes() {
|
|
7233
|
+
var pending = shapeListeners.slice();
|
|
7234
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
7235
|
+
try {
|
|
7236
|
+
pending[index]();
|
|
7237
|
+
} catch (error) {
|
|
7238
|
+
// A listener must never break the measurement that notified it.
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
|
|
6968
7243
|
/**
|
|
6969
7244
|
* The slider shape to use for one family: its range and its notch.
|
|
6970
7245
|
*
|
|
@@ -6972,15 +7247,19 @@ function inkOfSignature(signature) {
|
|
|
6972
7247
|
* range is measured rather than fixed (`weightProfileFrom`), which also makes
|
|
6973
7248
|
* every notch a change — for every layer of the ladder, so the measurement is
|
|
6974
7249
|
* told which base is the heaviest one the offset lands on. Measured once per
|
|
6975
|
-
* family and base and cached
|
|
6976
|
-
* to the nominal window with single units.
|
|
7250
|
+
* family and base and cached, in memory and across page loads; any failure (no
|
|
7251
|
+
* canvas, no 2D context) falls back to the nominal window with single units.
|
|
6977
7252
|
* @param {string} stack - resolved `font-family` value, "" for the page default.
|
|
6978
7253
|
* @param {number} [top] - the heaviest base the offset is added to; the ladder's
|
|
6979
7254
|
* own top for the conversation and interface axes, the axis's base for code.
|
|
7255
|
+
* @param {{peek?: boolean}} [options] - `peek: true` never measures: it answers
|
|
7256
|
+
* with the stored shape when there is one and the provisional shape otherwise,
|
|
7257
|
+
* so a render can never block on a canvas sweep (see `PENDING_WEIGHT_SHAPE`).
|
|
6980
7258
|
* @returns {{min: number, max: number, step: number}} the slider shape.
|
|
6981
7259
|
*/
|
|
6982
|
-
function weightProfileFor(stack, top) {
|
|
7260
|
+
function weightProfileFor(stack, top, options) {
|
|
6983
7261
|
var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
|
|
7262
|
+
var peek = options !== null && typeof options === "object" && options.peek === true;
|
|
6984
7263
|
var resolved = String(stack) === "" ? liveFamily() : String(stack);
|
|
6985
7264
|
// `hasOwnProperty` rather than a truthiness test: the alias map is keyed by family
|
|
6986
7265
|
// name, and a family called "constructor" must not read as promoted.
|
|
@@ -6994,6 +7273,15 @@ function weightProfileFor(stack, top) {
|
|
|
6994
7273
|
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
6995
7274
|
return weightProfileCache[key];
|
|
6996
7275
|
}
|
|
7276
|
+
// Measured on a previous visit: use it and skip the sweep entirely.
|
|
7277
|
+
var stored = storedWeightShape(key);
|
|
7278
|
+
if (stored !== null) {
|
|
7279
|
+
weightProfileCache[key] = stored;
|
|
7280
|
+
return stored;
|
|
7281
|
+
}
|
|
7282
|
+
// A render must never wait for eighty canvas renders: answer with the
|
|
7283
|
+
// provisional shape now, and let the idle warm-up measure and redraw.
|
|
7284
|
+
if (peek) return PENDING_WEIGHT_SHAPE;
|
|
6997
7285
|
var profile = promoted
|
|
6998
7286
|
? shared.continuousWeightProfile()
|
|
6999
7287
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
@@ -7034,6 +7322,20 @@ function weightProfileFor(stack, top) {
|
|
|
7034
7322
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
7035
7323
|
}
|
|
7036
7324
|
weightProfileCache[key] = profile;
|
|
7325
|
+
// A measured answer is worth a reload's freedom from the sweep, and the count it
|
|
7326
|
+
// changes has to reach whatever is already on screen with the provisional one.
|
|
7327
|
+
// Only when it actually differs: a family nothing could measure (no canvas) lands
|
|
7328
|
+
// on exactly the provisional window, and redrawing for that would be noise.
|
|
7329
|
+
rememberWeightShape(key, profile);
|
|
7330
|
+
// Nothing distinguishable measured (no canvas, a family the page cannot draw)
|
|
7331
|
+
// leaves the degenerate shape, which is not worth a redraw.
|
|
7332
|
+
var changed =
|
|
7333
|
+
profile.min !== 0 || profile.max !== 0
|
|
7334
|
+
? profile.min !== PENDING_WEIGHT_SHAPE.min ||
|
|
7335
|
+
profile.max !== PENDING_WEIGHT_SHAPE.max ||
|
|
7336
|
+
profile.step !== PENDING_WEIGHT_SHAPE.step
|
|
7337
|
+
: false;
|
|
7338
|
+
if (changed) notifyShapes();
|
|
7037
7339
|
return profile;
|
|
7038
7340
|
}
|
|
7039
7341
|
|
|
@@ -7086,7 +7388,22 @@ function deferWeightWarm(job) {
|
|
|
7086
7388
|
* @param {object} editing - the value set the axes read.
|
|
7087
7389
|
*/
|
|
7088
7390
|
var warmSignature = null;
|
|
7391
|
+
|
|
7392
|
+
/** When a preset was applied: its warm-up waits so the switch itself feels instant. */
|
|
7393
|
+
var presetSwitchAt = 0;
|
|
7394
|
+
|
|
7395
|
+
/** The code stack the deferred slice was last scheduled for (scheduled once). */
|
|
7396
|
+
var codeWarmSignature = null;
|
|
7089
7397
|
var scheduleWeightWarm = function (editing) {
|
|
7398
|
+
// A preset switch changes several stacks at once. Hydrated shapes make the rows
|
|
7399
|
+
// correct immediately, so the sweep those stacks would trigger can wait: measuring
|
|
7400
|
+
// inside the switch is what made switching stutter on a slow font.
|
|
7401
|
+
if (Date.now() - presetSwitchAt < 1200) {
|
|
7402
|
+
globalThis.setTimeout(function () {
|
|
7403
|
+
scheduleWeightWarm(editing);
|
|
7404
|
+
}, 1200);
|
|
7405
|
+
return;
|
|
7406
|
+
}
|
|
7090
7407
|
var stacks = weightAxisStacks(editing);
|
|
7091
7408
|
var signature = stacks.dialog + "\u0000" + stacks.ui + "\u0000" + stacks.code;
|
|
7092
7409
|
if (signature === warmSignature) return;
|
|
@@ -7102,11 +7419,6 @@ var scheduleWeightWarm = function (editing) {
|
|
|
7102
7419
|
weightProfileFor(stacks.ui, shared.WEIGHT_LADDER_STRONG);
|
|
7103
7420
|
probeLog("weight-warm", { axis: "ui", stack: stacks.ui });
|
|
7104
7421
|
},
|
|
7105
|
-
function () {
|
|
7106
|
-
probeContinuity(stacks.code);
|
|
7107
|
-
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
7108
|
-
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
7109
|
-
},
|
|
7110
7422
|
];
|
|
7111
7423
|
var runNext = function () {
|
|
7112
7424
|
var job = jobs.shift();
|
|
@@ -7115,6 +7427,20 @@ var scheduleWeightWarm = function (editing) {
|
|
|
7115
7427
|
deferWeightWarm(runNext);
|
|
7116
7428
|
};
|
|
7117
7429
|
deferWeightWarm(runNext);
|
|
7430
|
+
// The code axis is measured LAST and later still: its family is usually a
|
|
7431
|
+
// monospace the reader rarely touches, and on a slow machine the two axes above
|
|
7432
|
+
// already keep the main thread busy. Deferring it keeps the first seconds after
|
|
7433
|
+
// load (when a reader is likeliest to open the settings) free of its sweep.
|
|
7434
|
+
var codeSignature = stacks.code;
|
|
7435
|
+
if (codeSignature === codeWarmSignature) return;
|
|
7436
|
+
codeWarmSignature = codeSignature;
|
|
7437
|
+
globalThis.setTimeout(function () {
|
|
7438
|
+
deferWeightWarm(function () {
|
|
7439
|
+
probeContinuity(stacks.code);
|
|
7440
|
+
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
7441
|
+
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
7442
|
+
});
|
|
7443
|
+
}, WARM_CODE_DELAY_MS);
|
|
7118
7444
|
};
|
|
7119
7445
|
|
|
7120
7446
|
/**
|
|
@@ -8268,11 +8594,18 @@ function StackPicker(props) {
|
|
|
8268
8594
|
var panel = null;
|
|
8269
8595
|
if (open) {
|
|
8270
8596
|
var rows = [];
|
|
8597
|
+
// A machine with a huge font collection (or a Super TTC, which exposes every
|
|
8598
|
+
// weight and variant as its own family) can enumerate hundreds of names. Only
|
|
8599
|
+
// the first screenful is built unless the reader is searching, and a selected
|
|
8600
|
+
// family is always among them — drawing every row was a second of layout on
|
|
8601
|
+
// such a machine. The subsetting itself is pure and covered offline.
|
|
8602
|
+
var RENDER_LIMIT = 60;
|
|
8271
8603
|
for (var groupRender = 0; groupRender < options.length; groupRender += 1) {
|
|
8272
8604
|
var group = options[groupRender];
|
|
8273
8605
|
rows.push(h("div", { className: "dfp-group", key: "g:" + group.label }, t(group.label)));
|
|
8274
|
-
|
|
8275
|
-
|
|
8606
|
+
var visible = shared.pickerRows(group.families, typed, selectedLower, RENDER_LIMIT);
|
|
8607
|
+
for (var rowIndex = 0; rowIndex < visible.length; rowIndex += 1) {
|
|
8608
|
+
var name = visible[rowIndex];
|
|
8276
8609
|
var isSelected = selectedLower[name.toLowerCase()] === true;
|
|
8277
8610
|
rows.push(
|
|
8278
8611
|
h(
|
|
@@ -8281,12 +8614,13 @@ function StackPicker(props) {
|
|
|
8281
8614
|
type: "button",
|
|
8282
8615
|
className: "dfp-option",
|
|
8283
8616
|
key: "o:" + group.label + ":" + name,
|
|
8284
|
-
style: { fontFamily: quoteFamily(name) },
|
|
8285
8617
|
"data-family": name,
|
|
8286
8618
|
onClick: function (event) {
|
|
8287
8619
|
toggle(event.currentTarget.dataset.family);
|
|
8288
8620
|
},
|
|
8289
8621
|
},
|
|
8622
|
+
// The rows are NOT drawn in their own family: on a large collection that
|
|
8623
|
+
// loaded one font per row, which is exactly the stall this list is about.
|
|
8290
8624
|
h("span", { className: "dfp-optionLabel" }, name),
|
|
8291
8625
|
isSelected ? h("span", { className: "dfp-optionCheck" }, "✓") : null
|
|
8292
8626
|
)
|
|
@@ -9066,7 +9400,9 @@ function FontCard(props) {
|
|
|
9066
9400
|
var next = presetList.slice();
|
|
9067
9401
|
for (var index = 0; index < next.length; index += 1) {
|
|
9068
9402
|
if (next[index].name === entry.name) {
|
|
9403
|
+
var shapes = shapeSnapshotFor(values);
|
|
9069
9404
|
next[index] = { name: entry.name, values: values, savedAt: Date.now() };
|
|
9405
|
+
if (Object.keys(shapes).length > 0) next[index].shapes = shapes;
|
|
9070
9406
|
}
|
|
9071
9407
|
}
|
|
9072
9408
|
return { op: shared.PENDING_SET, path: [PRESETS_FIELD], value: JSON.stringify(next) };
|
|
@@ -9453,7 +9789,8 @@ function FontCard(props) {
|
|
|
9453
9789
|
var weightField = function (props) {
|
|
9454
9790
|
var profile = weightProfileFor(
|
|
9455
9791
|
props.stack === undefined ? "" : props.stack,
|
|
9456
|
-
props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top
|
|
9792
|
+
props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top,
|
|
9793
|
+
{ peek: true }
|
|
9457
9794
|
);
|
|
9458
9795
|
var stored = props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value;
|
|
9459
9796
|
// A stored offset outside the measured range (the font changed since it was
|
|
@@ -9909,10 +10246,63 @@ function FontCard(props) {
|
|
|
9909
10246
|
* is exact.
|
|
9910
10247
|
* @param {{name: string, values: object}} entry - the picked preset.
|
|
9911
10248
|
*/
|
|
10249
|
+
/**
|
|
10250
|
+
* The measured shapes a preset can carry.
|
|
10251
|
+
*
|
|
10252
|
+
* Switching presets changes the font stacks, which invalidates the slider shapes:
|
|
10253
|
+
* the new stacks would be measured again (a second of canvas work on a huge font
|
|
10254
|
+
* collection) the moment they are applied. A preset therefore remembers the
|
|
10255
|
+
* shapes it was saved with — three small records — and applying it restores them
|
|
10256
|
+
* instead of measuring (issue #3 follow-up: "switching presets stutters").
|
|
10257
|
+
* @param {object} values - the preset's value set.
|
|
10258
|
+
* @returns {object} cache key -> shape, empty when nothing is known yet.
|
|
10259
|
+
*/
|
|
10260
|
+
var shapeSnapshotFor = function (values) {
|
|
10261
|
+
var stacks = weightAxisStacks(values);
|
|
10262
|
+
var pairs = [
|
|
10263
|
+
[stacks.dialog, shared.WEIGHT_LADDER_STRONG],
|
|
10264
|
+
[stacks.ui, shared.WEIGHT_LADDER_STRONG],
|
|
10265
|
+
[stacks.code, shared.WEIGHT_BASE],
|
|
10266
|
+
];
|
|
10267
|
+
var snapshot = {};
|
|
10268
|
+
for (var index = 0; index < pairs.length; index += 1) {
|
|
10269
|
+
var stack = pairs[index][0];
|
|
10270
|
+
var base = pairs[index][1];
|
|
10271
|
+
var key = String(stack) + "\u0000" + base + "\u0000stepped";
|
|
10272
|
+
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
10273
|
+
snapshot[key] = weightProfileCache[key];
|
|
10274
|
+
} else {
|
|
10275
|
+
var stored = storedWeightShape(key);
|
|
10276
|
+
if (stored !== null) snapshot[key] = stored;
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
return snapshot;
|
|
10280
|
+
};
|
|
10281
|
+
|
|
10282
|
+
/**
|
|
10283
|
+
* Put a preset's remembered shapes back into the cache.
|
|
10284
|
+
* @param {object} shapes - what `shapeSnapshotFor` recorded.
|
|
10285
|
+
*/
|
|
10286
|
+
var hydrateShapes = function (shapes) {
|
|
10287
|
+
if (shapes === null || typeof shapes !== "object") return;
|
|
10288
|
+
for (var key in shapes) {
|
|
10289
|
+
if (!Object.prototype.hasOwnProperty.call(shapes, key)) continue;
|
|
10290
|
+
var shape = shapes[key];
|
|
10291
|
+
if (shape === null || typeof shape !== "object") continue;
|
|
10292
|
+
if (!isFinite(shape.min) || !isFinite(shape.max) || !isFinite(shape.step)) continue;
|
|
10293
|
+
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) continue;
|
|
10294
|
+
weightProfileCache[key] = { min: shape.min, max: shape.max, step: shape.step };
|
|
10295
|
+
}
|
|
10296
|
+
};
|
|
10297
|
+
|
|
9912
10298
|
var switchPreset = function (entry) {
|
|
9913
10299
|
if (!writable || entry.name === activeName) return;
|
|
10300
|
+
// Stamp the switch BEFORE anything is written: the sync that follows it would
|
|
10301
|
+
// otherwise start measuring the new stacks in the same task.
|
|
10302
|
+
presetSwitchAt = Date.now();
|
|
9914
10303
|
applyingRef.current = true;
|
|
9915
10304
|
try {
|
|
10305
|
+
hydrateShapes(entry.shapes);
|
|
9916
10306
|
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
9917
10307
|
var field = VALUE_FIELDS[index];
|
|
9918
10308
|
var value = Object.prototype.hasOwnProperty.call(entry.values, field)
|
|
@@ -10070,7 +10460,7 @@ function FontCard(props) {
|
|
|
10070
10460
|
var isCode = field === CODE_WEIGHT_FIELD;
|
|
10071
10461
|
var stack =
|
|
10072
10462
|
field === WEIGHT_DIALOG_FIELD ? dialogStack : isCode ? codeStack : uiStack;
|
|
10073
|
-
var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG);
|
|
10463
|
+
var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG, { peek: true });
|
|
10074
10464
|
return weightStepRange(profile, value === WEIGHT_UNSET ? 0 : value).value;
|
|
10075
10465
|
};
|
|
10076
10466
|
|
|
@@ -10141,6 +10531,23 @@ function FontCard(props) {
|
|
|
10141
10531
|
previewCodeStyle.lineHeight = "calc(20px + " + editing[CODE_LINE_HEIGHT_FIELD] + "px)";
|
|
10142
10532
|
}
|
|
10143
10533
|
|
|
10534
|
+
// A shape measured after this render changes the counts the rows above show, so
|
|
10535
|
+
// the card redraws once one lands (see `notifyShapes`). Declared last on purpose:
|
|
10536
|
+
// the hook order of everything before it stays exactly as it was.
|
|
10537
|
+
var [, setShapeRevision] = useState(0);
|
|
10538
|
+
useEffect(function () {
|
|
10539
|
+
var listener = function () {
|
|
10540
|
+
setShapeRevision(function (count) {
|
|
10541
|
+
return count + 1;
|
|
10542
|
+
});
|
|
10543
|
+
};
|
|
10544
|
+
shapeListeners.push(listener);
|
|
10545
|
+
return function () {
|
|
10546
|
+
var at = shapeListeners.indexOf(listener);
|
|
10547
|
+
if (at >= 0) shapeListeners.splice(at, 1);
|
|
10548
|
+
};
|
|
10549
|
+
}, []);
|
|
10550
|
+
|
|
10144
10551
|
return h(
|
|
10145
10552
|
"li",
|
|
10146
10553
|
{ className: "dfp-card" + (open ? " dfp-cardOpen" : "") },
|
|
@@ -13483,7 +13890,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
13483
13890
|
var dialogStack = weightAxisStacks(config).dialog;
|
|
13484
13891
|
var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
13485
13892
|
try {
|
|
13486
|
-
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG);
|
|
13893
|
+
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG, { peek: true });
|
|
13487
13894
|
} catch (profileError) {
|
|
13488
13895
|
// The nominal window stands in when the family cannot be measured.
|
|
13489
13896
|
}
|
|
@@ -13859,13 +14266,18 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
13859
14266
|
}
|
|
13860
14267
|
|
|
13861
14268
|
// A promotion changes the weight row's shape; the panel redraws itself when one
|
|
13862
|
-
// lands, and stops listening when it goes away.
|
|
14269
|
+
// lands, and stops listening when it goes away. A shape that only lands with the
|
|
14270
|
+
// idle measurement is the same story: the row was rendered with the provisional
|
|
14271
|
+
// counts, so it has to be redrawn once the real ones exist.
|
|
13863
14272
|
promotionListeners.push(render);
|
|
14273
|
+
shapeListeners.push(render);
|
|
13864
14274
|
|
|
13865
14275
|
return {
|
|
13866
14276
|
destroy: function () {
|
|
13867
14277
|
var at = promotionListeners.indexOf(render);
|
|
13868
14278
|
if (at >= 0) promotionListeners.splice(at, 1);
|
|
14279
|
+
var shapeAt = shapeListeners.indexOf(render);
|
|
14280
|
+
if (shapeAt >= 0) shapeListeners.splice(shapeAt, 1);
|
|
13869
14281
|
if (statusTimer !== null) globalThis.clearTimeout(statusTimer);
|
|
13870
14282
|
if (closeTimer !== null) globalThis.clearTimeout(closeTimer);
|
|
13871
14283
|
if (reflowTimer !== null) globalThis.clearTimeout(reflowTimer);
|
package/lib/shared.cjs
CHANGED
|
@@ -3467,6 +3467,70 @@ function balancedStackShape(entries, base, low, high, spread) {
|
|
|
3467
3467
|
return best;
|
|
3468
3468
|
}
|
|
3469
3469
|
|
|
3470
|
+
/**
|
|
3471
|
+
* The FIRST weight of an arithmetic grid whose rendering matches a signature.
|
|
3472
|
+
*
|
|
3473
|
+
* The probe grid is monotone: below a family's lightest face every weight renders
|
|
3474
|
+
* the same (the lightest face), above its heaviest face every weight renders the
|
|
3475
|
+
* same (the heaviest face), and in between it walks the faces. So "renders like the
|
|
3476
|
+
* ceiling" is a predicate that is false, then true, and its boundary is worth
|
|
3477
|
+
* `log2` samples instead of one render per grid point — which is what the two
|
|
3478
|
+
* horizon scans used to cost (eighty-one renders per family, issue #3).
|
|
3479
|
+
* @param {(weight: number) => string|number} sample - renders a probe at one weight.
|
|
3480
|
+
* @param {string|number} signature - the signature the boundary shares.
|
|
3481
|
+
* @param {number} low - first weight of the grid.
|
|
3482
|
+
* @param {number} high - last weight of the grid.
|
|
3483
|
+
* @param {number} step - spacing of the grid.
|
|
3484
|
+
* @param {number|null} fallback - returned when no grid weight matches.
|
|
3485
|
+
* @returns {number|null} the weight, or the fallback.
|
|
3486
|
+
*/
|
|
3487
|
+
function firstGridWeight(sample, signature, low, high, step, fallback) {
|
|
3488
|
+
var count = Math.floor((high - low) / step) + 1;
|
|
3489
|
+
if (count < 1 || !(step > 0)) return fallback;
|
|
3490
|
+
var at = function (index) {
|
|
3491
|
+
return low + index * step;
|
|
3492
|
+
};
|
|
3493
|
+
if (sample(at(count - 1)) !== signature) return fallback;
|
|
3494
|
+
var first = 0;
|
|
3495
|
+
var last = count - 1;
|
|
3496
|
+
while (first < last) {
|
|
3497
|
+
var middle = (first + last) >> 1;
|
|
3498
|
+
if (sample(at(middle)) === signature) last = middle;
|
|
3499
|
+
else first = middle + 1;
|
|
3500
|
+
}
|
|
3501
|
+
return at(first);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
/**
|
|
3505
|
+
* The LAST weight of an arithmetic grid whose rendering matches a signature.
|
|
3506
|
+
*
|
|
3507
|
+
* The mirror of `firstGridWeight`: "renders like the floor" is true, then false,
|
|
3508
|
+
* so the boundary is found from the other end.
|
|
3509
|
+
* @param {(weight: number) => string|number} sample - renders a probe at one weight.
|
|
3510
|
+
* @param {string|number} signature - the signature the boundary shares.
|
|
3511
|
+
* @param {number} low - first weight of the grid.
|
|
3512
|
+
* @param {number} high - last weight of the grid.
|
|
3513
|
+
* @param {number} step - spacing of the grid.
|
|
3514
|
+
* @param {number|null} fallback - returned when no grid weight matches.
|
|
3515
|
+
* @returns {number|null} the weight, or the fallback.
|
|
3516
|
+
*/
|
|
3517
|
+
function lastGridWeight(sample, signature, low, high, step, fallback) {
|
|
3518
|
+
var count = Math.floor((high - low) / step) + 1;
|
|
3519
|
+
if (count < 1 || !(step > 0)) return fallback;
|
|
3520
|
+
var at = function (index) {
|
|
3521
|
+
return low + index * step;
|
|
3522
|
+
};
|
|
3523
|
+
if (sample(at(0)) !== signature) return fallback;
|
|
3524
|
+
var first = 0;
|
|
3525
|
+
var last = count - 1;
|
|
3526
|
+
while (first < last) {
|
|
3527
|
+
var middle = (first + last + 1) >> 1;
|
|
3528
|
+
if (sample(at(middle)) === signature) first = middle;
|
|
3529
|
+
else last = middle - 1;
|
|
3530
|
+
}
|
|
3531
|
+
return at(first);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3470
3534
|
/**
|
|
3471
3535
|
* One slider shape for a stack whose text can land on more than one family.
|
|
3472
3536
|
*
|
|
@@ -3628,30 +3692,38 @@ function weightProfileFrom(sample, options) {
|
|
|
3628
3692
|
var top = typeof settings.top === "number" ? settings.top : WEIGHT_LADDER_STRONG;
|
|
3629
3693
|
var topSignature = sample(WEIGHT_PROBE_HIGH);
|
|
3630
3694
|
var bottomSignature = sample(WEIGHT_PROBE_LOW);
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3695
|
+
// The two horizons used to be found by walking the whole probe grid from one end
|
|
3696
|
+
// — eighty-one canvas renders per family, and on a font collection of a few
|
|
3697
|
+
// hundred megabytes each render is the browser rasterizing a new weight (issue
|
|
3698
|
+
// #3: seconds of blocked main thread). The grid is monotone in weight: everything
|
|
3699
|
+
// from a family's heaviest face UP renders the same, so the horizon is a
|
|
3700
|
+
// boundary, and a bisection finds it in seven samples instead of eighty-one.
|
|
3701
|
+
var heaviest = firstGridWeight(
|
|
3702
|
+
sample,
|
|
3703
|
+
topSignature,
|
|
3704
|
+
WEIGHT_PROBE_LOW,
|
|
3705
|
+
WEIGHT_PROBE_HIGH,
|
|
3706
|
+
WEIGHT_PROBE_STEP,
|
|
3707
|
+
WEIGHT_PROBE_HIGH
|
|
3708
|
+
);
|
|
3709
|
+
var lightest = lastGridWeight(
|
|
3710
|
+
sample,
|
|
3711
|
+
bottomSignature,
|
|
3712
|
+
WEIGHT_PROBE_LOW,
|
|
3713
|
+
WEIGHT_PROBE_HIGH,
|
|
3714
|
+
WEIGHT_PROBE_STEP,
|
|
3715
|
+
WEIGHT_PROBE_LOW
|
|
3716
|
+
);
|
|
3645
3717
|
// How far the offset can go before NO layer that must move can move any more:
|
|
3646
3718
|
// the widest horizon among those bases, since a layer that could still change
|
|
3647
3719
|
// must not be cut off by one that already sits at its heaviest face.
|
|
3648
3720
|
var headroom = Math.max(0, heaviest - WEIGHT_BASE);
|
|
3649
3721
|
if (top !== WEIGHT_BASE) {
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3722
|
+
// Same boundary, same bisection: the first weight at or above the heavy base
|
|
3723
|
+
// that already renders at the ceiling.
|
|
3724
|
+
var reached = firstGridWeight(sample, topSignature, top, WEIGHT_PROBE_HIGH, WEIGHT_PROBE_STEP, null);
|
|
3725
|
+
if (reached !== null) {
|
|
3726
|
+
headroom = Math.max(headroom, reached - top);
|
|
3655
3727
|
}
|
|
3656
3728
|
}
|
|
3657
3729
|
var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
|
|
@@ -4919,6 +4991,40 @@ function catalogRefreshDue(cache, now, retryMs) {
|
|
|
4919
4991
|
return now - (cache.at || 0) >= retryMs;
|
|
4920
4992
|
}
|
|
4921
4993
|
|
|
4994
|
+
/**
|
|
4995
|
+
* The rows one picker group actually renders.
|
|
4996
|
+
*
|
|
4997
|
+
* A machine with a huge font collection — or a Super TTC, which exposes every
|
|
4998
|
+
* weight and variant as its own family — can enumerate hundreds of names, and
|
|
4999
|
+
* building a row for each one is a visible stall when the list opens (issue #3:
|
|
5000
|
+
* the reporter's second symptom). The list therefore renders a screenful unless
|
|
5001
|
+
* the reader is searching, and a family that is already selected is always
|
|
5002
|
+
* rendered so its check mark never falls outside the window.
|
|
5003
|
+
* @param {string[]} families - the group's families, in display order.
|
|
5004
|
+
* @param {string} query - the search text (empty means "browse the whole list").
|
|
5005
|
+
* @param {Object<string, boolean>} selectedLower - lower-cased names already picked.
|
|
5006
|
+
* @param {number} [limit] - how many rows a browse renders.
|
|
5007
|
+
* @returns {string[]} the families to render.
|
|
5008
|
+
*/
|
|
5009
|
+
function pickerRows(families, query, selectedLower, limit) {
|
|
5010
|
+
var list = Array.isArray(families) ? families : [];
|
|
5011
|
+
var searching = typeof query === "string" && query.trim() !== "";
|
|
5012
|
+
if (searching) return list.slice();
|
|
5013
|
+
var cap = typeof limit === "number" && limit > 0 ? Math.floor(limit) : 60;
|
|
5014
|
+
var picked = selectedLower !== null && typeof selectedLower === "object" ? selectedLower : {};
|
|
5015
|
+
var rows = [];
|
|
5016
|
+
var shown = 0;
|
|
5017
|
+
for (var index = 0; index < list.length; index += 1) {
|
|
5018
|
+
var name = list[index];
|
|
5019
|
+
var isSelected = picked[String(name).toLowerCase()] === true;
|
|
5020
|
+
if (isSelected || shown < cap) {
|
|
5021
|
+
rows.push(name);
|
|
5022
|
+
shown += 1;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
return rows;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
4922
5028
|
var shared = {
|
|
4923
5029
|
NAMESPACE: NAMESPACE,
|
|
4924
5030
|
SANS_FIELD: SANS_FIELD,
|
|
@@ -5066,6 +5172,7 @@ var shared = {
|
|
|
5066
5172
|
PRESETS: PRESETS,
|
|
5067
5173
|
PRESET_GROUPS: PRESET_GROUPS,
|
|
5068
5174
|
MAX_LOCAL_FONTS: MAX_LOCAL_FONTS,
|
|
5175
|
+
pickerRows: pickerRows,
|
|
5069
5176
|
pickerGroups: pickerGroups,
|
|
5070
5177
|
catalogRefreshDue: catalogRefreshDue,
|
|
5071
5178
|
MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"icon": "icon.svg",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"docs/card-shaping.jpg"
|
|
26
26
|
],
|
|
27
27
|
"engines": {
|
|
28
|
-
"dsh": ">=0.
|
|
28
|
+
"dsh": ">=0.2.0-rc.1 <0.2.2-rc.2"
|
|
29
29
|
},
|
|
30
30
|
"dsh": {
|
|
31
31
|
"bundle": {
|
|
@@ -36,21 +36,14 @@
|
|
|
36
36
|
},
|
|
37
37
|
"compatibility": {
|
|
38
38
|
"dshReleases": {
|
|
39
|
-
"0.1.7-alpha.1": "compatible",
|
|
40
|
-
"0.1.7-alpha.2": "compatible",
|
|
41
|
-
"0.1.7-rc.1": "compatible",
|
|
42
|
-
"0.1.7-rc.2": "compatible",
|
|
43
39
|
"0.2.0-rc.1": "compatible",
|
|
44
40
|
"0.2.0-rc.2": "compatible",
|
|
45
|
-
"0.2.1-alpha.1": "compatible",
|
|
46
|
-
"0.2.1-alpha.2": "compatible",
|
|
47
|
-
"0.2.1-alpha.3": "compatible",
|
|
48
|
-
"0.2.1-alpha.4": "compatible",
|
|
49
|
-
"0.2.1-alpha.5": "compatible",
|
|
50
41
|
"0.2.1-rc.1": "compatible",
|
|
51
42
|
"0.2.1-rc.2": "compatible",
|
|
43
|
+
"0.2.1-rc.3": "compatible",
|
|
52
44
|
"0.2.1-rc.4": "compatible",
|
|
53
|
-
"0.2.
|
|
45
|
+
"0.2.1-rc.5": "compatible",
|
|
46
|
+
"0.2.2-rc.1": "compatible"
|
|
54
47
|
}
|
|
55
48
|
}
|
|
56
49
|
},
|