@typecad/ui 1.0.0-alpha.14 → 1.0.0-alpha.15

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.
Files changed (44) hide show
  1. package/README.md +1457 -1469
  2. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
  3. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
  4. package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
  5. package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
  6. package/dist/ui-engine/runtime-header/keyboard.js +676 -676
  7. package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1658
  8. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
  9. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
  10. package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
  11. package/dist/ui-engine/runtime-header/structs.js +228 -228
  12. package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
  13. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
  14. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
  15. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
  16. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
  17. package/dist/ui-engine/runtime-header/types-defines.js +110 -110
  18. package/dist/ui-engine/ui-lowering.d.ts +1 -1
  19. package/dist/ui-engine/ui-lowering.js +14 -5
  20. package/dist/ui-engine/ui-registry.js +9 -1
  21. package/dist/wizard/display-catalog.d.ts +0 -3
  22. package/dist/wizard/display-catalog.js +3 -12
  23. package/dist/wizard/integration-wizard.js +2 -13
  24. package/package.json +2 -2
  25. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
  26. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
  27. package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
  28. package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
  29. package/src/ui-engine/runtime-header/keyboard.ts +689 -689
  30. package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1683
  31. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
  32. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
  33. package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
  34. package/src/ui-engine/runtime-header/structs.ts +234 -234
  35. package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
  36. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
  37. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
  38. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
  39. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
  40. package/src/ui-engine/runtime-header/types-defines.ts +116 -116
  41. package/src/ui-engine/ui-lowering.ts +12 -4
  42. package/src/ui-engine/ui-registry.ts +9 -1
  43. package/src/wizard/display-catalog.ts +261 -273
  44. package/src/wizard/integration-wizard.ts +2 -12
@@ -2,29 +2,29 @@
2
2
  // scrollbar + buffered scroll canvas push.
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitTickScrollCanvasPhase() {
5
- return `
6
- // ②b Draw scrollbar + push canvas for the buffered scroll container.
7
- if (bufferedScrollNode >= 0 && bufferedScrollCanvas) {
8
- ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
9
- bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
10
- bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
11
- } else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
12
- // Band render fallback: the main loop didn't trigger it at the owner's
13
- // z-slot (e.g. the owner was filtered out after dispatch). Render now so
14
- // the subtree is never left unpainted. ui_render_scroll_bands sets
15
- // lastPaintedScrollY itself on success; only the failure fallback (band
16
- // canvas wouldn't allocate) needs to record the scrollY here so the next
17
- // frame's delta is correct.
18
- if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
19
- // No band canvas means there is no coherent frame to submit. Retain the
20
- // previous viewport rather than drawing the scrollbar/direct subtree to
21
- // the live SPI target, which would reintroduce tearing.
22
- if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
23
- __ui_nodes[bufferedScrollNode].dirty = 0;
24
- }
25
- } else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
26
- // The direct-full path is intentionally disabled for tear-sensitive scroll
27
- // rendering. Keep the retained pixels when no compositor is available.
28
- if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
5
+ return `
6
+ // ②b Draw scrollbar + push canvas for the buffered scroll container.
7
+ if (bufferedScrollNode >= 0 && bufferedScrollCanvas) {
8
+ ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
9
+ bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
10
+ bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
11
+ } else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
12
+ // Band render fallback: the main loop didn't trigger it at the owner's
13
+ // z-slot (e.g. the owner was filtered out after dispatch). Render now so
14
+ // the subtree is never left unpainted. ui_render_scroll_bands sets
15
+ // lastPaintedScrollY itself on success; only the failure fallback (band
16
+ // canvas wouldn't allocate) needs to record the scrollY here so the next
17
+ // frame's delta is correct.
18
+ if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
19
+ // No band canvas means there is no coherent frame to submit. Retain the
20
+ // previous viewport rather than drawing the scrollbar/direct subtree to
21
+ // the live SPI target, which would reintroduce tearing.
22
+ if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
23
+ __ui_nodes[bufferedScrollNode].dirty = 0;
24
+ }
25
+ } else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
26
+ // The direct-full path is intentionally disabled for tear-sensitive scroll
27
+ // rendering. Keep the retained pixels when no compositor is available.
28
+ if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
29
29
  __ui_nodes[bufferedScrollNode].dirty = 0;`;
30
30
  }
@@ -26,7 +26,7 @@ extern const uint16_t __ui_rangechange_handler_count;
26
26
 
27
27
  // Touch state machine: tracks down → hold → up → click lifecycle
28
28
  // Touch node/scroll node state is defined near navigation because ui_navigate resets it.
29
- static uint32_t __ui_touch_down_time = 0; // millis() when touch started
29
+ static uint32_t __ui_touch_down_time = 0; // __tc_now_ms() when touch started
30
30
  static int16_t __ui_touch_down_y_pos = 0; // Y position when touch started (for tap vs drag detection)
31
31
  static uint32_t __ui_last_touch_time = 0; // for debounce (updated on touch down only)
32
32
  static uint32_t __ui_last_release_time = 0; // for release debounce
@@ -174,13 +174,13 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
174
174
  // option rows. No hit-test/press/scroll while the overlay is open.
175
175
  if (__ui_select_menu >= 0) {
176
176
  __ui_touch_state = 1;
177
- __ui_touch_down_time = millis();
177
+ __ui_touch_down_time = __tc_now_ms();
178
178
  return;
179
179
  }
180
180
  int16_t node = ui_hit_test(tx, ty);
181
181
  __ui_touch_node = node;
182
182
  __ui_touch_state = 1;
183
- __ui_touch_down_time = millis();
183
+ __ui_touch_down_time = __tc_now_ms();
184
184
  __ui_touch_down_y_pos = ty;
185
185
  __ui_drag_start_x = tx;
186
186
  __ui_drag_start_y = ty;
@@ -215,7 +215,7 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
215
215
  int16_t usable = __ui_nodes[node].box.w - 8;
216
216
  if (usable <= 0) usable = 1;
217
217
  int16_t nextVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
218
- nextVal = constrain(nextVal, rMin, rMax);
218
+ nextVal = __ui_constrain(nextVal, rMin, rMax);
219
219
  if (nextVal != __ui_nodes[node].value) {
220
220
  __ui_nodes[node].value = nextVal;
221
221
  ui_mark_dirty(node);
@@ -261,7 +261,7 @@ static void ui_touch_up() {
261
261
  if (__ui_select_menu >= 0) {
262
262
  ui_select_menu_tap(__ui_last_touch_x, __ui_last_touch_y);
263
263
  __ui_touch_state = 0;
264
- __ui_last_touch_time = millis();
264
+ __ui_last_touch_time = __tc_now_ms();
265
265
  return;
266
266
  }
267
267
  #if !defined(UI_HIDE_OSK)
@@ -269,11 +269,11 @@ static void ui_touch_up() {
269
269
  ui_kb_handle_tap(__ui_last_touch_x, __ui_last_touch_y);
270
270
  __ui_kb_bs_held = 0;
271
271
  __ui_touch_state = 0;
272
- __ui_last_touch_time = millis();
272
+ __ui_last_touch_time = __tc_now_ms();
273
273
  return;
274
274
  }
275
275
  #endif
276
- uint32_t elapsed = millis() - __ui_touch_down_time;
276
+ uint32_t elapsed = __tc_now_ms() - __ui_touch_down_time;
277
277
  // Release the scroll owner: arm a bounded settle (bounce-back / edge-snap).
278
278
  // No fling — motion ends with the finger (the settle animation is the only
279
279
  // post-lift motion, terminating within UI_SCROLL_SETTLE_MS).
@@ -353,7 +353,7 @@ static void ui_touch_up() {
353
353
  // Called each frame from ui_poll_touch when touch is detected.
354
354
  // Implements debounce + the down/hold/up/click state machine.
355
355
  static inline void ui_handle_touch(int16_t tx, int16_t ty) {
356
- uint32_t now = millis();
356
+ uint32_t now = __tc_now_ms();
357
357
  // Track last touch coords for tap-up routing.
358
358
  __ui_last_touch_x = tx;
359
359
  __ui_last_touch_y = ty;
@@ -404,7 +404,7 @@ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
404
404
  int16_t usable = __ui_nodes[__ui_range_node].box.w - 8;
405
405
  if (usable <= 0) usable = 1;
406
406
  int16_t newVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
407
- newVal = constrain(newVal, rMin, rMax);
407
+ newVal = __ui_constrain(newVal, rMin, rMax);
408
408
  if (newVal != __ui_nodes[__ui_range_node].value) {
409
409
  __ui_nodes[__ui_range_node].value = newVal;
410
410
  ui_mark_dirty(__ui_range_node);
@@ -457,9 +457,9 @@ static inline void ui_handle_no_touch() {
457
457
  if (__ui_touch_state != 0) {
458
458
  // Debounce: require a gap since the last release before processing another.
459
459
  // This prevents crash from rapid touch/no-touch flicker on resistive screens.
460
- if (millis() - __ui_last_release_time < UI_TOUCH_DEBOUNCE_MS) return;
460
+ if (__tc_now_ms() - __ui_last_release_time < UI_TOUCH_DEBOUNCE_MS) return;
461
461
  ui_touch_up();
462
- __ui_last_release_time = millis();
462
+ __ui_last_release_time = __tc_now_ms();
463
463
  }
464
464
  }
465
465
 
@@ -2,115 +2,115 @@
2
2
  // Guard open, includes, and #define knobs.
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitTypesDefines() {
5
- return `
6
- // ── Cuttlefish UI runtime (emit once per TU) ──────────────────────────────
7
- #ifndef __TC_UI_RUNTIME
8
- #define __TC_UI_RUNTIME
9
- #include <stdint.h>
10
- #include <cstring>
11
- #include <new>
12
- #define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
13
- #define UI_TEXT_LINE_BUF 96
14
- #define UI_WS_NORMAL 0
15
- #define UI_WS_NOWRAP 1
16
- #define UI_WS_PRE 2
17
- #define UI_WS_PRE_LINE 3
18
- #ifndef UI_MAX_BUFFERED_PAINT_PIXELS
19
- #define UI_MAX_BUFFERED_PAINT_PIXELS 20000
20
- #endif
21
- // Above this many pixels, prefer the band renderer (small ~10KB SRAM canvas,
22
- // strip-at-a-time) over the repair canvas even when the repair canvas could
23
- // allocate (e.g. in PSRAM). A large repair canvas (a full-width button is
24
- // ~15-30KB) has higher alloc/seed/push latency than banding — the per-band
25
- // push is smaller and the band canvas lives in fast internal SRAM. Set below
26
- // UI_MAX_BUFFERED_PAINT_PIXELS so large rects band while small repaints keep
27
- // the fast single-canvas composite. Tune per target; 0 = always prefer the
28
- // repair canvas when it allocates (the pre-PSRAM behavior).
29
- #ifndef UI_BAND_PREFER_PIXELS
30
- #define UI_BAND_PREFER_PIXELS 8000
31
- #endif
32
- #ifndef UI_USE_FULL_FRAMEBUFFER
33
- #define UI_USE_FULL_FRAMEBUFFER 0
34
- #endif
35
- #ifndef UI_TRANSITION_SNAP_MS
36
- #define UI_TRANSITION_SNAP_MS 100
37
- #endif
38
- // Scroll physics + capability defaults. The per-TU #define overrides emitted
39
- // by the UI emitter (from resolveScrollConfig) hit BEFORE this header, so these
40
- // #ifndef guards adopt the configured values. Spec: 2026-06-28-scroll-engine-rewrite-design.md
41
- #ifndef UI_SCROLL_MAX_OVERSCROLL
42
- #define UI_SCROLL_MAX_OVERSCROLL 40
43
- #endif
44
- #ifndef UI_SCROLL_STIFFNESS_X10
45
- #define UI_SCROLL_STIFFNESS_X10 5
46
- #endif
47
- #ifndef UI_SCROLL_EDGE_SNAP_PX
48
- #define UI_SCROLL_EDGE_SNAP_PX 12
49
- #endif
50
- #ifndef UI_SCROLL_DRAG_SCALE_X10
51
- #define UI_SCROLL_DRAG_SCALE_X10 10
52
- #endif
53
- #ifndef UI_SCROLL_SETTLE_MS
54
- #define UI_SCROLL_SETTLE_MS 180
55
- #endif
56
- #ifndef UI_SCROLL_DEADBAND_PX
57
- #define UI_SCROLL_DEADBAND_PX 2
58
- #endif
59
- // Capability tier flags (emitted per-TU before this header; defaults = full).
60
- // The default-on check must run BEFORE the per-flag #ifndef defaults below —
61
- // after them, all three flags are defined 0 and the fallback would redefine
62
- // UI_SCROLL_INPUT_TIER_RESISTIVE with a different value (ill-formed).
63
- #if !defined(UI_SCROLL_INPUT_TIER_CAPACITIVE) && !defined(UI_SCROLL_INPUT_TIER_RESISTIVE) && !defined(UI_SCROLL_INPUT_TIER_NONE)
64
- #define UI_SCROLL_INPUT_TIER_RESISTIVE 1
65
- #endif
66
- #ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
67
- #define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
68
- #endif
69
- #ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
70
- #define UI_SCROLL_INPUT_TIER_RESISTIVE 0
71
- #endif
72
- #ifndef UI_SCROLL_INPUT_TIER_NONE
73
- #define UI_SCROLL_INPUT_TIER_NONE 0
74
- #endif
75
- #ifndef UI_SCROLL_RENDER_TIER_FULL
76
- #define UI_SCROLL_RENDER_TIER_FULL 1
77
- #endif
78
- #ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
79
- #define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
80
- #endif
81
- #define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
82
- #define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
83
- // Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
84
- #ifndef UI_SCROLL_DEBUG
85
- #define UI_SCROLL_DEBUG 0
86
- #endif
87
- // Band renderer: height (px) of the horizontal band canvas used to composite
88
- // scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
89
- // budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
90
- // single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
91
- // fits within one band — if a glyph straddles a band boundary, its anti-
92
- // aliased edge pixels blend toward the wrong bg in the adjacent band, visible
93
- // as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
94
- // that's ~30KB (fits SRAM on most ESP32 targets).
95
- // Dirty-rect merging (LVGL refresh-cycle technique): several same-frame
96
- // dirty nodes whose paint rects are near each other composite as one banded
97
- // region instead of per-node transactions. Regions cap at the band-
98
- // compositable pixel budget; singleton rects keep the per-node ladder.
99
- #ifndef UI_MERGE_INFLATE
100
- #define UI_MERGE_INFLATE 8
101
- #endif
102
- #ifndef UI_MERGE_MAX_REGIONS
103
- #define UI_MERGE_MAX_REGIONS 6
104
- #endif
105
- #ifndef UI_MERGE_MAX_NODES
106
- #define UI_MERGE_MAX_NODES 12
107
- #endif
108
-
109
- #ifndef UI_STRIP_BAND_HEIGHT
110
- #define UI_STRIP_BAND_HEIGHT 32
111
- #endif
112
- #if defined(ESP32) || defined(ESP8266)
113
- #include <Esp.h>
114
- #endif
5
+ return `
6
+ // ── Cuttlefish UI runtime (emit once per TU) ──────────────────────────────
7
+ #ifndef __TC_UI_RUNTIME
8
+ #define __TC_UI_RUNTIME
9
+ #include <stdint.h>
10
+ #include <cstring>
11
+ #include <new>
12
+ #define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
13
+ #define UI_TEXT_LINE_BUF 96
14
+ #define UI_WS_NORMAL 0
15
+ #define UI_WS_NOWRAP 1
16
+ #define UI_WS_PRE 2
17
+ #define UI_WS_PRE_LINE 3
18
+ #ifndef UI_MAX_BUFFERED_PAINT_PIXELS
19
+ #define UI_MAX_BUFFERED_PAINT_PIXELS 20000
20
+ #endif
21
+ // Above this many pixels, prefer the band renderer (small ~10KB SRAM canvas,
22
+ // strip-at-a-time) over the repair canvas even when the repair canvas could
23
+ // allocate (e.g. in PSRAM). A large repair canvas (a full-width button is
24
+ // ~15-30KB) has higher alloc/seed/push latency than banding — the per-band
25
+ // push is smaller and the band canvas lives in fast internal SRAM. Set below
26
+ // UI_MAX_BUFFERED_PAINT_PIXELS so large rects band while small repaints keep
27
+ // the fast single-canvas composite. Tune per target; 0 = always prefer the
28
+ // repair canvas when it allocates (the pre-PSRAM behavior).
29
+ #ifndef UI_BAND_PREFER_PIXELS
30
+ #define UI_BAND_PREFER_PIXELS 8000
31
+ #endif
32
+ #ifndef UI_USE_FULL_FRAMEBUFFER
33
+ #define UI_USE_FULL_FRAMEBUFFER 0
34
+ #endif
35
+ #ifndef UI_TRANSITION_SNAP_MS
36
+ #define UI_TRANSITION_SNAP_MS 100
37
+ #endif
38
+ // Scroll physics + capability defaults. The per-TU #define overrides emitted
39
+ // by the UI emitter (from resolveScrollConfig) hit BEFORE this header, so these
40
+ // #ifndef guards adopt the configured values. Spec: 2026-06-28-scroll-engine-rewrite-design.md
41
+ #ifndef UI_SCROLL_MAX_OVERSCROLL
42
+ #define UI_SCROLL_MAX_OVERSCROLL 40
43
+ #endif
44
+ #ifndef UI_SCROLL_STIFFNESS_X10
45
+ #define UI_SCROLL_STIFFNESS_X10 5
46
+ #endif
47
+ #ifndef UI_SCROLL_EDGE_SNAP_PX
48
+ #define UI_SCROLL_EDGE_SNAP_PX 12
49
+ #endif
50
+ #ifndef UI_SCROLL_DRAG_SCALE_X10
51
+ #define UI_SCROLL_DRAG_SCALE_X10 10
52
+ #endif
53
+ #ifndef UI_SCROLL_SETTLE_MS
54
+ #define UI_SCROLL_SETTLE_MS 180
55
+ #endif
56
+ #ifndef UI_SCROLL_DEADBAND_PX
57
+ #define UI_SCROLL_DEADBAND_PX 2
58
+ #endif
59
+ // Capability tier flags (emitted per-TU before this header; defaults = full).
60
+ // The default-on check must run BEFORE the per-flag #ifndef defaults below —
61
+ // after them, all three flags are defined 0 and the fallback would redefine
62
+ // UI_SCROLL_INPUT_TIER_RESISTIVE with a different value (ill-formed).
63
+ #if !defined(UI_SCROLL_INPUT_TIER_CAPACITIVE) && !defined(UI_SCROLL_INPUT_TIER_RESISTIVE) && !defined(UI_SCROLL_INPUT_TIER_NONE)
64
+ #define UI_SCROLL_INPUT_TIER_RESISTIVE 1
65
+ #endif
66
+ #ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
67
+ #define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
68
+ #endif
69
+ #ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
70
+ #define UI_SCROLL_INPUT_TIER_RESISTIVE 0
71
+ #endif
72
+ #ifndef UI_SCROLL_INPUT_TIER_NONE
73
+ #define UI_SCROLL_INPUT_TIER_NONE 0
74
+ #endif
75
+ #ifndef UI_SCROLL_RENDER_TIER_FULL
76
+ #define UI_SCROLL_RENDER_TIER_FULL 1
77
+ #endif
78
+ #ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
79
+ #define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
80
+ #endif
81
+ #define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
82
+ #define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
83
+ // Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
84
+ #ifndef UI_SCROLL_DEBUG
85
+ #define UI_SCROLL_DEBUG 0
86
+ #endif
87
+ // Band renderer: height (px) of the horizontal band canvas used to composite
88
+ // scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
89
+ // budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
90
+ // single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
91
+ // fits within one band — if a glyph straddles a band boundary, its anti-
92
+ // aliased edge pixels blend toward the wrong bg in the adjacent band, visible
93
+ // as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
94
+ // that's ~30KB (fits SRAM on most ESP32 targets).
95
+ // Dirty-rect merging (LVGL refresh-cycle technique): several same-frame
96
+ // dirty nodes whose paint rects are near each other composite as one banded
97
+ // region instead of per-node transactions. Regions cap at the band-
98
+ // compositable pixel budget; singleton rects keep the per-node ladder.
99
+ #ifndef UI_MERGE_INFLATE
100
+ #define UI_MERGE_INFLATE 8
101
+ #endif
102
+ #ifndef UI_MERGE_MAX_REGIONS
103
+ #define UI_MERGE_MAX_REGIONS 6
104
+ #endif
105
+ #ifndef UI_MERGE_MAX_NODES
106
+ #define UI_MERGE_MAX_NODES 12
107
+ #endif
108
+
109
+ #ifndef UI_STRIP_BAND_HEIGHT
110
+ #define UI_STRIP_BAND_HEIGHT 32
111
+ #endif
112
+ #if defined(ESP32) || defined(ESP8266)
113
+ #include <Esp.h>
114
+ #endif
115
115
  `;
116
116
  }
@@ -27,5 +27,5 @@ export interface LoweredUI {
27
27
  }
28
28
  type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
29
29
  type Storage = "progmem" | "flash";
30
- export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[]): LoweredUI;
30
+ export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], keyframeNs?: string): LoweredUI;
31
31
  export {};
@@ -16,7 +16,7 @@ import { resolveColor } from "./color.js";
16
16
  import { defaultFontAttribution } from "./default-font.js";
17
17
  import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
18
18
  import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
19
- export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = []) {
19
+ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = [], keyframeNs = "") {
20
20
  void storage;
21
21
  const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
22
22
  // Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
@@ -69,7 +69,7 @@ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [],
69
69
  : `void (*__ui_kb_loaders[])() = {};\nconst uint16_t __ui_kb_loader_count = 0;`;
70
70
  const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
71
71
  const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
72
- const keyframeTables = emitKeyframeTables(model);
72
+ const keyframeTables = emitKeyframeTables(model, keyframeNs);
73
73
  return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, diagnostics: [] };
74
74
  }
75
75
  function sanitizedId(id) {
@@ -358,7 +358,7 @@ function emitTransitionTable(model) {
358
358
  ].join("\n");
359
359
  }
360
360
  /** Emit keyframe stop arrays + keyframe set index + animation table. */
361
- function emitKeyframeTables(model) {
361
+ function emitKeyframeTables(model, keyframeNs = "") {
362
362
  if (model.keyframeSets.length === 0 && model.animations.length === 0) {
363
363
  return [
364
364
  `const UIKeyframeSet __ui_keyframe_sets[] = {};`,
@@ -369,8 +369,15 @@ function emitKeyframeTables(model) {
369
369
  }
370
370
  const lines = [];
371
371
  // Emit one stop array per keyframe set.
372
+ // `keyframeNs` namespaces symbols per UI module, and shared stylesheets can
373
+ // register the same animation name into multiple mounted modules — dedupe
374
+ // by name so identical sets emit once (duplicate static arrays fail to link).
375
+ const seenKfNames = new Set();
372
376
  for (const ks of model.keyframeSets) {
373
- const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
377
+ if (seenKfNames.has(ks.name))
378
+ continue;
379
+ seenKfNames.add(ks.name);
380
+ const safeName = (keyframeNs ? keyframeNs + "_" : "") + ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
374
381
  lines.push(`static const UIKeyframeStop __ui_kf_${safeName}_stops[] = {`);
375
382
  for (const s of ks.stops) {
376
383
  lines.push(` { .percent=${s.percent}, .props=${s.props}, .bg=${hex(s.bg)}, .fg=${hex(s.fg)}, .opacity=${s.opacity}, .transformOffsetX=${s.transformOffsetX}, .transformOffsetY=${s.transformOffsetY}, .translatePctX=${s.translatePctX}, .translatePctY=${s.translatePctY}, .scaleX=${s.scaleX}, .scaleY=${s.scaleY}, .rotateDeg=${s.rotateDeg}, .width=${s.width}, .height=${s.height} },`);
@@ -380,7 +387,9 @@ function emitKeyframeTables(model) {
380
387
  // Emit keyframe set index table.
381
388
  lines.push(`const UIKeyframeSet __ui_keyframe_sets[] = {`);
382
389
  model.keyframeSets.forEach((ks) => {
383
- const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
390
+ if (!seenKfNames.has(ks.name))
391
+ return;
392
+ const safeName = (keyframeNs ? keyframeNs + "_" : "") + ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
384
393
  lines.push(` { .stopCount=${ks.stops.length}, .stops=__ui_kf_${safeName}_stops },`);
385
394
  });
386
395
  lines.push(`};`);
@@ -165,7 +165,8 @@ export function lowerOnMount(htmlPath, opts) {
165
165
  const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
166
166
  // Load image assets before lowering so imgDataId can be set.
167
167
  const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
168
- const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets);
168
+ const kfNs = hashlibLike(abs);
169
+ const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, kfNs);
169
170
  // Layout diagnostics: viewport-overflow (node bottom past the viewport) and
170
171
  // text-overflow (node right edge past its parent). Both reference the node by
171
172
  // a human-label (#id / <tag "text"> / <tag>) so the author can find it in
@@ -247,6 +248,13 @@ export function getLoweredUIModule(htmlPath) {
247
248
  export function hasUIModule(htmlPath) {
248
249
  return modules.has(path.resolve(htmlPath));
249
250
  }
251
+ /** Stable short per-module namespace for generated symbols (link-level dedup). */
252
+ function hashlibLike(absPath) {
253
+ let h = 5381;
254
+ for (let i = 0; i < absPath.length; i++)
255
+ h = ((h * 33) ^ absPath.charCodeAt(i)) >>> 0;
256
+ return "m" + h.toString(36);
257
+ }
250
258
  /** All loaded UI modules (used by the emitter to inject every mounted tree). */
251
259
  export function allUIModules() {
252
260
  return [...modules.values()];
@@ -31,8 +31,6 @@ export interface DisplayCatalogEntry {
31
31
  invert?: boolean;
32
32
  antialias?: boolean;
33
33
  };
34
- /** Arduino libraries the driver needs (for the printed next steps). */
35
- arduinoLibraries?: string[];
36
34
  /** A wiring note printed with the summary. */
37
35
  wiringNote?: string;
38
36
  }
@@ -64,7 +62,6 @@ export interface TouchCatalogEntry {
64
62
  rx: number;
65
63
  };
66
64
  };
67
- arduinoLibrary?: string;
68
65
  }
69
66
  export declare const TOUCH_CATALOG: readonly TouchCatalogEntry[];
70
67
  /**
@@ -1,8 +1,8 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // Display + touch hardware catalog for the @typecad/ui integration wizard.
3
3
  //
4
- // Mirrors the built-in display profiles exported by @typecad/framework-arduino
5
- // (src/displays/*.ts — BUILT_IN_PROFILES) and the TouchLibrary union in
4
+ // Mirrors the built-in display profiles exported by the active framework
5
+ // (BUILT_IN_PROFILES) and the TouchLibrary union in
6
6
  // @typecad/cuttlefish's display-profile.ts. The wizard asks questions and
7
7
  // fills defaults from these entries; the resulting object literal is written
8
8
  // into the project's cuttlefish.config.ts `display` section.
@@ -32,7 +32,6 @@ export const DISPLAY_CATALOG = [
32
32
  colorOrder: "rgb",
33
33
  antialias: true,
34
34
  },
35
- arduinoLibraries: ["Adafruit GFX Library", "Adafruit ILI9341"],
36
35
  wiringNote: "Defaults assume ESP32 VSPI: SCK=18, MOSI=23, MISO=19.",
37
36
  },
38
37
  {
@@ -57,10 +56,9 @@ export const DISPLAY_CATALOG = [
57
56
  invert: false,
58
57
  antialias: true,
59
58
  },
60
- arduinoLibraries: ["Adafruit GFX Library", "Adafruit ST7735 and ST7789 Library"],
61
59
  wiringNote: "Defaults assume ESP32 VSPI: SCK=18, MOSI=23, MISO=19. Adafruit_ST7796S.h comes from the" +
62
60
  " Adafruit_ST7735_and_ST7789 fork bundled in the typecode demos (demos/demo-st/lib) —" +
63
- " copy it into your sketchbook libraries/ if your Library Manager copy lacks the class.",
61
+ " copy it into your libraries/ folder if your Library Manager copy lacks the class.",
64
62
  },
65
63
  {
66
64
  id: "ssd1309-i2c",
@@ -75,7 +73,6 @@ export const DISPLAY_CATALOG = [
75
73
  rotation: 0,
76
74
  antialias: false,
77
75
  },
78
- arduinoLibraries: ["Adafruit GFX Library", "Adafruit SSD1306"],
79
76
  },
80
77
  {
81
78
  id: "sdl",
@@ -116,7 +113,6 @@ export const TOUCH_CATALOG = [
116
113
  // Typical raw-ADC range for 320×240 modules; refine per panel.
117
114
  calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
118
115
  },
119
- arduinoLibrary: "XPT2046_Touchscreen",
120
116
  },
121
117
  {
122
118
  id: "FT6336U",
@@ -128,7 +124,6 @@ export const TOUCH_CATALOG = [
128
124
  i2cFrequency: 400_000,
129
125
  },
130
126
  // The framework's FT6336U adapter includes RAK14014_FT6336U.h.
131
- arduinoLibrary: "RAK14014-FT6336U",
132
127
  },
133
128
  {
134
129
  id: "GT911",
@@ -139,7 +134,6 @@ export const TOUCH_CATALOG = [
139
134
  i2cAddress: 0x5d,
140
135
  i2cFrequency: 400_000,
141
136
  },
142
- arduinoLibrary: "GT911",
143
137
  },
144
138
  {
145
139
  id: "CST816S",
@@ -150,7 +144,6 @@ export const TOUCH_CATALOG = [
150
144
  i2cAddress: 0x15,
151
145
  i2cFrequency: 400_000,
152
146
  },
153
- arduinoLibrary: "CST816S",
154
147
  },
155
148
  {
156
149
  id: "Adafruit_TouchScreen",
@@ -161,7 +154,6 @@ export const TOUCH_CATALOG = [
161
154
  analogPins: { xp: 24, yp: 25, xm: 26, ym: 27, rx: 300 },
162
155
  calibration: { xMin: 100, xMax: 900, yMin: 120, yMax: 900 },
163
156
  },
164
- arduinoLibrary: "Adafruit TouchScreen",
165
157
  },
166
158
  {
167
159
  id: "Adafruit_STMPE610",
@@ -172,7 +164,6 @@ export const TOUCH_CATALOG = [
172
164
  cs: 14,
173
165
  calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
174
166
  },
175
- arduinoLibrary: "Adafruit STMPE610",
176
167
  },
177
168
  {
178
169
  id: "adapter",
@@ -505,18 +505,7 @@ export async function runIntegrationWizard(cwd = process.cwd(), streams = {}) {
505
505
  console.log();
506
506
  console.log(chalk.cyan("Next steps"));
507
507
  console.log();
508
- const libraries = [];
509
- if (displayEntry.arduinoLibraries)
510
- libraries.push(...displayEntry.arduinoLibraries);
511
- if (touchEntry?.arduinoLibrary && touchRecord?.library)
512
- libraries.push(touchEntry.arduinoLibrary);
513
- if (libraries.length > 0) {
514
- console.log(" 1. Install the display/touch Arduino libraries (once per machine):");
515
- console.log();
516
- console.log(` ${chalk.cyan(`arduino-cli lib install ${libraries.map((lib) => `"${lib}"`).join(" ")}`)}`);
517
- console.log();
518
- }
519
- const firstStep = libraries.length > 0 ? 2 : 1;
508
+ const firstStep = 1;
520
509
  console.log(` ${firstStep}. Preview your UI on the desktop:`);
521
510
  console.log();
522
511
  console.log(` ${chalk.cyan(previewCommand ? "npm run preview" : "npx @typecad/cuttlefish preview")}`);
@@ -525,7 +514,7 @@ export async function runIntegrationWizard(cwd = process.cwd(), streams = {}) {
525
514
  console.log();
526
515
  console.log(` ${chalk.cyan("npx @typecad/cuttlefish build --compile")}`);
527
516
  console.log();
528
- console.log(` ${firstStep + 2}. Flash it (set console.port in the config, or pass --port):`);
517
+ console.log(` ${firstStep + 2}. Flash it (pass --port, or set CUTTLEFISH_PORT):`);
529
518
  console.log();
530
519
  console.log(` ${chalk.cyan("npx @typecad/cuttlefish build --compile --upload")}`);
531
520
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/ui",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "TypeCAD UI authoring library — HTML/CSS-driven graphics for microcontrollers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "assets"
45
45
  ],
46
46
  "dependencies": {
47
- "@typecad/cuttlefish": "1.0.0-alpha.14",
47
+ "@typecad/cuttlefish": "1.0.0-alpha.15",
48
48
  "chalk": "^6.0.0",
49
49
  "css-tree": "^3.2.1",
50
50
  "decode-ico": "^0.4.1",