dsh-fonttune 0.7.5 → 0.7.7
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 +14 -0
- package/CHANGELOG.md +14 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +336 -34
- package/lib/index.js +60 -13
- package/lib/shared.cjs +91 -19
- package/package.json +5 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.7.7] - 2026-11-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **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.
|
|
13
|
+
|
|
14
|
+
## [0.7.6] - 2026-11-04
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **The plugin did not actually load on some hosts**: the Font tune settings card and the first-frame type stylesheet were missing while the conversation still looked completely normal, so it was easy to miss; the plugin now waits for its dependency to finish loading before it registers, and no longer starts half-loaded.
|
|
18
|
+
|
|
5
19
|
## [0.7.5] - 2026-11-01
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.7.7] - 2026-11-05
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **打开字体设置时的卡顿**:字体档位的测量改为分步进行,并避开设置页面的打开与刷新;同一套字体只需测量一次,之后打开设置不再重新计算。
|
|
9
|
+
- **部分超大字体集合下的长时间无响应**:字体文件特别大时,测量会在超时后改用粗略档位,不再长时间占住界面。
|
|
10
|
+
|
|
11
|
+
### 变更
|
|
12
|
+
- **兼容 DSH 0.2.2-rc.1**:README 的兼容表与插件清单同步补上 0.2.1-rc.3、0.2.1-rc.5 与 0.2.2-rc.1。
|
|
13
|
+
|
|
14
|
+
## [0.7.6] - 2026-11-04
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
- **部分环境下插件启动后没有真正加载**:字体增强的设置卡片与首帧字体样式会缺失,而对话外观看起来一切正常,所以很难发现;现在启动时会等依赖就绪再注册,不再出现这种「看着正常、其实没加载」的状态。
|
|
18
|
+
|
|
5
19
|
## [0.7.5] - 2026-11-01
|
|
6
20
|
|
|
7
21
|
### 修复
|
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.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, 0.2.1-rc.4, 0.2.2-alpha.2, 0.2.1-rc.3, 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.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、0.2.1-rc.4、0.2.2-alpha.2、0.2.1-rc.3、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);
|
|
@@ -6483,6 +6555,18 @@ var weightProbeCanvas = null;
|
|
|
6483
6555
|
/** Cache of measured slider shapes, keyed by the resolved family stack. */
|
|
6484
6556
|
var weightProfileCache = {};
|
|
6485
6557
|
|
|
6558
|
+
/**
|
|
6559
|
+
* How slow one canvas render may be before the font is treated as too heavy to
|
|
6560
|
+
* sweep in detail (a few-hundred-megabyte collection answers in tens of ms).
|
|
6561
|
+
*/
|
|
6562
|
+
var SLOW_SAMPLE_MS = 40;
|
|
6563
|
+
|
|
6564
|
+
/** How long after load the code axis is measured (the other two go first). */
|
|
6565
|
+
var WARM_CODE_DELAY_MS = 2500;
|
|
6566
|
+
|
|
6567
|
+
/** Thrown by the guarded sampler to abandon the fine sweep on a slow font. */
|
|
6568
|
+
var WEIGHT_TOO_SLOW = { tooSlow: true };
|
|
6569
|
+
|
|
6486
6570
|
/**
|
|
6487
6571
|
* The families a stack can actually render text with.
|
|
6488
6572
|
*
|
|
@@ -6561,6 +6645,15 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6561
6645
|
return familyProfileCache[key];
|
|
6562
6646
|
}
|
|
6563
6647
|
var record = null;
|
|
6648
|
+
// A font collection of a few hundred megabytes makes EVERY render expensive (the
|
|
6649
|
+
// browser re-parses the collection for a weight it has not drawn yet), and the
|
|
6650
|
+
// sweep asks for a dozen of them in a row — measured at 20-50 ms each on such a
|
|
6651
|
+
// file, i.e. a second of blocked main thread per family (issue #3). One render
|
|
6652
|
+
// that costs more than SLOW_SAMPLE_MS is enough evidence: abandon the fine sweep
|
|
6653
|
+
// and answer with a coarse one, so the worst case stays bounded instead of
|
|
6654
|
+
// growing with the file size.
|
|
6655
|
+
var slow = false;
|
|
6656
|
+
var guarded = null;
|
|
6564
6657
|
try {
|
|
6565
6658
|
var target = '"' + (alias === "" ? family : alias) + '"';
|
|
6566
6659
|
var seen = {};
|
|
@@ -6570,10 +6663,16 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6570
6663
|
}
|
|
6571
6664
|
return seen[weight];
|
|
6572
6665
|
};
|
|
6666
|
+
guarded = function (weight) {
|
|
6667
|
+
var started = Date.now();
|
|
6668
|
+
var signature = sample(weight);
|
|
6669
|
+
if (Date.now() - started >= SLOW_SAMPLE_MS) throw WEIGHT_TOO_SLOW;
|
|
6670
|
+
return signature;
|
|
6671
|
+
};
|
|
6573
6672
|
var shape =
|
|
6574
6673
|
alias === ""
|
|
6575
|
-
? weightProfileFrom(
|
|
6576
|
-
: weightProfileFrom(
|
|
6674
|
+
? weightProfileFrom(guarded, { top: base })
|
|
6675
|
+
: weightProfileFrom(guarded, { top: base, candidates: shared.CONTINUOUS_CANDIDATES });
|
|
6577
6676
|
record = { min: shape.min, max: shape.max, step: shape.step, inkOf: null };
|
|
6578
6677
|
if (text !== null) {
|
|
6579
6678
|
var inks = {};
|
|
@@ -6585,7 +6684,31 @@ function familyWeightProfile(family, base, alias, inkText) {
|
|
|
6585
6684
|
};
|
|
6586
6685
|
}
|
|
6587
6686
|
} catch (error) {
|
|
6588
|
-
|
|
6687
|
+
if (error !== WEIGHT_TOO_SLOW) {
|
|
6688
|
+
record = null;
|
|
6689
|
+
} else {
|
|
6690
|
+
// Too slow for the fine sweep: nine renders on the hundred grid describe the
|
|
6691
|
+
// family coarsely, which is still a control that moves in visible steps. No
|
|
6692
|
+
// ink comparison — that would be a second sweep of the same price.
|
|
6693
|
+
slow = true;
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
if (slow) {
|
|
6697
|
+
try {
|
|
6698
|
+
var coarseSeen = {};
|
|
6699
|
+
var coarse = weightProfileFrom(
|
|
6700
|
+
function (weight) {
|
|
6701
|
+
if (!Object.prototype.hasOwnProperty.call(coarseSeen, weight)) {
|
|
6702
|
+
coarseSeen[weight] = sampleWeightSignature(target, Math.round(weight / 100) * 100);
|
|
6703
|
+
}
|
|
6704
|
+
return coarseSeen[weight];
|
|
6705
|
+
},
|
|
6706
|
+
{ top: base, candidates: [100] }
|
|
6707
|
+
);
|
|
6708
|
+
record = { min: coarse.min, max: coarse.max, step: coarse.step, inkOf: null };
|
|
6709
|
+
} catch (error) {
|
|
6710
|
+
record = null;
|
|
6711
|
+
}
|
|
6589
6712
|
}
|
|
6590
6713
|
familyProfileCache[key] = record;
|
|
6591
6714
|
return record;
|
|
@@ -6965,6 +7088,123 @@ function inkOfSignature(signature) {
|
|
|
6965
7088
|
return isFinite(ink) ? ink : 0;
|
|
6966
7089
|
}
|
|
6967
7090
|
|
|
7091
|
+
/**
|
|
7092
|
+
* Where a measured slider shape survives a page reload.
|
|
7093
|
+
*
|
|
7094
|
+
* Measuring one axis is around eighty canvas renders per family, and the cost per
|
|
7095
|
+
* render is the browser rasterizing a weight it has never drawn (measured: ~3 ms
|
|
7096
|
+
* for an 18 MB family, ~6 ms for a 35 MB one, and seconds per render on a font
|
|
7097
|
+
* collection of a few hundred megabytes — issue #3). The answer only changes when
|
|
7098
|
+
* the reader changes the fonts, so it is kept: the first visit pays, every later
|
|
7099
|
+
* one opens instantly. The TTL bounds how long a font file that was replaced
|
|
7100
|
+
* under the same family name may go unnoticed.
|
|
7101
|
+
*/
|
|
7102
|
+
var SHAPE_STORE_KEY = "dsh-fonttune:weight-shapes:v1";
|
|
7103
|
+
var SHAPE_STORE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7104
|
+
var SHAPE_STORE_LIMIT = 32;
|
|
7105
|
+
|
|
7106
|
+
/**
|
|
7107
|
+
* What a slider shows while its measurement has not landed yet.
|
|
7108
|
+
*
|
|
7109
|
+
* The nominal offset window, unit one: the same answer the code already gives when
|
|
7110
|
+
* a family cannot be measured at all, so nothing new appears in the document and a
|
|
7111
|
+
* stored offset stays reachable. Rendering is what used to MEASURE (a cold cache at
|
|
7112
|
+
* open = eighty canvas renders on the main thread), which is the "opening the
|
|
7113
|
+
* settings freezes" of issue #3; the real counts replace these the moment the idle
|
|
7114
|
+
* measurement finishes and the card redraws.
|
|
7115
|
+
*/
|
|
7116
|
+
var PENDING_WEIGHT_SHAPE = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
7117
|
+
|
|
7118
|
+
/** The decoded store, or an empty one when storage is unavailable. */
|
|
7119
|
+
var shapeStore = null;
|
|
7120
|
+
|
|
7121
|
+
/**
|
|
7122
|
+
* The persistent shape store, read once.
|
|
7123
|
+
* @returns {Object} key -> `{min, max, step, at}`.
|
|
7124
|
+
*/
|
|
7125
|
+
function readShapeStore() {
|
|
7126
|
+
if (shapeStore !== null) return shapeStore;
|
|
7127
|
+
shapeStore = {};
|
|
7128
|
+
try {
|
|
7129
|
+
var raw = globalThis.localStorage.getItem(SHAPE_STORE_KEY);
|
|
7130
|
+
var parsed = raw === null ? null : JSON.parse(raw);
|
|
7131
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
7132
|
+
for (var key in parsed) {
|
|
7133
|
+
if (!Object.prototype.hasOwnProperty.call(parsed, key)) continue;
|
|
7134
|
+
var entry = parsed[key];
|
|
7135
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
7136
|
+
if (!isFinite(entry.min) || !isFinite(entry.max) || !isFinite(entry.step)) continue;
|
|
7137
|
+
if (!(entry.step > 0) || !isFinite(entry.at)) continue;
|
|
7138
|
+
shapeStore[key] = { min: entry.min, max: entry.max, step: entry.step, at: entry.at };
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
} catch (error) {
|
|
7142
|
+
// Private mode, a disabled store, a corrupted value: measuring still works.
|
|
7143
|
+
}
|
|
7144
|
+
return shapeStore;
|
|
7145
|
+
}
|
|
7146
|
+
|
|
7147
|
+
/**
|
|
7148
|
+
* The stored shape for one cache key, when it is still fresh.
|
|
7149
|
+
* @param {string} key - the same key the memory cache uses.
|
|
7150
|
+
* @returns {{min: number, max: number, step: number}|null} the shape, or null.
|
|
7151
|
+
*/
|
|
7152
|
+
function storedWeightShape(key) {
|
|
7153
|
+
var store = readShapeStore();
|
|
7154
|
+
if (!Object.prototype.hasOwnProperty.call(store, key)) return null;
|
|
7155
|
+
var entry = store[key];
|
|
7156
|
+
if (Date.now() - entry.at > SHAPE_STORE_TTL_MS) return null;
|
|
7157
|
+
return { min: entry.min, max: entry.max, step: entry.step };
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
/**
|
|
7161
|
+
* Keep one measured shape for the next page load.
|
|
7162
|
+
*
|
|
7163
|
+
* Writes the whole (small) map back, dropping the oldest entries past the limit:
|
|
7164
|
+
* a reader who tries many browsing fonts must not grow this without bound.
|
|
7165
|
+
* @param {string} key - the same key the memory cache uses.
|
|
7166
|
+
* @param {{min: number, max: number, step: number}} profile - what was measured.
|
|
7167
|
+
*/
|
|
7168
|
+
function rememberWeightShape(key, profile) {
|
|
7169
|
+
var store = readShapeStore();
|
|
7170
|
+
store[key] = { min: profile.min, max: profile.max, step: profile.step, at: Date.now() };
|
|
7171
|
+
var keys = Object.keys(store);
|
|
7172
|
+
if (keys.length > SHAPE_STORE_LIMIT) {
|
|
7173
|
+
keys.sort(function (left, right) {
|
|
7174
|
+
return store[left].at - store[right].at;
|
|
7175
|
+
});
|
|
7176
|
+
for (var index = 0; index < keys.length - SHAPE_STORE_LIMIT; index += 1) {
|
|
7177
|
+
delete store[keys[index]];
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
try {
|
|
7181
|
+
globalThis.localStorage.setItem(SHAPE_STORE_KEY, JSON.stringify(store));
|
|
7182
|
+
} catch (error) {
|
|
7183
|
+
// A full or disabled store only costs the next load the measurement again.
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
/**
|
|
7188
|
+
* What to redraw when a measurement lands after the first paint.
|
|
7189
|
+
*
|
|
7190
|
+
* The card renders the count of positions, which is a property of the font: a
|
|
7191
|
+
* shape that arrives later (the idle warm-up, a slower family) has to reach the
|
|
7192
|
+
* mounted cards or they would keep showing the provisional counts.
|
|
7193
|
+
*/
|
|
7194
|
+
var shapeListeners = [];
|
|
7195
|
+
|
|
7196
|
+
/** Ask every mounted card to re-read the shapes. */
|
|
7197
|
+
function notifyShapes() {
|
|
7198
|
+
var pending = shapeListeners.slice();
|
|
7199
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
7200
|
+
try {
|
|
7201
|
+
pending[index]();
|
|
7202
|
+
} catch (error) {
|
|
7203
|
+
// A listener must never break the measurement that notified it.
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
|
|
6968
7208
|
/**
|
|
6969
7209
|
* The slider shape to use for one family: its range and its notch.
|
|
6970
7210
|
*
|
|
@@ -6972,15 +7212,19 @@ function inkOfSignature(signature) {
|
|
|
6972
7212
|
* range is measured rather than fixed (`weightProfileFrom`), which also makes
|
|
6973
7213
|
* every notch a change — for every layer of the ladder, so the measurement is
|
|
6974
7214
|
* 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.
|
|
7215
|
+
* family and base and cached, in memory and across page loads; any failure (no
|
|
7216
|
+
* canvas, no 2D context) falls back to the nominal window with single units.
|
|
6977
7217
|
* @param {string} stack - resolved `font-family` value, "" for the page default.
|
|
6978
7218
|
* @param {number} [top] - the heaviest base the offset is added to; the ladder's
|
|
6979
7219
|
* own top for the conversation and interface axes, the axis's base for code.
|
|
7220
|
+
* @param {{peek?: boolean}} [options] - `peek: true` never measures: it answers
|
|
7221
|
+
* with the stored shape when there is one and the provisional shape otherwise,
|
|
7222
|
+
* so a render can never block on a canvas sweep (see `PENDING_WEIGHT_SHAPE`).
|
|
6980
7223
|
* @returns {{min: number, max: number, step: number}} the slider shape.
|
|
6981
7224
|
*/
|
|
6982
|
-
function weightProfileFor(stack, top) {
|
|
7225
|
+
function weightProfileFor(stack, top, options) {
|
|
6983
7226
|
var base = typeof top === "number" ? top : shared.WEIGHT_LADDER_TOP;
|
|
7227
|
+
var peek = options !== null && typeof options === "object" && options.peek === true;
|
|
6984
7228
|
var resolved = String(stack) === "" ? liveFamily() : String(stack);
|
|
6985
7229
|
// `hasOwnProperty` rather than a truthiness test: the alias map is keyed by family
|
|
6986
7230
|
// name, and a family called "constructor" must not read as promoted.
|
|
@@ -6994,6 +7238,15 @@ function weightProfileFor(stack, top) {
|
|
|
6994
7238
|
if (Object.prototype.hasOwnProperty.call(weightProfileCache, key)) {
|
|
6995
7239
|
return weightProfileCache[key];
|
|
6996
7240
|
}
|
|
7241
|
+
// Measured on a previous visit: use it and skip the sweep entirely.
|
|
7242
|
+
var stored = storedWeightShape(key);
|
|
7243
|
+
if (stored !== null) {
|
|
7244
|
+
weightProfileCache[key] = stored;
|
|
7245
|
+
return stored;
|
|
7246
|
+
}
|
|
7247
|
+
// A render must never wait for eighty canvas renders: answer with the
|
|
7248
|
+
// provisional shape now, and let the idle warm-up measure and redraw.
|
|
7249
|
+
if (peek) return PENDING_WEIGHT_SHAPE;
|
|
6997
7250
|
var profile = promoted
|
|
6998
7251
|
? shared.continuousWeightProfile()
|
|
6999
7252
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
@@ -7034,6 +7287,20 @@ function weightProfileFor(stack, top) {
|
|
|
7034
7287
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
7035
7288
|
}
|
|
7036
7289
|
weightProfileCache[key] = profile;
|
|
7290
|
+
// A measured answer is worth a reload's freedom from the sweep, and the count it
|
|
7291
|
+
// changes has to reach whatever is already on screen with the provisional one.
|
|
7292
|
+
// Only when it actually differs: a family nothing could measure (no canvas) lands
|
|
7293
|
+
// on exactly the provisional window, and redrawing for that would be noise.
|
|
7294
|
+
rememberWeightShape(key, profile);
|
|
7295
|
+
// Nothing distinguishable measured (no canvas, a family the page cannot draw)
|
|
7296
|
+
// leaves the degenerate shape, which is not worth a redraw.
|
|
7297
|
+
var changed =
|
|
7298
|
+
profile.min !== 0 || profile.max !== 0
|
|
7299
|
+
? profile.min !== PENDING_WEIGHT_SHAPE.min ||
|
|
7300
|
+
profile.max !== PENDING_WEIGHT_SHAPE.max ||
|
|
7301
|
+
profile.step !== PENDING_WEIGHT_SHAPE.step
|
|
7302
|
+
: false;
|
|
7303
|
+
if (changed) notifyShapes();
|
|
7037
7304
|
return profile;
|
|
7038
7305
|
}
|
|
7039
7306
|
|
|
@@ -7086,6 +7353,9 @@ function deferWeightWarm(job) {
|
|
|
7086
7353
|
* @param {object} editing - the value set the axes read.
|
|
7087
7354
|
*/
|
|
7088
7355
|
var warmSignature = null;
|
|
7356
|
+
|
|
7357
|
+
/** The code stack the deferred slice was last scheduled for (scheduled once). */
|
|
7358
|
+
var codeWarmSignature = null;
|
|
7089
7359
|
var scheduleWeightWarm = function (editing) {
|
|
7090
7360
|
var stacks = weightAxisStacks(editing);
|
|
7091
7361
|
var signature = stacks.dialog + "\u0000" + stacks.ui + "\u0000" + stacks.code;
|
|
@@ -7102,11 +7372,6 @@ var scheduleWeightWarm = function (editing) {
|
|
|
7102
7372
|
weightProfileFor(stacks.ui, shared.WEIGHT_LADDER_STRONG);
|
|
7103
7373
|
probeLog("weight-warm", { axis: "ui", stack: stacks.ui });
|
|
7104
7374
|
},
|
|
7105
|
-
function () {
|
|
7106
|
-
probeContinuity(stacks.code);
|
|
7107
|
-
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
7108
|
-
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
7109
|
-
},
|
|
7110
7375
|
];
|
|
7111
7376
|
var runNext = function () {
|
|
7112
7377
|
var job = jobs.shift();
|
|
@@ -7115,6 +7380,20 @@ var scheduleWeightWarm = function (editing) {
|
|
|
7115
7380
|
deferWeightWarm(runNext);
|
|
7116
7381
|
};
|
|
7117
7382
|
deferWeightWarm(runNext);
|
|
7383
|
+
// The code axis is measured LAST and later still: its family is usually a
|
|
7384
|
+
// monospace the reader rarely touches, and on a slow machine the two axes above
|
|
7385
|
+
// already keep the main thread busy. Deferring it keeps the first seconds after
|
|
7386
|
+
// load (when a reader is likeliest to open the settings) free of its sweep.
|
|
7387
|
+
var codeSignature = stacks.code;
|
|
7388
|
+
if (codeSignature === codeWarmSignature) return;
|
|
7389
|
+
codeWarmSignature = codeSignature;
|
|
7390
|
+
globalThis.setTimeout(function () {
|
|
7391
|
+
deferWeightWarm(function () {
|
|
7392
|
+
probeContinuity(stacks.code);
|
|
7393
|
+
weightProfileFor(stacks.code, shared.WEIGHT_BASE);
|
|
7394
|
+
probeLog("weight-warm", { axis: "code", stack: stacks.code });
|
|
7395
|
+
});
|
|
7396
|
+
}, WARM_CODE_DELAY_MS);
|
|
7118
7397
|
};
|
|
7119
7398
|
|
|
7120
7399
|
/**
|
|
@@ -9453,7 +9732,8 @@ function FontCard(props) {
|
|
|
9453
9732
|
var weightField = function (props) {
|
|
9454
9733
|
var profile = weightProfileFor(
|
|
9455
9734
|
props.stack === undefined ? "" : props.stack,
|
|
9456
|
-
props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top
|
|
9735
|
+
props.top === undefined ? shared.WEIGHT_LADDER_STRONG : props.top,
|
|
9736
|
+
{ peek: true }
|
|
9457
9737
|
);
|
|
9458
9738
|
var stored = props.value === WEIGHT_UNSET ? NEUTRAL_WEIGHT : props.value;
|
|
9459
9739
|
// A stored offset outside the measured range (the font changed since it was
|
|
@@ -10070,7 +10350,7 @@ function FontCard(props) {
|
|
|
10070
10350
|
var isCode = field === CODE_WEIGHT_FIELD;
|
|
10071
10351
|
var stack =
|
|
10072
10352
|
field === WEIGHT_DIALOG_FIELD ? dialogStack : isCode ? codeStack : uiStack;
|
|
10073
|
-
var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG);
|
|
10353
|
+
var profile = weightProfileFor(stack, isCode ? shared.WEIGHT_BASE : shared.WEIGHT_LADDER_STRONG, { peek: true });
|
|
10074
10354
|
return weightStepRange(profile, value === WEIGHT_UNSET ? 0 : value).value;
|
|
10075
10355
|
};
|
|
10076
10356
|
|
|
@@ -10141,6 +10421,23 @@ function FontCard(props) {
|
|
|
10141
10421
|
previewCodeStyle.lineHeight = "calc(20px + " + editing[CODE_LINE_HEIGHT_FIELD] + "px)";
|
|
10142
10422
|
}
|
|
10143
10423
|
|
|
10424
|
+
// A shape measured after this render changes the counts the rows above show, so
|
|
10425
|
+
// the card redraws once one lands (see `notifyShapes`). Declared last on purpose:
|
|
10426
|
+
// the hook order of everything before it stays exactly as it was.
|
|
10427
|
+
var [, setShapeRevision] = useState(0);
|
|
10428
|
+
useEffect(function () {
|
|
10429
|
+
var listener = function () {
|
|
10430
|
+
setShapeRevision(function (count) {
|
|
10431
|
+
return count + 1;
|
|
10432
|
+
});
|
|
10433
|
+
};
|
|
10434
|
+
shapeListeners.push(listener);
|
|
10435
|
+
return function () {
|
|
10436
|
+
var at = shapeListeners.indexOf(listener);
|
|
10437
|
+
if (at >= 0) shapeListeners.splice(at, 1);
|
|
10438
|
+
};
|
|
10439
|
+
}, []);
|
|
10440
|
+
|
|
10144
10441
|
return h(
|
|
10145
10442
|
"li",
|
|
10146
10443
|
{ className: "dfp-card" + (open ? " dfp-cardOpen" : "") },
|
|
@@ -13483,7 +13780,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
13483
13780
|
var dialogStack = weightAxisStacks(config).dialog;
|
|
13484
13781
|
var profile = { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
13485
13782
|
try {
|
|
13486
|
-
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG);
|
|
13783
|
+
profile = weightProfileFor(dialogStack, shared.WEIGHT_LADDER_STRONG, { peek: true });
|
|
13487
13784
|
} catch (profileError) {
|
|
13488
13785
|
// The nominal window stands in when the family cannot be measured.
|
|
13489
13786
|
}
|
|
@@ -13859,13 +14156,18 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
13859
14156
|
}
|
|
13860
14157
|
|
|
13861
14158
|
// A promotion changes the weight row's shape; the panel redraws itself when one
|
|
13862
|
-
// lands, and stops listening when it goes away.
|
|
14159
|
+
// lands, and stops listening when it goes away. A shape that only lands with the
|
|
14160
|
+
// idle measurement is the same story: the row was rendered with the provisional
|
|
14161
|
+
// counts, so it has to be redrawn once the real ones exist.
|
|
13863
14162
|
promotionListeners.push(render);
|
|
14163
|
+
shapeListeners.push(render);
|
|
13864
14164
|
|
|
13865
14165
|
return {
|
|
13866
14166
|
destroy: function () {
|
|
13867
14167
|
var at = promotionListeners.indexOf(render);
|
|
13868
14168
|
if (at >= 0) promotionListeners.splice(at, 1);
|
|
14169
|
+
var shapeAt = shapeListeners.indexOf(render);
|
|
14170
|
+
if (shapeAt >= 0) shapeListeners.splice(shapeAt, 1);
|
|
13869
14171
|
if (statusTimer !== null) globalThis.clearTimeout(statusTimer);
|
|
13870
14172
|
if (closeTimer !== null) globalThis.clearTimeout(closeTimer);
|
|
13871
14173
|
if (reflowTimer !== null) globalThis.clearTimeout(reflowTimer);
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { createRequire } from "node:module";
|
|
24
24
|
import { dirname, sep } from "node:path";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
26
|
// Default import, not named: `shared.cjs` is a CommonJS module, and Node's
|
|
27
27
|
// CJS-to-ESM interop only guarantees `module.exports` as the default binding
|
|
28
28
|
// (statically-detected named exports would break this half at boot).
|
|
@@ -47,26 +47,73 @@ import shared from "./shared.cjs";
|
|
|
47
47
|
* entry (its `node_modules` holds the authoritative copy); anything resolving
|
|
48
48
|
* back inside THIS package is skipped, so the offline suites — which have no
|
|
49
49
|
* host — still fall through to the devDependency.
|
|
50
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* RESOLVE with `require`, LOAD with the ESM loader. The package resolves to its
|
|
52
|
+
* CommonJS build, and that build `require`s `@deepseek-ai/cosmokit`, which ships
|
|
53
|
+
* as ES module only. Requiring it while the host is still loading that same
|
|
54
|
+
* module graph throws `ERR_REQUIRE_ESM_RACE_CONDITION` — "Cannot require() ES
|
|
55
|
+
* Module … because it is not yet fully loaded" — and then the whole host half
|
|
56
|
+
* fails to import: no settings namespace, no first-frame stylesheet, while the
|
|
57
|
+
* browser half keeps working and hides the damage (issue #7, reproduced here).
|
|
58
|
+
* `import()` waits for the graph instead of racing it, and it lands on the same
|
|
59
|
+
* instance: a `.cjs` file loaded through the ESM loader goes through the
|
|
60
|
+
* CommonJS cache, so the host's copy is still the only copy.
|
|
61
|
+
* @returns {Promise<object>} the schemastery export.
|
|
51
62
|
*/
|
|
52
|
-
function loadSchemastery() {
|
|
63
|
+
async function loadSchemastery() {
|
|
53
64
|
const own = dirname(dirname(fileURLToPath(import.meta.url))) + sep;
|
|
54
65
|
const bases = [process.argv[1], fileURLToPath(import.meta.url)];
|
|
55
66
|
for (const base of bases) {
|
|
56
67
|
if (typeof base !== "string" || base === "") continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
const resolved = resolveSchemastery(createRequire(base), own);
|
|
69
|
+
if (resolved !== null) return importSchemastery(resolved);
|
|
70
|
+
}
|
|
71
|
+
// Nothing outside this package offers it (the offline suites have no host), so
|
|
72
|
+
// the devDependency is the answer — still loaded through the ESM loader.
|
|
73
|
+
const fallback = resolveSchemastery(createRequire(import.meta.url), "");
|
|
74
|
+
if (fallback === null) {
|
|
75
|
+
throw new Error("@deepseek-ai/schemastery is not resolvable from the host or this package");
|
|
76
|
+
}
|
|
77
|
+
return importSchemastery(fallback);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Locate the schema library from one anchor WITHOUT loading it.
|
|
82
|
+
*
|
|
83
|
+
* Only the resolution is anchored to the host: `createRequire(base).resolve()`
|
|
84
|
+
* answers "which file would the host use", while nothing is executed. An answer
|
|
85
|
+
* that points back inside this package is refused, so a linked install cannot
|
|
86
|
+
* shadow the host's copy with this package's devDependency.
|
|
87
|
+
* @param {NodeRequire} from - the resolver anchored at the host entry.
|
|
88
|
+
* @param {string} own - this package's directory prefix, "" to accept anything.
|
|
89
|
+
* @returns {string|null} the file to load, or null when this anchor cannot offer it.
|
|
90
|
+
*/
|
|
91
|
+
function resolveSchemastery(from, own) {
|
|
92
|
+
try {
|
|
93
|
+
const resolved = from.resolve("@deepseek-ai/schemastery");
|
|
94
|
+
if (typeof resolved !== "string" || resolved === "") return null;
|
|
95
|
+
if (own !== "" && resolved.startsWith(own)) return null;
|
|
96
|
+
return resolved;
|
|
97
|
+
} catch {
|
|
98
|
+
// Not resolvable from that anchor (or has no `require` entry): try the next.
|
|
99
|
+
return null;
|
|
65
100
|
}
|
|
66
|
-
return createRequire(import.meta.url)("@deepseek-ai/schemastery");
|
|
67
101
|
}
|
|
68
102
|
|
|
69
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Load one resolved file through the ESM loader.
|
|
105
|
+
*
|
|
106
|
+
* A CommonJS build arrives as its `default` binding, which is the module's own
|
|
107
|
+
* `module.exports`; a build that is already ESM answers with itself.
|
|
108
|
+
* @param {string} resolved - an absolute path, from `resolveSchemastery`.
|
|
109
|
+
* @returns {Promise<object>} the schemastery export.
|
|
110
|
+
*/
|
|
111
|
+
async function importSchemastery(resolved) {
|
|
112
|
+
const loaded = await import(pathToFileURL(resolved).href);
|
|
113
|
+
return loaded.default ?? loaded;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const z = await loadSchemastery();
|
|
70
117
|
|
|
71
118
|
const {
|
|
72
119
|
buildFontCss,
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
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",
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"0.2.1-rc.1": "compatible",
|
|
51
51
|
"0.2.1-rc.2": "compatible",
|
|
52
52
|
"0.2.1-rc.4": "compatible",
|
|
53
|
-
"0.2.
|
|
53
|
+
"0.2.1-rc.3": "compatible",
|
|
54
|
+
"0.2.1-rc.5": "compatible",
|
|
55
|
+
"0.2.2-alpha.2": "compatible",
|
|
56
|
+
"0.2.2-rc.1": "compatible"
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
},
|