@typecad/ui 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +1443 -0
- package/dist/engine-index.d.ts +7 -0
- package/dist/engine-index.js +41 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +42 -0
- package/dist/preview/build-program.d.ts +7 -0
- package/dist/preview/build-program.js +665 -0
- package/dist/preview/host-gfx.d.ts +90 -0
- package/dist/preview/host-gfx.js +552 -0
- package/dist/preview/host-ui-runtime.d.ts +241 -0
- package/dist/preview/host-ui-runtime.js +3236 -0
- package/dist/preview/types.d.ts +83 -0
- package/dist/preview/types.js +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +10 -0
- package/dist/ui-engine/block-layout.d.ts +8 -0
- package/dist/ui-engine/block-layout.js +95 -0
- package/dist/ui-engine/color.d.ts +49 -0
- package/dist/ui-engine/color.js +273 -0
- package/dist/ui-engine/css-parser.d.ts +155 -0
- package/dist/ui-engine/css-parser.js +1058 -0
- package/dist/ui-engine/default-keyboards.d.ts +3 -0
- package/dist/ui-engine/default-keyboards.js +53 -0
- package/dist/ui-engine/easing.d.ts +12 -0
- package/dist/ui-engine/easing.js +76 -0
- package/dist/ui-engine/font-assets.d.ts +50 -0
- package/dist/ui-engine/font-assets.js +431 -0
- package/dist/ui-engine/html-parser.d.ts +116 -0
- package/dist/ui-engine/html-parser.js +284 -0
- package/dist/ui-engine/image-assets.d.ts +21 -0
- package/dist/ui-engine/image-assets.js +125 -0
- package/dist/ui-engine/inline-parser.d.ts +30 -0
- package/dist/ui-engine/inline-parser.js +66 -0
- package/dist/ui-engine/keyframes.d.ts +3 -0
- package/dist/ui-engine/keyframes.js +39 -0
- package/dist/ui-engine/layout-engine.d.ts +35 -0
- package/dist/ui-engine/layout-engine.js +203 -0
- package/dist/ui-engine/model.d.ts +239 -0
- package/dist/ui-engine/model.js +987 -0
- package/dist/ui-engine/rich-layout.d.ts +51 -0
- package/dist/ui-engine/rich-layout.js +220 -0
- package/dist/ui-engine/run-types.d.ts +13 -0
- package/dist/ui-engine/run-types.js +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
- package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
- package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
- package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
- package/dist/ui-engine/runtime-header/display-shim.js +104 -0
- package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
- package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
- package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
- package/dist/ui-engine/runtime-header/guard-close.js +8 -0
- package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
- package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
- package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
- package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
- package/dist/ui-engine/runtime-header/keyboard.js +342 -0
- package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
- package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
- package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
- package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
- package/dist/ui-engine/runtime-header/structs.js +220 -0
- package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
- package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
- package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick.js +17 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
- package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
- package/dist/ui-engine/runtime-header/types-defines.js +79 -0
- package/dist/ui-engine/runtime-header.d.ts +1 -0
- package/dist/ui-engine/runtime-header.js +69 -0
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
- package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
- package/dist/ui-engine/select-engine.d.ts +4 -0
- package/dist/ui-engine/select-engine.js +19 -0
- package/dist/ui-engine/style-resolver.d.ts +77 -0
- package/dist/ui-engine/style-resolver.js +341 -0
- package/dist/ui-engine/text-layout.d.ts +19 -0
- package/dist/ui-engine/text-layout.js +92 -0
- package/dist/ui-engine/transpile-ui.d.ts +11 -0
- package/dist/ui-engine/transpile-ui.js +33 -0
- package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
- package/dist/ui-engine/ua-stylesheet.js +56 -0
- package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
- package/dist/ui-engine/ui-file-splitter.js +47 -0
- package/dist/ui-engine/ui-lowering.d.ts +33 -0
- package/dist/ui-engine/ui-lowering.js +390 -0
- package/dist/ui-engine/ui-registry.d.ts +81 -0
- package/dist/ui-engine/ui-registry.js +429 -0
- package/dist/ui-engine/yoga-layout.d.ts +12 -0
- package/dist/ui-engine/yoga-layout.js +314 -0
- package/package.json +86 -0
- package/src/engine-index.ts +51 -0
- package/src/index.ts +232 -0
- package/src/preview/build-program.ts +734 -0
- package/src/preview/host-gfx.ts +580 -0
- package/src/preview/host-ui-runtime.ts +3245 -0
- package/src/preview/types.ts +96 -0
- package/src/types.ts +127 -0
- package/src/ui-engine/block-layout.ts +104 -0
- package/src/ui-engine/color.ts +263 -0
- package/src/ui-engine/css-parser.ts +1003 -0
- package/src/ui-engine/default-keyboards.ts +58 -0
- package/src/ui-engine/easing.ts +53 -0
- package/src/ui-engine/font-assets.ts +512 -0
- package/src/ui-engine/html-parser.ts +397 -0
- package/src/ui-engine/image-assets.ts +170 -0
- package/src/ui-engine/inline-parser.ts +78 -0
- package/src/ui-engine/keyframes.ts +47 -0
- package/src/ui-engine/layout-engine.ts +228 -0
- package/src/ui-engine/model.ts +1225 -0
- package/src/ui-engine/rich-layout.ts +257 -0
- package/src/ui-engine/run-types.ts +14 -0
- package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
- package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
- package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
- package/src/ui-engine/runtime-header/display-shim.ts +104 -0
- package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
- package/src/ui-engine/runtime-header/guard-close.ts +8 -0
- package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
- package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
- package/src/ui-engine/runtime-header/keyboard.ts +342 -0
- package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
- package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
- package/src/ui-engine/runtime-header/structs.ts +220 -0
- package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
- package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
- package/src/ui-engine/runtime-header/tick.ts +18 -0
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
- package/src/ui-engine/runtime-header/types-defines.ts +79 -0
- package/src/ui-engine/runtime-header.ts +71 -0
- package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
- package/src/ui-engine/select-engine.ts +21 -0
- package/src/ui-engine/style-resolver.ts +416 -0
- package/src/ui-engine/text-layout.ts +114 -0
- package/src/ui-engine/transpile-ui.ts +45 -0
- package/src/ui-engine/ua-stylesheet.ts +60 -0
- package/src/ui-engine/ui-file-splitter.ts +59 -0
- package/src/ui-engine/ui-lowering.ts +459 -0
- package/src/ui-engine/ui-registry.ts +530 -0
- package/src/ui-engine/yoga-layout.ts +309 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Built-in default keyboard templates — emitted by the transpiler when an
|
|
3
|
+
// <input> has no explicit <keyboard> ref. Both are KeyboardTemplate constants
|
|
4
|
+
// (same shape as author-written <keyboard> blocks) so the lowering path is
|
|
5
|
+
// uniform: a loader function is generated for each.
|
|
6
|
+
//
|
|
7
|
+
// Every key carries well-known CSS classes (ui-key, ui-key-ok, etc.) so authors
|
|
8
|
+
// can theme the defaults without writing a custom <keyboard>. The lowering
|
|
9
|
+
// resolves these via CSS and falls back to hardcoded defaults when no CSS
|
|
10
|
+
// matches.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const k = (ch) => ({ ch, special: 0, classes: ["ui-key"] });
|
|
13
|
+
const sk = (ch, special, kind) => ({
|
|
14
|
+
ch,
|
|
15
|
+
special,
|
|
16
|
+
classes: ["ui-key", `ui-key-${kind}`],
|
|
17
|
+
});
|
|
18
|
+
// Alpha: 10×4 grid (bottom dock). Row 2 has shift + backspace; row 3 has
|
|
19
|
+
// 123 (page-swap to symbols), space (_), and OK.
|
|
20
|
+
export const DEFAULT_ALPHA_KEYBOARD = {
|
|
21
|
+
id: "default_alpha",
|
|
22
|
+
variant: "alpha",
|
|
23
|
+
classes: ["ui-keyboard"],
|
|
24
|
+
rows: [
|
|
25
|
+
[k("1"), k("2"), k("3"), k("4"), k("5"), k("6"), k("7"), k("8"), k("9"), k("0")],
|
|
26
|
+
[k("q"), k("w"), k("e"), k("r"), k("t"), k("y"), k("u"), k("i"), k("o"), k("p")],
|
|
27
|
+
[
|
|
28
|
+
sk("⇧", 1, "shift"),
|
|
29
|
+
k("a"), k("s"), k("d"), k("f"), k("g"), k("h"), k("j"), k("k"), k("l"),
|
|
30
|
+
sk("⌫", 2, "del"),
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
sk("123", 4, "page"),
|
|
34
|
+
k("z"), k("x"), k("c"), k("v"), k("b"), k("n"), k("m"),
|
|
35
|
+
k("_"),
|
|
36
|
+
sk("OK", 3, "ok"),
|
|
37
|
+
],
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
// Number: 4×4 grid (kept uniform with the alpha column count so the hit-mapping
|
|
41
|
+
// math stays simple). Covers digits, ".", "-", ABC (page-swap to alpha),
|
|
42
|
+
// backspace, and OK.
|
|
43
|
+
export const DEFAULT_NUMBER_KEYBOARD = {
|
|
44
|
+
id: "default_number",
|
|
45
|
+
variant: "number",
|
|
46
|
+
classes: ["ui-keyboard"],
|
|
47
|
+
rows: [
|
|
48
|
+
[k("1"), k("2"), k("3"), sk("⌫", 2, "del")],
|
|
49
|
+
[k("4"), k("5"), k("6"), k(".")],
|
|
50
|
+
[k("7"), k("8"), k("9"), k("-")],
|
|
51
|
+
[sk("ABC", 4, "page"), k("0"), sk("OK", 3, "ok")],
|
|
52
|
+
],
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const TIMING_LINEAR = 0;
|
|
2
|
+
export declare const TIMING_EASE_IN_OUT = 1;
|
|
3
|
+
export declare const TIMING_EASE = 2;
|
|
4
|
+
export declare const TIMING_EASE_IN = 3;
|
|
5
|
+
export declare const TIMING_EASE_OUT = 4;
|
|
6
|
+
/** Map a CSS animation-timing-function keyword to its TIMING_* code.
|
|
7
|
+
* Unknown/unsupported (cubic-bezier(), steps()) -> linear. */
|
|
8
|
+
export declare function timingFunctionCode(keyword: string | undefined): number;
|
|
9
|
+
/** Apply the easing curve to a 0..100 linear lerp factor (fixed-point, no
|
|
10
|
+
* floats). Mirrors the C++ ui_ease_lerp_k in runtime-header.ts exactly.
|
|
11
|
+
* Returns k unchanged for TIMING_LINEAR. */
|
|
12
|
+
export declare function easeCurveLerpK(timing: number, k: number): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// animation-timing-function codes (kept in sync with runtime-header.ts UI_TIMING_*).
|
|
2
|
+
export const TIMING_LINEAR = 0;
|
|
3
|
+
export const TIMING_EASE_IN_OUT = 1;
|
|
4
|
+
export const TIMING_EASE = 2;
|
|
5
|
+
export const TIMING_EASE_IN = 3;
|
|
6
|
+
export const TIMING_EASE_OUT = 4;
|
|
7
|
+
/** Map a CSS animation-timing-function keyword to its TIMING_* code.
|
|
8
|
+
* Unknown/unsupported (cubic-bezier(), steps()) -> linear. */
|
|
9
|
+
export function timingFunctionCode(keyword) {
|
|
10
|
+
switch ((keyword ?? "").trim().toLowerCase()) {
|
|
11
|
+
case "ease-in-out": return TIMING_EASE_IN_OUT;
|
|
12
|
+
case "ease": return TIMING_EASE;
|
|
13
|
+
case "ease-in": return TIMING_EASE_IN;
|
|
14
|
+
case "ease-out": return TIMING_EASE_OUT;
|
|
15
|
+
default: return TIMING_LINEAR; // linear + cubic-bezier() + steps() unsupported
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Apply the easing curve to a 0..100 linear lerp factor (fixed-point, no
|
|
19
|
+
* floats). Mirrors the C++ ui_ease_lerp_k in runtime-header.ts exactly.
|
|
20
|
+
* Returns k unchanged for TIMING_LINEAR. */
|
|
21
|
+
export function easeCurveLerpK(timing, k) {
|
|
22
|
+
if (timing === TIMING_LINEAR || k <= 0)
|
|
23
|
+
return k;
|
|
24
|
+
if (k >= 100)
|
|
25
|
+
return 100;
|
|
26
|
+
// CSS cubic-bezier control points (normalized 0..1). Endpoints are (0,0),(1,1).
|
|
27
|
+
let x1 = 0, y1 = 0, x2 = 1, y2 = 1;
|
|
28
|
+
switch (timing) {
|
|
29
|
+
case TIMING_EASE_IN_OUT:
|
|
30
|
+
x1 = 0.42;
|
|
31
|
+
y1 = 0;
|
|
32
|
+
x2 = 0.58;
|
|
33
|
+
y2 = 1;
|
|
34
|
+
break;
|
|
35
|
+
case TIMING_EASE:
|
|
36
|
+
x1 = 0.25;
|
|
37
|
+
y1 = 0.1;
|
|
38
|
+
x2 = 0.25;
|
|
39
|
+
y2 = 1;
|
|
40
|
+
break;
|
|
41
|
+
case TIMING_EASE_IN:
|
|
42
|
+
x1 = 0.42;
|
|
43
|
+
y1 = 0;
|
|
44
|
+
x2 = 1;
|
|
45
|
+
y2 = 1;
|
|
46
|
+
break;
|
|
47
|
+
case TIMING_EASE_OUT:
|
|
48
|
+
x1 = 0;
|
|
49
|
+
y1 = 0;
|
|
50
|
+
x2 = 0.58;
|
|
51
|
+
y2 = 1;
|
|
52
|
+
break;
|
|
53
|
+
default: return k;
|
|
54
|
+
}
|
|
55
|
+
// Solve X(t)=input for t, then return Y(t). X(t)=3(1-t)^2*t*x1 + 3(1-t)*t^2*x2 + t^3.
|
|
56
|
+
// Bisection (not Newton-Raphson): Newton diverges for curves whose x-derivative
|
|
57
|
+
// is ~0 near an endpoint (ease-out: x1=0), snapping the dot to the wrong stop.
|
|
58
|
+
// X(t) is monotonic increasing for valid CSS control points, so bisection always
|
|
59
|
+
// converges. The device runtime's ui_ease_lerp_k uses the identical algorithm +
|
|
60
|
+
// control points (in /1000 fixed point) so preview and device agree.
|
|
61
|
+
const input = k / 100;
|
|
62
|
+
let lo = 0, hi = 1;
|
|
63
|
+
for (let i = 0; i < 20; i++) {
|
|
64
|
+
const t = (lo + hi) / 2;
|
|
65
|
+
const mt = 1 - t;
|
|
66
|
+
const x = 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t;
|
|
67
|
+
if (x < input)
|
|
68
|
+
lo = t;
|
|
69
|
+
else
|
|
70
|
+
hi = t;
|
|
71
|
+
}
|
|
72
|
+
const t = (lo + hi) / 2;
|
|
73
|
+
const mt = 1 - t;
|
|
74
|
+
const y = 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t;
|
|
75
|
+
return Math.round(y * 100);
|
|
76
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CSSFontFace, CSSProperty } from "./css-parser.js";
|
|
2
|
+
import type { StyledNode } from "./style-resolver.js";
|
|
3
|
+
export interface UIFontGlyphModel {
|
|
4
|
+
codepoint: number;
|
|
5
|
+
xOffset: number;
|
|
6
|
+
yOffset: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
advance: number;
|
|
10
|
+
/** Offset into the packed alpha stream, measured in 4-bit pixels. */
|
|
11
|
+
dataOffset: number;
|
|
12
|
+
}
|
|
13
|
+
export type UIFontSubsetMode = "exact" | "fallback";
|
|
14
|
+
export interface UIFontAssetModel {
|
|
15
|
+
id: number;
|
|
16
|
+
family: string;
|
|
17
|
+
sourcePath: string;
|
|
18
|
+
px: number;
|
|
19
|
+
fontWeight: string;
|
|
20
|
+
fontStyle: string;
|
|
21
|
+
subset: UIFontSubsetMode;
|
|
22
|
+
lineHeight: number;
|
|
23
|
+
baseline: number;
|
|
24
|
+
glyphs: UIFontGlyphModel[];
|
|
25
|
+
alpha: number[];
|
|
26
|
+
}
|
|
27
|
+
export interface UIFontAssetPlan {
|
|
28
|
+
family: string;
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
px: number;
|
|
31
|
+
fontWeight: string;
|
|
32
|
+
fontStyle: string;
|
|
33
|
+
subset: UIFontSubsetMode;
|
|
34
|
+
chars: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare function normalizeFontFamily(value: string | undefined): string | undefined;
|
|
37
|
+
export declare function fontPxOf(style: CSSProperty): number;
|
|
38
|
+
export declare function normalizeFontWeight(value: string | undefined): string;
|
|
39
|
+
export declare function normalizeFontStyle(value: string | undefined): string;
|
|
40
|
+
export declare function fontSubsetOf(style: CSSProperty): UIFontSubsetMode;
|
|
41
|
+
export declare function selectFontFaceForStyle(fontFaces: CSSFontFace[], style: CSSProperty): CSSFontFace | undefined;
|
|
42
|
+
export declare function selectFontAssetForStyle(fontAssets: UIFontAssetModel[], style: CSSProperty): UIFontAssetModel | undefined;
|
|
43
|
+
/** Measure a string's pixel width using the real per-glyph advances of the
|
|
44
|
+
* asset font a node resolves to. Returns undefined when the node uses the
|
|
45
|
+
* default font (no matching asset), so callers fall back to the 6*ts advance.
|
|
46
|
+
* Characters missing from the asset's subset fall back to half the line height
|
|
47
|
+
* (matching the runtime's ui_asset_text_width fallback). */
|
|
48
|
+
export declare function assetTextWidth(text: string, style: CSSProperty, fontAssets: UIFontAssetModel[]): number | undefined;
|
|
49
|
+
export declare function buildUIFontAssets(root: StyledNode, fontFaces: CSSFontFace[], baseDir: string): UIFontAssetModel[];
|
|
50
|
+
export declare function planUIFontAssets(root: StyledNode, fontFaces: CSSFontFace[], baseDir: string): UIFontAssetPlan[];
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import opentype from "opentype.js";
|
|
4
|
+
import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
|
|
5
|
+
const FALLBACK_CHARS = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.,:;!?+-*/=%()[]{}<>_#@&";
|
|
6
|
+
const SUPERSAMPLE = 4;
|
|
7
|
+
export function normalizeFontFamily(value) {
|
|
8
|
+
if (!value)
|
|
9
|
+
return undefined;
|
|
10
|
+
const first = value.split(",")[0]?.trim();
|
|
11
|
+
if (!first)
|
|
12
|
+
return undefined;
|
|
13
|
+
if ((first.startsWith('"') && first.endsWith('"')) || (first.startsWith("'") && first.endsWith("'"))) {
|
|
14
|
+
return first.slice(1, -1);
|
|
15
|
+
}
|
|
16
|
+
return first;
|
|
17
|
+
}
|
|
18
|
+
export function fontPxOf(style) {
|
|
19
|
+
if (!style.fontSize)
|
|
20
|
+
return 16;
|
|
21
|
+
const px = parseInt(style.fontSize, 10);
|
|
22
|
+
return Number.isFinite(px) && px > 0 ? px : 16;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeFontWeight(value) {
|
|
25
|
+
const normalized = value?.trim().toLowerCase();
|
|
26
|
+
if (!normalized || normalized === "normal")
|
|
27
|
+
return "400";
|
|
28
|
+
if (normalized === "bold" || normalized === "bolder")
|
|
29
|
+
return "700";
|
|
30
|
+
if (normalized === "lighter")
|
|
31
|
+
return "300";
|
|
32
|
+
const numeric = /^(\d{1,4})/.exec(normalized);
|
|
33
|
+
if (!numeric)
|
|
34
|
+
return "400";
|
|
35
|
+
const n = Math.max(1, Math.min(1000, Number(numeric[1])));
|
|
36
|
+
return Number.isFinite(n) ? String(n) : "400";
|
|
37
|
+
}
|
|
38
|
+
export function normalizeFontStyle(value) {
|
|
39
|
+
const normalized = value?.trim().toLowerCase();
|
|
40
|
+
if (!normalized || normalized === "normal")
|
|
41
|
+
return "normal";
|
|
42
|
+
if (normalized.includes("italic"))
|
|
43
|
+
return "italic";
|
|
44
|
+
if (normalized.includes("oblique"))
|
|
45
|
+
return "oblique";
|
|
46
|
+
return "normal";
|
|
47
|
+
}
|
|
48
|
+
export function fontSubsetOf(style) {
|
|
49
|
+
const subset = style.fontSubset?.trim().toLowerCase();
|
|
50
|
+
if (!subset || subset === "exact" || subset === "used")
|
|
51
|
+
return "exact";
|
|
52
|
+
if (subset === "fallback" || subset === "auto" || subset === "ascii" || subset === "common") {
|
|
53
|
+
return "fallback";
|
|
54
|
+
}
|
|
55
|
+
return "exact";
|
|
56
|
+
}
|
|
57
|
+
export function selectFontFaceForStyle(fontFaces, style) {
|
|
58
|
+
const family = normalizeFontFamily(style.fontFamily);
|
|
59
|
+
if (!family)
|
|
60
|
+
return undefined;
|
|
61
|
+
const candidates = fontFaces.filter((face) => face.fontFamily.toLowerCase() === family.toLowerCase());
|
|
62
|
+
if (candidates.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
const desiredWeight = Number(normalizeFontWeight(style.fontWeight));
|
|
65
|
+
const desiredStyle = normalizeFontStyle(style.fontStyle);
|
|
66
|
+
return [...candidates].sort((a, b) => fontFaceScore(a, desiredWeight, desiredStyle) - fontFaceScore(b, desiredWeight, desiredStyle))[0];
|
|
67
|
+
}
|
|
68
|
+
export function selectFontAssetForStyle(fontAssets, style) {
|
|
69
|
+
const family = normalizeFontFamily(style.fontFamily);
|
|
70
|
+
if (!family)
|
|
71
|
+
return undefined;
|
|
72
|
+
const px = fontPxOf(style);
|
|
73
|
+
const candidates = fontAssets.filter((asset) => asset.family.toLowerCase() === family.toLowerCase() && asset.px === px);
|
|
74
|
+
if (candidates.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
const desiredWeight = Number(normalizeFontWeight(style.fontWeight));
|
|
77
|
+
const desiredStyle = normalizeFontStyle(style.fontStyle);
|
|
78
|
+
return [...candidates].sort((a, b) => fontAssetScore(a, desiredWeight, desiredStyle) - fontAssetScore(b, desiredWeight, desiredStyle))[0];
|
|
79
|
+
}
|
|
80
|
+
/** Measure a string's pixel width using the real per-glyph advances of the
|
|
81
|
+
* asset font a node resolves to. Returns undefined when the node uses the
|
|
82
|
+
* default font (no matching asset), so callers fall back to the 6*ts advance.
|
|
83
|
+
* Characters missing from the asset's subset fall back to half the line height
|
|
84
|
+
* (matching the runtime's ui_asset_text_width fallback). */
|
|
85
|
+
export function assetTextWidth(text, style, fontAssets) {
|
|
86
|
+
const asset = selectFontAssetForStyle(fontAssets, style);
|
|
87
|
+
if (!asset)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (text.length === 0)
|
|
90
|
+
return 0;
|
|
91
|
+
const fallback = Math.max(1, Math.floor(asset.lineHeight / 2));
|
|
92
|
+
let w = 0;
|
|
93
|
+
for (const ch of text) {
|
|
94
|
+
const cp = ch.codePointAt(0);
|
|
95
|
+
const glyph = asset.glyphs.find((g) => g.codepoint === cp);
|
|
96
|
+
w += glyph ? glyph.advance : fallback;
|
|
97
|
+
}
|
|
98
|
+
return w;
|
|
99
|
+
}
|
|
100
|
+
export function buildUIFontAssets(root, fontFaces, baseDir) {
|
|
101
|
+
const plans = planUIFontAssets(root, fontFaces, baseDir);
|
|
102
|
+
const parsedFonts = new Map();
|
|
103
|
+
const assets = [];
|
|
104
|
+
let id = 1;
|
|
105
|
+
for (const plan of plans) {
|
|
106
|
+
let font = parsedFonts.get(plan.sourcePath);
|
|
107
|
+
if (!font) {
|
|
108
|
+
const bytes = fs.readFileSync(plan.sourcePath);
|
|
109
|
+
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
110
|
+
font = opentype.parse(arrayBuffer);
|
|
111
|
+
parsedFonts.set(plan.sourcePath, font);
|
|
112
|
+
}
|
|
113
|
+
assets.push(rasterizeFontAsset({
|
|
114
|
+
id: id++,
|
|
115
|
+
family: plan.family,
|
|
116
|
+
sourcePath: plan.sourcePath,
|
|
117
|
+
px: plan.px,
|
|
118
|
+
fontWeight: plan.fontWeight,
|
|
119
|
+
fontStyle: plan.fontStyle,
|
|
120
|
+
subset: plan.subset,
|
|
121
|
+
chars: plan.chars,
|
|
122
|
+
font,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
return assets;
|
|
126
|
+
}
|
|
127
|
+
export function planUIFontAssets(root, fontFaces, baseDir) {
|
|
128
|
+
if (fontFaces.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
const requests = new Map();
|
|
131
|
+
const collect = (node) => {
|
|
132
|
+
const face = selectFontFaceForStyle(fontFaces, node.style);
|
|
133
|
+
if (face) {
|
|
134
|
+
const px = fontPxOf(node.style);
|
|
135
|
+
const sourcePath = resolveFontPath(face.src, baseDir);
|
|
136
|
+
const fontWeight = normalizeFontWeight(face.fontWeight ?? node.style.fontWeight);
|
|
137
|
+
const fontStyle = normalizeFontStyle(face.fontStyle ?? node.style.fontStyle);
|
|
138
|
+
const key = `${sourcePath}:${px}:${fontWeight}:${fontStyle}`;
|
|
139
|
+
let request = requests.get(key);
|
|
140
|
+
if (!request) {
|
|
141
|
+
request = {
|
|
142
|
+
family: face.fontFamily,
|
|
143
|
+
sourcePath,
|
|
144
|
+
px,
|
|
145
|
+
fontWeight,
|
|
146
|
+
fontStyle,
|
|
147
|
+
subset: "exact",
|
|
148
|
+
chars: new Set(),
|
|
149
|
+
};
|
|
150
|
+
requests.set(key, request);
|
|
151
|
+
}
|
|
152
|
+
if (fontSubsetOf(node.style) === "fallback" && request.subset !== "fallback") {
|
|
153
|
+
request.subset = "fallback";
|
|
154
|
+
addText(request.chars, FALLBACK_CHARS);
|
|
155
|
+
}
|
|
156
|
+
addNodeText(request.chars, node);
|
|
157
|
+
}
|
|
158
|
+
// Rich-text inline runs: each run has its own resolved style (bold, italic,
|
|
159
|
+
// different font-size) which maps to a different font face/asset. Collect
|
|
160
|
+
// each run's text under its OWN style so the per-face subsetting includes
|
|
161
|
+
// the run's characters. Without this, the run's font face is subsetted from
|
|
162
|
+
// unrelated text and glyphs go missing at draw time.
|
|
163
|
+
if (node.runs) {
|
|
164
|
+
for (const run of node.runs) {
|
|
165
|
+
const runStyle = { ...node.style, ...run.style };
|
|
166
|
+
const runFace = selectFontFaceForStyle(fontFaces, runStyle);
|
|
167
|
+
if (!runFace)
|
|
168
|
+
continue;
|
|
169
|
+
const runPx = fontPxOf(runStyle);
|
|
170
|
+
const runSourcePath = resolveFontPath(runFace.src, baseDir);
|
|
171
|
+
const runWeight = normalizeFontWeight(runFace.fontWeight ?? runStyle.fontWeight);
|
|
172
|
+
const runStyleAttr = normalizeFontStyle(runFace.fontStyle ?? runStyle.fontStyle);
|
|
173
|
+
const runKey = `${runSourcePath}:${runPx}:${runWeight}:${runStyleAttr}`;
|
|
174
|
+
let runReq = requests.get(runKey);
|
|
175
|
+
if (!runReq) {
|
|
176
|
+
runReq = {
|
|
177
|
+
family: runFace.fontFamily,
|
|
178
|
+
sourcePath: runSourcePath,
|
|
179
|
+
px: runPx,
|
|
180
|
+
fontWeight: runWeight,
|
|
181
|
+
fontStyle: runStyleAttr,
|
|
182
|
+
subset: "exact",
|
|
183
|
+
chars: new Set(),
|
|
184
|
+
};
|
|
185
|
+
requests.set(runKey, runReq);
|
|
186
|
+
}
|
|
187
|
+
addText(runReq.chars, applyTextTransform(run.text, runStyle));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const child of node.children)
|
|
191
|
+
collect(child);
|
|
192
|
+
};
|
|
193
|
+
collect(root);
|
|
194
|
+
return [...requests.values()]
|
|
195
|
+
.filter((request) => request.chars.size > 0)
|
|
196
|
+
.map((request) => ({
|
|
197
|
+
family: request.family,
|
|
198
|
+
sourcePath: request.sourcePath,
|
|
199
|
+
px: request.px,
|
|
200
|
+
fontWeight: request.fontWeight,
|
|
201
|
+
fontStyle: request.fontStyle,
|
|
202
|
+
subset: request.subset,
|
|
203
|
+
chars: [...request.chars].sort((a, b) => a.codePointAt(0) - b.codePointAt(0)),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
function resolveFontPath(src, baseDir) {
|
|
207
|
+
if (/^https?:\/\//i.test(src)) {
|
|
208
|
+
throw new Error(`@font-face src "${src}" is remote; use a local font file for embedded builds.`);
|
|
209
|
+
}
|
|
210
|
+
const withoutFileScheme = src.startsWith("file://") ? src.slice("file://".length) : src;
|
|
211
|
+
const resolved = path.isAbsolute(withoutFileScheme)
|
|
212
|
+
? withoutFileScheme
|
|
213
|
+
: path.resolve(baseDir, withoutFileScheme);
|
|
214
|
+
if (!fs.existsSync(resolved)) {
|
|
215
|
+
throw new Error(`@font-face font file not found: ${resolved}`);
|
|
216
|
+
}
|
|
217
|
+
return resolved;
|
|
218
|
+
}
|
|
219
|
+
function fontFaceScore(face, desiredWeight, desiredStyle) {
|
|
220
|
+
const style = normalizeFontStyle(face.fontStyle);
|
|
221
|
+
const weight = Number(normalizeFontWeight(face.fontWeight));
|
|
222
|
+
return styleScore(style, desiredStyle) * 10000 + Math.abs(weight - desiredWeight);
|
|
223
|
+
}
|
|
224
|
+
function fontAssetScore(asset, desiredWeight, desiredStyle) {
|
|
225
|
+
const weight = Number(normalizeFontWeight(asset.fontWeight));
|
|
226
|
+
return styleScore(asset.fontStyle, desiredStyle) * 10000 + Math.abs(weight - desiredWeight);
|
|
227
|
+
}
|
|
228
|
+
function styleScore(actual, desired) {
|
|
229
|
+
if (actual === desired)
|
|
230
|
+
return 0;
|
|
231
|
+
if (actual === "normal")
|
|
232
|
+
return 1;
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
function addNodeText(chars, node) {
|
|
236
|
+
addText(chars, applyTextTransform(node.text, node.style));
|
|
237
|
+
addText(chars, applyTextTransform(node.placeholder, node.style));
|
|
238
|
+
for (const option of node.options ?? []) {
|
|
239
|
+
addText(chars, applyTextTransform(option.text, node.style));
|
|
240
|
+
}
|
|
241
|
+
// Interpolation ({expr}) and text bindings produce runtime text the static
|
|
242
|
+
// template can't predict (numbers, dates, etc). Add digits + common
|
|
243
|
+
// formatting chars so the font subset can render the runtime output.
|
|
244
|
+
if (node.text && node.text.includes("{")) {
|
|
245
|
+
addText(chars, "0123456789.,-+/()%");
|
|
246
|
+
}
|
|
247
|
+
// <input> nodes on the SDL desktop target (UI_HIDE_OSK) accept arbitrary
|
|
248
|
+
// real-keyboard text — the OSK grid isn't shown, so the user can type any
|
|
249
|
+
// character, not just the keys on the on-screen grid. Pack the full printable
|
|
250
|
+
// ASCII range so every typed character has a glyph (otherwise letters absent
|
|
251
|
+
// from static UI text render blank — ui_font_glyph returns null). Hardware
|
|
252
|
+
// targets keep the minimal subset: the OSK grid is the only input path and
|
|
253
|
+
// only carries the keys it shows.
|
|
254
|
+
if (node.tag === "input") {
|
|
255
|
+
let driver;
|
|
256
|
+
try {
|
|
257
|
+
driver = getDisplayProfile().driver;
|
|
258
|
+
}
|
|
259
|
+
catch { /* no profile bound */ }
|
|
260
|
+
if (driver === "sdl") {
|
|
261
|
+
let ascii = "";
|
|
262
|
+
for (let cp = 0x20; cp <= 0x7e; cp++)
|
|
263
|
+
ascii += String.fromCodePoint(cp);
|
|
264
|
+
addText(chars, ascii);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function applyTextTransform(text, style) {
|
|
269
|
+
if (!text)
|
|
270
|
+
return text;
|
|
271
|
+
switch (style.textTransform) {
|
|
272
|
+
case "uppercase": return text.toUpperCase();
|
|
273
|
+
case "lowercase": return text.toLowerCase();
|
|
274
|
+
case "capitalize":
|
|
275
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
276
|
+
default: return text;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function addText(chars, text) {
|
|
280
|
+
if (!text)
|
|
281
|
+
return;
|
|
282
|
+
for (const ch of text) {
|
|
283
|
+
const cp = ch.codePointAt(0);
|
|
284
|
+
if (cp !== undefined && cp >= 32 && cp <= 0xffff)
|
|
285
|
+
chars.add(ch);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function rasterizeFontAsset(options) {
|
|
289
|
+
const scale = options.px / options.font.unitsPerEm;
|
|
290
|
+
const baseline = Math.ceil((options.font.ascender ?? options.font.unitsPerEm) * scale) + 1;
|
|
291
|
+
const lineHeight = Math.ceil(((options.font.ascender ?? options.font.unitsPerEm) - (options.font.descender ?? 0)) * scale) + 2;
|
|
292
|
+
const glyphs = [];
|
|
293
|
+
const unpackedAlpha = [];
|
|
294
|
+
for (const ch of options.chars) {
|
|
295
|
+
const glyph = options.font.charToGlyph(ch);
|
|
296
|
+
const advance = Math.max(1, Math.ceil((glyph.advanceWidth ?? options.font.unitsPerEm / 2) * scale));
|
|
297
|
+
const path = glyph.getPath(0, 0, options.px);
|
|
298
|
+
const bbox = path.getBoundingBox();
|
|
299
|
+
const empty = !Number.isFinite(bbox.x1) || !Number.isFinite(bbox.y1) || bbox.x1 === bbox.x2 || bbox.y1 === bbox.y2;
|
|
300
|
+
const xOffset = empty ? 0 : Math.floor(bbox.x1) - 1;
|
|
301
|
+
const yOffset = empty ? 0 : Math.floor(bbox.y1) - 1;
|
|
302
|
+
const width = empty ? 0 : Math.max(0, Math.ceil(bbox.x2) - xOffset + 1);
|
|
303
|
+
const height = empty ? 0 : Math.max(0, Math.ceil(bbox.y2) - yOffset + 1);
|
|
304
|
+
const dataOffset = unpackedAlpha.length;
|
|
305
|
+
if (width > 0 && height > 0) {
|
|
306
|
+
const contours = flattenPath(path.commands);
|
|
307
|
+
for (let py = 0; py < height; py++) {
|
|
308
|
+
for (let px = 0; px < width; px++) {
|
|
309
|
+
let covered = 0;
|
|
310
|
+
for (let sy = 0; sy < SUPERSAMPLE; sy++) {
|
|
311
|
+
for (let sx = 0; sx < SUPERSAMPLE; sx++) {
|
|
312
|
+
const x = xOffset + px + (sx + 0.5) / SUPERSAMPLE;
|
|
313
|
+
const y = yOffset + py + (sy + 0.5) / SUPERSAMPLE;
|
|
314
|
+
if (pointInContours(x, y, contours))
|
|
315
|
+
covered++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
unpackedAlpha.push(Math.round((covered * 15) / (SUPERSAMPLE * SUPERSAMPLE)));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
glyphs.push({
|
|
323
|
+
codepoint: ch.codePointAt(0) ?? 0,
|
|
324
|
+
xOffset,
|
|
325
|
+
yOffset,
|
|
326
|
+
width,
|
|
327
|
+
height,
|
|
328
|
+
advance,
|
|
329
|
+
dataOffset,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
id: options.id,
|
|
334
|
+
family: options.family,
|
|
335
|
+
sourcePath: options.sourcePath,
|
|
336
|
+
px: options.px,
|
|
337
|
+
fontWeight: options.fontWeight,
|
|
338
|
+
fontStyle: options.fontStyle,
|
|
339
|
+
subset: options.subset,
|
|
340
|
+
lineHeight,
|
|
341
|
+
baseline,
|
|
342
|
+
glyphs,
|
|
343
|
+
alpha: packNibbles(unpackedAlpha),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function flattenPath(commands) {
|
|
347
|
+
const contours = [];
|
|
348
|
+
let current = { x: 0, y: 0 };
|
|
349
|
+
let start = null;
|
|
350
|
+
let contour = [];
|
|
351
|
+
const push = (p) => {
|
|
352
|
+
contour.push(p);
|
|
353
|
+
current = p;
|
|
354
|
+
};
|
|
355
|
+
const finish = () => {
|
|
356
|
+
if (contour.length > 1)
|
|
357
|
+
contours.push(contour);
|
|
358
|
+
contour = [];
|
|
359
|
+
start = null;
|
|
360
|
+
};
|
|
361
|
+
for (const cmd of commands) {
|
|
362
|
+
if (cmd.type === "M") {
|
|
363
|
+
finish();
|
|
364
|
+
current = { x: cmd.x ?? 0, y: cmd.y ?? 0 };
|
|
365
|
+
start = current;
|
|
366
|
+
contour = [current];
|
|
367
|
+
}
|
|
368
|
+
else if (cmd.type === "L") {
|
|
369
|
+
push({ x: cmd.x ?? current.x, y: cmd.y ?? current.y });
|
|
370
|
+
}
|
|
371
|
+
else if (cmd.type === "Q") {
|
|
372
|
+
const p0 = current;
|
|
373
|
+
const p1 = { x: cmd.x1 ?? current.x, y: cmd.y1 ?? current.y };
|
|
374
|
+
const p2 = { x: cmd.x ?? current.x, y: cmd.y ?? current.y };
|
|
375
|
+
for (let i = 1; i <= 8; i++) {
|
|
376
|
+
const t = i / 8;
|
|
377
|
+
const mt = 1 - t;
|
|
378
|
+
push({
|
|
379
|
+
x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
|
|
380
|
+
y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (cmd.type === "C") {
|
|
385
|
+
const p0 = current;
|
|
386
|
+
const p1 = { x: cmd.x1 ?? current.x, y: cmd.y1 ?? current.y };
|
|
387
|
+
const p2 = { x: cmd.x2 ?? current.x, y: cmd.y2 ?? current.y };
|
|
388
|
+
const p3 = { x: cmd.x ?? current.x, y: cmd.y ?? current.y };
|
|
389
|
+
for (let i = 1; i <= 12; i++) {
|
|
390
|
+
const t = i / 12;
|
|
391
|
+
const mt = 1 - t;
|
|
392
|
+
push({
|
|
393
|
+
x: mt * mt * mt * p0.x + 3 * mt * mt * t * p1.x + 3 * mt * t * t * p2.x + t * t * t * p3.x,
|
|
394
|
+
y: mt * mt * mt * p0.y + 3 * mt * mt * t * p1.y + 3 * mt * t * t * p2.y + t * t * t * p3.y,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else if (cmd.type === "Z") {
|
|
399
|
+
if (start)
|
|
400
|
+
push(start);
|
|
401
|
+
finish();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
finish();
|
|
405
|
+
return contours;
|
|
406
|
+
}
|
|
407
|
+
function pointInContours(x, y, contours) {
|
|
408
|
+
let inside = false;
|
|
409
|
+
for (const contour of contours) {
|
|
410
|
+
for (let i = 0, j = contour.length - 1; i < contour.length; j = i++) {
|
|
411
|
+
const a = contour[i];
|
|
412
|
+
const b = contour[j];
|
|
413
|
+
const crosses = (a.y > y) !== (b.y > y);
|
|
414
|
+
if (crosses) {
|
|
415
|
+
const ix = ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x;
|
|
416
|
+
if (x < ix)
|
|
417
|
+
inside = !inside;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return inside;
|
|
422
|
+
}
|
|
423
|
+
function packNibbles(values) {
|
|
424
|
+
const out = [];
|
|
425
|
+
for (let i = 0; i < values.length; i += 2) {
|
|
426
|
+
const hi = Math.max(0, Math.min(15, values[i] ?? 0));
|
|
427
|
+
const lo = Math.max(0, Math.min(15, values[i + 1] ?? 0));
|
|
428
|
+
out.push((hi << 4) | lo);
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|