From cced1be19f3f4a7e84bd1ae66b3642e3688dac6f Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:00:59 -0700 Subject: [PATCH] Step 518: add icon and typography harmonization model --- editor/CMakeLists.txt | 9 ++ editor/src/IconTypographyHarmonizer.h | 60 +++++++++++++ editor/tests/step518_test.cpp | 125 ++++++++++++++++++++++++++ progress.md | 34 +++++++ 4 files changed, 228 insertions(+) create mode 100644 editor/src/IconTypographyHarmonizer.h create mode 100644 editor/tests/step518_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 19f9862..1508566 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3541,4 +3541,13 @@ target_link_libraries(step517_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step518_test tests/step518_test.cpp) +target_include_directories(step518_test PRIVATE src) +target_link_libraries(step518_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/IconTypographyHarmonizer.h b/editor/src/IconTypographyHarmonizer.h new file mode 100644 index 0000000..fcdeec9 --- /dev/null +++ b/editor/src/IconTypographyHarmonizer.h @@ -0,0 +1,60 @@ +#pragma once +// Step 518: Icon + Typography Contrast Harmonization + +#include +#include +#include + +struct TypographyProfile { + float bodySizePx = 14.0f; + float headingSizePx = 20.0f; + float lineHeight = 1.4f; + float weightBody = 450.0f; + float weightHeading = 640.0f; + float textContrast = 1.0f; +}; + +struct IconProfile { + float strokePx = 1.5f; + float contrast = 1.0f; + float opticalSize = 16.0f; +}; + +class IconTypographyHarmonizer { +public: + static TypographyProfile darkSurfaceTypography(float density) { + float d = std::max(0.8f, std::min(1.4f, density)); + TypographyProfile t; + t.bodySizePx = 14.0f * d; + t.headingSizePx = 20.0f * d; + t.lineHeight = 1.38f; + t.weightBody = 460.0f; + t.weightHeading = 650.0f; + t.textContrast = 6.4f; + return t; + } + + static IconProfile iconProfileFor(float density, bool emphasized) { + float d = std::max(0.8f, std::min(1.4f, density)); + IconProfile i; + i.strokePx = emphasized ? 1.9f * d : 1.5f * d; + i.contrast = emphasized ? 4.5f : 3.4f; + i.opticalSize = 16.0f * d; + return i; + } + + static bool harmonized(const TypographyProfile& t, const IconProfile& i) { + return t.textContrast >= 4.5f && i.contrast >= 3.0f && + i.strokePx >= 1.0f && t.headingSizePx > t.bodySizePx; + } + + static float rhythmScore(const TypographyProfile& t, + const std::vector& panelDensities) { + if (panelDensities.empty()) return 0.0f; + float spread = *std::max_element(panelDensities.begin(), panelDensities.end()) - + *std::min_element(panelDensities.begin(), panelDensities.end()); + float base = 1.0f - std::min(1.0f, spread / 12.0f); + float typeBonus = (t.lineHeight >= 1.3f && t.lineHeight <= 1.6f) ? 0.2f : 0.0f; + return std::max(0.0f, std::min(1.0f, base + typeBonus)); + } +}; diff --git a/editor/tests/step518_test.cpp b/editor/tests/step518_test.cpp new file mode 100644 index 0000000..eaff35c --- /dev/null +++ b/editor/tests/step518_test.cpp @@ -0,0 +1,125 @@ +// Step 518: Icon + Typography Contrast Harmonization (12 tests) + +#include "IconTypographyHarmonizer.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_typography_scales_with_density() { + TEST(typography_scales_with_density); + auto a = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + auto b = IconTypographyHarmonizer::darkSurfaceTypography(1.3f); + CHECK(b.bodySizePx > a.bodySizePx, "body size should scale up"); + PASS(); +} + +void test_density_clamps_low() { + TEST(density_clamps_low); + auto a = IconTypographyHarmonizer::darkSurfaceTypography(0.2f); + auto b = IconTypographyHarmonizer::darkSurfaceTypography(0.8f); + CHECK(a.bodySizePx == b.bodySizePx, "density should clamp low"); + PASS(); +} + +void test_density_clamps_high() { + TEST(density_clamps_high); + auto a = IconTypographyHarmonizer::darkSurfaceTypography(3.0f); + auto b = IconTypographyHarmonizer::darkSurfaceTypography(1.4f); + CHECK(a.bodySizePx == b.bodySizePx, "density should clamp high"); + PASS(); +} + +void test_heading_larger_than_body() { + TEST(heading_larger_than_body); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + CHECK(t.headingSizePx > t.bodySizePx, "heading should be larger"); + PASS(); +} + +void test_text_contrast_meets_aa() { + TEST(text_contrast_meets_aa); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + CHECK(t.textContrast >= 4.5f, "text contrast should meet AA"); + PASS(); +} + +void test_icon_profile_emphasized_has_higher_stroke() { + TEST(icon_profile_emphasized_has_higher_stroke); + auto a = IconTypographyHarmonizer::iconProfileFor(1.0f, false); + auto b = IconTypographyHarmonizer::iconProfileFor(1.0f, true); + CHECK(b.strokePx > a.strokePx, "emphasized icon stroke should be higher"); + PASS(); +} + +void test_icon_profile_has_min_contrast() { + TEST(icon_profile_has_min_contrast); + auto i = IconTypographyHarmonizer::iconProfileFor(1.0f, false); + CHECK(i.contrast >= 3.0f, "icon contrast should meet threshold"); + PASS(); +} + +void test_harmonized_profile_passes() { + TEST(harmonized_profile_passes); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + auto i = IconTypographyHarmonizer::iconProfileFor(1.0f, false); + CHECK(IconTypographyHarmonizer::harmonized(t, i), "harmonization should pass"); + PASS(); +} + +void test_harmonized_fails_with_low_icon_contrast() { + TEST(harmonized_fails_with_low_icon_contrast); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + IconProfile i = IconTypographyHarmonizer::iconProfileFor(1.0f, false); + i.contrast = 2.0f; + CHECK(!IconTypographyHarmonizer::harmonized(t, i), "harmonization should fail"); + PASS(); +} + +void test_rhythm_score_high_for_consistent_panels() { + TEST(rhythm_score_high_for_consistent_panels); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + float r = IconTypographyHarmonizer::rhythmScore(t, {12.0f, 13.0f, 12.5f}); + CHECK(r > 0.7f, "rhythm should be high for consistent densities"); + PASS(); +} + +void test_rhythm_score_low_for_wide_spread() { + TEST(rhythm_score_low_for_wide_spread); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + float r = IconTypographyHarmonizer::rhythmScore(t, {4.0f, 20.0f, 30.0f}); + CHECK(r < 0.5f, "rhythm should drop for wide spread"); + PASS(); +} + +void test_rhythm_zero_when_no_panels() { + TEST(rhythm_zero_when_no_panels); + auto t = IconTypographyHarmonizer::darkSurfaceTypography(1.0f); + CHECK(IconTypographyHarmonizer::rhythmScore(t, {}) == 0.0f, + "empty panel list should produce zero score"); + PASS(); +} + +int main() { + std::cout << "Step 518: Icon + Typography Contrast Harmonization\n"; + + test_typography_scales_with_density(); // 1 + test_density_clamps_low(); // 2 + test_density_clamps_high(); // 3 + test_heading_larger_than_body(); // 4 + test_text_contrast_meets_aa(); // 5 + test_icon_profile_emphasized_has_higher_stroke(); // 6 + test_icon_profile_has_min_contrast(); // 7 + test_harmonized_profile_passes(); // 8 + test_harmonized_fails_with_low_icon_contrast(); // 9 + test_rhythm_score_high_for_consistent_panels(); // 10 + test_rhythm_score_low_for_wide_spread(); // 11 + test_rhythm_zero_when_no_panels(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 30c6bf1..22abbba 100644 --- a/progress.md +++ b/progress.md @@ -8317,3 +8317,37 @@ contrast thresholds, and DPI-stable geometry scaling. - `editor/src/ControlLibraryRestyle.h` within header-size limit (`83` <= `600`) - `editor/tests/step517_test.cpp` within test-file size guidance (`127` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 518: Icon + Typography Contrast Harmonization +**Status:** PASS (12/12 tests) + +Implements icon and typography harmonization for dark surfaces with density-aware +sizing, stroke-weight balancing, contrast thresholds, and panel rhythm scoring +to reduce visual drift between dense and sparse UI regions. + +**Files added:** +- `editor/src/IconTypographyHarmonizer.h` - harmonization module: + - density-aware typography profile synthesis + - icon stroke/contrast profile synthesis + - harmonization validity checks for text/icon contrast and sizing + - rhythm scoring for cross-panel density consistency +- `editor/tests/step518_test.cpp` - 12 tests covering: + - density scaling + clamp behavior + - heading/body sizing hierarchy + - text/icon contrast thresholds + - harmonization pass/fail behavior + - rhythm score behavior for consistent vs divergent panel densities + +**Files modified:** +- `editor/CMakeLists.txt` - `step518_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step518_test` - PASS +- `./editor/build-native/step518_test` - PASS (12/12) +- `./editor/build-native/step517_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/IconTypographyHarmonizer.h` within header-size limit (`60` <= `600`) +- `editor/tests/step518_test.cpp` within test-file size guidance (`125` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`