Skip to content

[WIP]: Refractor layout and render system - #56

Draft
Windorain wants to merge 210 commits into
GTNewHorizons:masterfrom
Windorain:refractor-layout-and-render-system
Draft

[WIP]: Refractor layout and render system#56
Windorain wants to merge 210 commits into
GTNewHorizons:masterfrom
Windorain:refractor-layout-and-render-system

Conversation

@Windorain

@Windorain Windorain commented Aug 7, 2026

Copy link
Copy Markdown

Summary

Refactor the layout and rendering system to provide a better visual experience
Provide headless rendering functionality for AI workflows and automated testing

Todo

  • Incorrect LaTeX baseline and font size
  • Incorrect italic kerning
  • Mermaid jitter issue when zooming
  • Incorrect rasterization of underline dots and wavy line effects
  • Monospaced font support for code blocks
  • Settings page for configuring custom fonts
  • Merge other PRs to catch up on progress
  • Rewrite the doc documentation to support proper formatting
  • Clean up redundant files

Checklist

  • I have tested this PR in DevEnv
  • I have tested this PR in Fullpack
  • This PR is in compliance with the GTNH AI Policy
  • This PR requires another PR in order to merge

Windorain and others added 30 commits July 10, 2026 21:52
…ZipFile scans in GuidePageLanguageIndex/GuideResourceLanguageIndex
…l methods, add scan result caching

- Killed dead code: load(), discoverGuideLanguages(), scanResourcePack* chain
- Inlined 12 pure-function helpers (counters, freeze/format helpers, path utilities)
- Merged cold-path scan methods into their single callers
- Added scan result caching: same pack roots + folder → skip re-scan
- Skip pre-init reload in DefaultGuideResourcePackManager (0 guides always)
- Reduced method count from 57 to ~30, file from 1455 to 754 lines
…variants

- Wrap::WordOrGlyph + float-wrap band splitting (narrow beside float, full width below)
- rich text spans into glyph runs (argb/shear/decoration rects), justification pass
- min_h bridge for <br clear> so following blocks stack below floats
- harness: 19 scenarios incl. floatClearPage + glyph-vs-float overlap invariant
- fix order-dependent ThemeTokenSystemSmokeTest.testNameLookup (registry snapshot)
…lips/justify_pass deleted

- parley 0.11 BreakerState per-line widths replace band pre-split (float_clips schema)
- inline blocks become parley InlineBox (wrap width native, vertical growth stays post-pass)
- rich spans ride StyleProperty::Brush(span index); justify = native Alignment::Justify
- FFFC anchors stripped before shaping (no more .notdef placeholders)
- shapeText JNI (GuideText.emitText path) switched to parley; cosmic rasterizer deleted
- spike: parley_probe numeric A/B PASS, parley_render side-by-side PNG identical
- harness 19 scenarios 0 issues; runParleySmoke JNI A/B green
…s-taffy NO-GO spike + arch principles); carries in-flight WIP
…= the bridge), Java float wrapper eliminated + inner carries real float (gap via margin), float-replay scaffolding deleted; harness 20 scenarios 0 issues + unit tests green; layout-geometry single authority = Rust
…ecipe output arrow-gap -> output.marginLeft, toolbar label flex-grow -> LytBlock.flexGrow field; delete applyChildRules + marginLeftAdjust/flexGrowOverrides maps. Table column-width pin retained (runtime column model, deferred to grid migration). Zero new regression: recipe/toolbar/table scenarios green. NOTE: harness floatClearPage 1 issue is a PRE-EXISTING Java legacy clear bug (reproduced at c950f74 via git stash), unrelated to this commit — tracked separately
…ly declares). Schema TextData.clears [ClearBreak{raw_offset,side}]; Java serializer records each <br clear> at its raw UTF-8 byte offset (walkClears mirrors extractFlowText); Rust collect_layout drops lines after the covering line to the cleared floats' bottom edge and grows content height. Exact for the only real form (trailing clear); mid-paragraph clear is first-order (never mispositions). harness floatClearPage now GREEN (follower y=130 >= floatBottom=125) -> 20/20; test --rerun-tasks green. NOTE: harness reads follower.getBounds() which updateLayout fills from the RUST layout, so fixing Rust alone turned it green — no Java LineBuilder/harness change needed, confirming Rust is the layout authority. generated.rs + src/test are gitignored (not committed). LayoutStyleExtractor/LayoutTreeSerializer carry jump-5 spotless format debt, reverted out of this atomic commit
…t not hugging' bug). Root cause: collect_layout added the clear floor into the paragraph's own content_height, so a paragraph ending with <br clear=all> (the only real form, e.g. large_sifter.md intro) stretched its box to the float bottom — text sat at the top, leaving the blank gap the screenshot shows, and the following callout landed at the float bottom looking detached. CSS-correct fix: a trailing clear must NOT stretch the current paragraph; it pushes the flow AFTER it down. collect_layout now returns the clear floor instead of baking it into content_height; the pusher advances its cursor to max(natural, floor) after the paragraph, so the callout starts below the float while the intro hugs its text. Mid-paragraph clears are unaffected (their dropped later lines already raise content_height, so the natural cursor advance already clears the floor). Verified: harness floatClearPage intro now height=10 (was 125), follower y=130 >= floatBottom=125; new assertion 'intro inflated by trailing clear' added + floatAwareBlockPage guard scenario added (both in the gitignored local harness); 20/20 scenarios, test green. NOTE: the regression guard lives in src/test (gitignored by project convention), so it is local-only
…-stack). Schema TextData.breaks [uint] = raw byte offsets of non-clearing <br> in the break-free text; the Rust measure splits the paragraph at these offsets and shapes each piece independently, stacking them vertically (piece k+1 starts at the accumulated height), so <br> produces a real line break under parley 0.11 which has no white-space control and would fold a literal newline into a space. Clearing breaks are deliberately excluded from breaks (handled by the clear-floor path; a trailing clear on a segment boundary would otherwise be lost). PRE_WRAP code bodies stay on the legacy path: parley 0.11 exposes no Preserve collapse, so migrating them would drop indentation — tracked as a parley-capability blocker, not done here. Two offset-accumulation bugs fixed during observe: walkClears/walkBreaks were given a fresh offset counter per flow node (so every break recorded offset 0), and the harness DIAG caught breaks=[0] collapsing the split into a single shape. Verified: new breakWrapPage scenario height=20 (was 10), floatClearPage follower y=130 still green, 20/20 scenarios, test green. Regression guard + DIAG scaffolding live in the gitignored local harness
…deferred format debt + this session's edits). No logic change — verified whitespace/line-wrap only (e.g. NodeAdjustments record param rewrap); compileJava + test + runLayoutDump 20/20 green
Delete FlowBuilder/LineBuilder/Line/LineElement/LineBlock/LineTextRun
(1,179 lines). All paragraph layout is now Rust's single authority;
Java only declares intent.

Changes:
- Delete LytBalancedColumns, LytPlaceholderBlock, mode2 rendering
- Delete javaHeight/Math.max fallback, fontHandle==0 defense
- Delete deprecated horizontalLayout overloads, LayoutContext dead methods
- Delete LayoutStyleExtractor dead flags, LayoutTreeSerializer dead fields
- PRE_WRAP/Spoiler non-opaque via Rust glyph-run path
- §k non-opaque: Rust shapes layout, GuideText renders per-frame animation
- LytParagraph: FlowBuilder→ArrayList; computeLayout skips all LineBuilder;
  clean render() no-op, pickContent/getFirstLineBounds fallback paths
- Delete onExternalLayoutApplied/onLayoutMoved dead content.move() calls
- Schema InlineBlockRef.align docs add 3=FLOAT_LEFT 4=FLOAT_RIGHT
- Cargo.toml add lib crate-type for space_probe example

Harness 20/20 0 issues.
Two-pass shaping for inline blocks with align=3 (float-left) or
align=4 (float-right). Pass 1 shapes all inlines at full width to
determine each float's anchor line via layout.lines().text_range();
pass 2 skips float inline boxes and shapes with a merged document +
paragraph-local float table constraining subsequent line widths.

Changes:
- InlineSpec: add height, float_side, node fields
- GlyphAccum/ParleyShaped: add float_anchors (node_index, para_rel_y)
- shape_paragraph: two-pass path when any InlineSpec.float_side.is_some();
  extracted push_defaults/push_spans/push_inlines/break_and_align/
  break_with_floats helpers
- measure_text: fill float_side + height from InlineBlockRef.align
- inline_post_pass: regular markers→inline anchoring (unchanged);
  float markers (align≥3)→position at paragraph left/right edges
  using computed taffy sizes

Harness 20/20 0 issues.
…wText fallback, A-class SIZE_FROM_JAVA_BOUNDS removal, delete dead GuidePrimitiveCollector

T0: LytHeading overrides computePrimitives (FillRect separator, geometry/colors match legacy render)
T1: LytParagraph usePrimitives=!flowContent.isEmpty + emitTextFallback via GuideText.emitText; GuideScreen pageTitle routed through PrimitiveCollector+Engine
T4: A-class blocks declare sizes (GuiSprite excluded from flag, inline FloatAbs reads declared sizes, buildLatexData uses formulaDisplayW/H)
T7a: GuidePrimitiveCollector.java deleted (zero references)
Gate: compileJava compileTestJava test runLayoutDump green, TOTAL ISSUES: 0
… grid row overflow

Root cause: Rust measure_mediawiki_special_generated hardcoded TITLE/SUBTITLE_LINE_HEIGHT=10.0px (measure.rs) while Java renders/relayouts at GuideText.lineHeight=17px, so node_type=30 box heights were systematically ~1/3 too small and the last grid row overflowed the box bottom (Special Index 'floating text', in-game too). Fix (diagnostician option A): fbs append-only link_line_height/subtitle_line_height (default 10.0 = old-data compatible); collectFontFactsImpl serializes GuideText.lineHeight(LINK/SUBTITLE_STYLE); Rust measure consumes serialized values, wrong constants deleted. Verified: Categories block h 237->335, all 6 rows inside box (ink below bottom 0); blocks without subtitles unchanged; gate TOTAL ISSUES 0; ratchet 33/33; reviewer ACCEPT (anti-pattern 4/4 clean). Earlier dead-path Java estimate unification stashed (superseded).
…emLink

Inline item icons used a uniform 16px cell with zero text gap — icons
appeared glued to following text (P6) regardless of actual sprite ink.

- New IconMetrics: lazy static cache (item:meta), multi-pass merge,
  alpha>8 ink box; primary path getFrameTextureData, PNG resource
  fallback since runtime CPU frames are cleared after atlas upload
  (deviation: fallback added, all-fail -> null -> legacy 16px cell)
- LytItemImage: tight advance for label-less inline path
  round(inkW*scale)+2*PAD (PAD=2) + draw offset -inkLeft*scale+PAD;
  label/block paths untouched

Width evidence (headless bounds): crafting_table 16->20, compass 16->18,
apple 32->28, diamond 64->52, emerald 64->48, gold_ingot 64->68;
game-tags widths [28,62,72,105,96,64,20,18] all distinct.
Visual gate TOTAL ISSUES 0, ratchet 33/33.
…t icon tofu

ⓘ✦➤⚠☢ (U+24D8/2726/27A4/26A0/2622, Common script) are absent from
msyh.ttc and have no system fallback path (fontique FallbackMap only
tracks 7 scripts; the parley Hani-hack lands on msyh/SimSun which also
lack them) -> .notdef tofu boxes in callout titles.

- Rust: ParleyFonts::load_fallback_font_data registers fallback bytes,
  primes the system Hani fallback cache, then append_fallbacks (append,
  never set — preserves CJK fallback order); GuideFontSystem delegate;
  new JNI LayoutBridge_loadFallbackFont (init signature untouched)
- Java: FontProvider default getFallbackFontData (empty); SystemFontProvider
  resolves seguisym.ttf (Windows) / Apple Symbols / DejaVu Sans candidates;
  call sites in GuideScreen.ensureLayout + RenderPageService.ensureFontEngineReady
  after init, before first layout; empty-skip + UnsatisfiedLinkError guard
  with logged warning (stale native lib tolerant)

Evidence: fallback font load log (2514056 bytes seguisym); alert title
run measured widths shift +1..3px (tofu -> real glyph); layout JSON
byte-identical with/without fallback (no primary-glyph theft);
geometric 0 findings, ratchet 33/33.

Known deviation (executor-adjudicated): pre-existing identity-hash
iteration-order fragility in guide layout — any Java-side call on the
font init path deterministically flips some layout decisions (2-4px
reflows, mermaid canvas arrangement); old build mermaid pages were
already run-nondeterministic. Reflow-only, glyphs identical. Logged as
tech debt for follow-up.
…la glyphs

Two layered causes made inline formulas render far below body x-height
(P1: '公式字体过小 + 基线未对齐'):

1. jlatexmath TeXIcon.setInsets(Insets) single-arg silently ADDS
   (int)(0.18f*size) per side — at size=100 the real inset is 20px/side,
   not the 2px every comment assumed. ~40px of vertical padding was
   baked into each texture, so the calibrated 'x-height' box was half
   padding: measured $x$ ink was 0.56x body x-height.
2. depth anchoring double-rounded (ceil source px, then round scaled),
   drifting the math baseline 1-2px.

Round 1: INLINE_PERCEPTUAL_FACTOR=1.2f (TeX lowercase letters read small
next to CJK body text at exact x-height parity); GuideLatexRenderer
exposes TeXIcon.getBaseLine() exact ratio (baselineRatioCache +
measureBaselineRatio); LytLatexBlock computes display depth in a single
round; dead sourceDepthPx/LATEX_BOTTOM_INSET_PX removed.

Round 2 (root fix): all 4 setInsets sites switched to the two-arg
setInsets(insets, true) so insets are the intended 2px/side; javadocs
document the single-arg pitfall. LytLatexDisplayBlock legacyPaddingDiff
re-adds the removed phantom padding (2*(int)(0.18f*sourceScale)) to both
size and refH — an algebraic identity (Fc+40)/(Xc+40) keeping the
ratcheted display box ABI stable (11/11 display blocks 0% deviation)
while textures now carry real ink.

Evidence: $x$ ink 15px vs body x-height 12px = 1.25x (target 1.2);
display blocks byte-identical dims; assert_bounds 35/35 (2 new h-floor
assertions from measured values); geometric 0 findings; pixel diffs
confined to formula regions (mermaid/nei/tasks/content-tabs exempt —
known identity-hash nondeterminism).
ItemImage/ItemLink/FloatingImage/Image script error fallbacks render
their red LytParagraph.error segment inline but occupied zero layout
width: LayoutTreeSerializer's inline-block loop fell through to the
else branch reading getBounds()=LytRect.empty() -> FloatAbs(0,0) ->
Parley InlineBox width 0 -> pen never advanced, so the following body
text overlapped the error text (12+ real corpus pages, e.g.
alchemic_router 8 sibling intersections, electric_hoe 6).

Add a LytParagraph branch before else: declare FloatAbs from
GuideText.measureWidth (longest line) x lineHeight*lines (error
paragraphs are not PRE; multi-line covered). Empty text or failed
measurement falls back to the previous zero-width behavior with a
debug log. One fix covers all script error fallbacks; LytImage and
other dedicated branches untouched.

Evidence: alchemic_router follow-up text x=39->494, dispensers list
container w=26->322; geometric sibling_intersection 14->0 across the
5 affected pages; visualtest 23-page ratchet 35/35, geometric 0,
pixel-identical outside known-nondeterministic pages.
…dotted

Wavy and dotted underlines were rasterized by three hardcoded Java
copies (main Tessellator path, legacy drawRect, VanillaRenderContext)
drawing axis-aligned opaque 2x4px blocks per doc-pixel column — 8-phase
sine rounded to integer offsets, 3x3 hard squares for dots, zero
antialiasing (P4: '波浪线/下划虚线渲染太糙').

R1: new shared DecorationRasterizer (pure functions, no GL state) —
subpixel sine sampling at 0.25 doc px with per-column coverage alpha,
circular dot mask with soft edge falloff, run-merge for vertex bounding;
all three call sites converged to it (wave: 2 discrete levels -> 19
brightness levels; dots: hard squares -> 243 core with 104/42 edge
falloff). Geometry params unchanged (8px period, +/-2 amp, 2px thick,
4px pitch).

R2 (review REJECT fixes): GL scope (pushAttrib GL_ENABLE_BIT + disable
GL_ALPHA_TEST + try/finally pop) around wavy/dotted drawing on the two
legacy paths — tooltip chain (GuideScreen ContentTooltip ->
VanillaRenderContext) restores GL_GREATER 0.1 alpha test, which would
cull alpha<=25 soft-edge fragments; alpha compositing corrected from
coverage-replaces-tint-alpha to round(coverage*tintAlpha/255) (no-op at
tint=255, documented in rasterizer javadoc).

Evidence: 23/23 render ok, ratchet 35/35, geometric 0 findings; pixel
diffs only in decorations + lists_tasks ^^wave^^ band (R1) and none on
R2 (decorations page zero-diff vs R1 product); mermaid edges verified
not routed through decoration kinds 4/5.
Real corpus uses FloatingImage with only width (or only height) on many
pages (toolbox, ore_stats, angelica, amount, fether, modern_tab,
baubles, coke_oven, naq_reactor...), but the compiler hard-rejected it
at parse time ('requires width or w, and height or h') — red error in
game and headless alike.

Single-dimension now means display-size semantics: full image (no
crop), given dimension = display px * scale, missing dimension inferred
from the natural aspect ratio (inferred = given * natural_other /
natural_given, no extra scale on the inferred axis). Two-dimension
crop semantics unchanged; natural dims <= 0 falls back to the previous
error path (which now reserves layout width thanks to the earlier
inline-error fix).

- FloatingImageCompiler: CropSpec has flags; exactly-one-dimension
  legal, both-missing still errors with the original message
- Rust measure_image: aspect inference with divide-by-zero guard
- Java mirrors (LayoutTreeSerializer inline LytImage branch,
  LytImage.computeLayout) carry the identical formula
- New fixture visualtest/images/single-param.md (width-only + height-only)
  + 3 ratchet assertions (38 total)

Evidence: 10 real-corpus images inferred within 1px rounding of natural
aspect; red error gone from render logs; images actually render (PIL
non-background content); floating-image.md 11 two-dimension blocks
pixel-exact (crop semantics untouched); 24/24 render ok, ratchet 38/38,
geometric 0, pixel-identical outside known-nondeterministic pages.
…or channel

Headless real-corpus renders showed scene viewports silently black
(subnetworks, crops_automation). Static hypothesis (modern SNBT dialect
unsupported) was falsified by probes: all corpus structures use the
legacy palette/blocks dialect and placed fine (76/90 blocks). The real
root cause was camera geometry:

- Explicit rotation center (centerY={-15}) skipped the auto-centering
  translation, projecting the structure to screen y ~ -595 (offscreen)
- Ponder keyframe and GameScene offsets (offX/offY) were applied as
  world units though documented/implemented everywhere else as screen
  pixels (SceneTagCompiler comment, auto-pan writes screen px, ponder
  drag accumulates screen px): -75/-90 * 10x zoom threw the structure
  far out of view

Fixes:
- SceneScript.finalizeSceneGeometry: explicit-center scenes also get
  structure centering (level center); offsets unified to screen px with
  world conversion via s = 0.625*16*zoom = 10x zoom; dead center[] code
  removed (explicit-center handled in applyCameraAndViewport)
- LytGuidebookScene: ponder camera offset unit unification; build()
  errorSink=null replaced with a real scene error channel —
  sceneBuildError recorded, warnAlways logged, drawSceneBuildError
  draws red text; empty-level scenes get explicit feedback instead of
  silent background (never silent black again)
- ImportStructureElementCompiler.placeStructure: modern dialect parsing
  (string palette[] + data layers + string state) added alongside the
  legacy path (zero behavior change, forward-compatible)
- wiki scene-camera.md (en+zh): stale 'units: blocks' corrected to
  screen-space pixels + migration note (deployed resourcepack1 snapshot
  intentionally untouched — follow-up redeploy item)

Evidence: subnetworks scene ROI non-background 7.13%->32.15%,
crops_automation 16.89%->23.56%, camera.md offset scene 9.68%->40.15%,
spatial-io offset scene 2.35%->9.75%; scenes/import fixture stable
(37.2%); 25/25 render ok, ratchet 38/38, geometric 0, pixel-identical
outside known-nondeterministic pages; 8-file affected-offset inventory
recorded.
…mpletes

MediaWikiSpecialDataIndex warms up asynchronously on a daemon executor,
but the headless batch renderer started immediately — alphabetically
early Special pages (data-index driven) rendered as 96px empty fallback
(13 victim pages in the 408-page real-corpus batch; warmup completed
1619ms AFTER page 12 had already rendered).

GuideNhHeadlessRenderDriver.handleWorldStable now awaits warmup
readiness before entering RENDERING (shared by batch and single-page
modes): polls until specialDataIndex() is no longer the EMPTY singleton
(reference comparison — fallback always uses the singleton, completed
warmup installs a fresh instance), 25ms interval, 30s cap with warning,
InterruptedException restores the interrupt flag. In-game path
untouched (warmup long done before a user opens a page); the ~1s wait
applies per headless render run regardless of MediaWiki content
(documented minor cost).

Evidence: changelog allPages 195/195 ok; all 13 race victims restored
(allpages 48->954, categorytree 48->4521, doubleredirects 48->6078,
globalfileusage 48->6231; 3 static pages unchanged as expected); log
order 'warmup complete (964ms)' precedes 'Batch render start';
visualtest 25/25, ratchet 38/38, geometric 0, pixel diffs only in
known-nondeterministic pages.
Map.copyOf produces JDK ImmutableCollections.MapN, which salts its hash
table with System.nanoTime() per JVM — iteration order differs on every
run. getNodes() order feeds both computeNodeMinSizes and the ELK
layered layout's node creation order; ELK's crossing minimization keeps
input order among equal solutions, so flowchart node placement drifted
run-to-run (and any change to class-init/allocation order, e.g. an
empty method call on the font init path, deterministically flipped some
layouts — the P5-B anomaly).

FlowchartDocument now wraps a LinkedHashMap (parse insertion order)
with Collections.unmodifiableMap — same immutability, deterministic
iteration. Diagnosis table: 6 other collections (IdentityHashMap
caches, Integer-keyed HashMaps, List.copyOf) verified harmless
(lookup-only or order-stable).

Evidence: 25 pages rendered twice in independent JVM runs — 24/25
pixel-identical including ALL mermaid pages, tasks, content-tabs (the
exemption list shrinks to nei_recipes only: its residual 1344px lives
in the external GTNH sprite-atlas registration order, unreachable from
this project); ratchet 38/38; geometric 0; text pages zero diff vs
pre-fix baseline (mermaid pages allowed one-time baseline migration).
GuideNavBar was the last GUI chrome island on MC FontRenderer bitmap
fonts (user directive: sidebar integration). All text now goes through
the GuideText/Rust pipeline following the drawPageTitle precedent:
title, row titles, and hover-scroll titles are emitted via
GuideText.emitText into a PrimitiveCollector and executed once at the
end of render() (legacy rects/icons/scissor draw first; execute()
restores GL state and disables scissor so it must come last).

- Row style fontScale 0.70 (lineHeight 12 == ROW_H 12), title 0.80
  (lineHeight 14 in TITLE_H 16); vertical centering derived from
  lineHeight+ascent (measured ascent 12.0; row baseline rowY+8.4, CJK
  ink verified within [2,9] of the 12px row)
- Colors via ConstantColor from getRowTextColor, shadow=false, matching
  previous drawString semantics; truncation via clipToWidth with
  ellipsis counted in the width budget
- Hover smooth scroll preserved with pushScreenScissor/popScreenScissor
  primitives (single execute kept)
- FontRenderer path retained as the GuideText.isAvailable()==false
  fallback (init-failure window) for every text element
- executeNavText does an explicit beginFrame with DisplayScale.scaleFactor
  and clears stale body scissor before executing

GuideScreen untouched; GuideNavProjection/State/GuidebookText
untouched; mini item icons unchanged (not text). Headless page renders
do not include GUI chrome, so runtime verification here is compile +
static review; a dedicated headless chrome pass (S2) follows for pixel
evidence. Regression: 25/25 render ok, ratchet 38/38, geometric 0,
24/25 pixel-identical (nei_recipes exempt — external atlas
nondeterminism).

Known follow-ups (reviewer-noted, non-blocking): sticky-row text
z-order in scrolled state now composites above sticky rects (single
execute consequence); navbar font metrics are msyh-measured.
…ication

The S1 navbar migration had no runtime coverage: headless renders only
collect document primitives, never the GUI chrome. Add an opt-in
-Dguidenh.renderpage.chrome=true switch (build.gradle.kts forwards the
key) that appends a navbar primitive pass:

- RenderPageService: chrome=true renders the document independently
  (byte-identical path to chrome=false), then collects navbar
  primitives and composes side-by-side (nav left 162 logical px
  mirroring resolveNavigationOpenWidth 18%/min-110, document shifted
  right; output width width+navW; document bounds JSON untouched)
- GuideNavBar.collectPrimitives: headless-friendly collection entry
  reusing the S1 text path (renderRowTitle -> GuideText.emitText),
  geometry as GradientFill/FillRect primitives, body clip via
  document-space pushScissor (scales with offscreen density); live
  render() untouched
- GuideNhHeadlessRenderDriver: chrome config parse + passthrough +
  startup log

Verified with the canonical gradlew -D command (no JAVA_TOOL_OPTIONS):
chrome pass composed 79 nav primitives into 2124x1622 output; navbar
band + separator present; text ink smoothly antialiased (vector glyphs,
not bitmap); row pitch 24px at scale 2 == ROW_H 12; row-by-row
projection log matches; chrome-vs-plain same-page document bounds 78
blocks byte-identical; chrome=false regression 25/25, ratchet 38/38,
geometric 0, 24/25 pixel-identical (nei exempt).

Tech debt (reviewer-noted): >100 lines of mirrored geometry between
collectPrimitives and render() (arrow quads, row highlight colors) —
share a geometry data source in a follow-up.
…gression

Coverage-matrix gaps closed with 9 new fixture pages (syntax verified
against each tag compiler, positions collision-free):
- P0 zero-coverage: tags/block-stats.md (visible/buttonEnabled — the
  only wired attrs), tags/recipes-usage.md, tags/quest-link.md,
  mdx/file-tree.md, text/mark.md
- P1 thin-coverage: text/item-link-dense.md (dense ItemLink mixes),
  text/color-named.md (16 symbolic colors; 3-digit hex confirmed
  malformed by the parser, 6-digit used), layout/callout-combos.md
- F-N2 regression lock: text/error-fallback.md — ItemLink/ItemImage to
  nonexistent items followed by body text; ratchet pins all paragraphs
  w>=200 (zero-width collapse would fail); blind spot (error node
  disappearing entirely) documented in the fixture INVARIANTS

17 new ratchet assertions appended by hand (38 -> 55, strictly
append-only, zero modifications to existing entries).

34/34 render ok, 55/55 assertions ok, geometric 0 findings, pixel
regression 24/25 zero-diff (nei_recipes exempt). Documented engine
behaviors found during authoring: [!NOTE] marker is swallowed by the
parser (alert detection keys on body first word); title=/color-only
quote directives fall through to the plain blockquote path (LytVBox,
no header row) — fixture prose records actual behavior.
…rity

Follow-ups bringing the static site export in line with the in-game
pipeline:
- GuideSiteLatexExporter: setInsets switched to the two-arg form (the
  single-arg overload silently adds (int)(0.18f*size) per side — 20px
  instead of 2px at size 100), same fix as the runtime P1 change;
  pitfall documented in comments
- GuideSiteHtmlCompiler FloatingImage: single-dimension usage now
  legal (matches runtime F-N1): x/y optional (default 0), at least one
  of width/height required, missing axis left to the browser's
  intrinsic aspect ratio (export side cannot read PNG bytes —
  ImageResolver only returns URLs; class-less inline-style structure
  avoids app.js layoutCroppedFloatingImage overriding the given
  dimension); both-missing still errors with the runtime-aligned
  message; malformed/non-positive dimensions now error by attribute
  PRESENCE (hasText) instead of silently falling back to single-param
  mode ('must be an integer' / 'must be positive' texts aligned with
  FloatingImageCompiler); two-dimension crop path output unchanged
- wiki Images-And-Assets + Tags-Reference (en/zh): parameter contract
  updated to the new semantics

Verified: compile ok; probe 13 cases (malformed/zero/valid-single/
both-missing/alias-conflict) mismatches=0; headless Chromium bboxes
128x32 / 32x128 match fixture invariants; 34/34 render, 55/55 ratchet,
geometric 0, pixel diffs only in nei_recipes (exempt). Known limits:
single-param regional ImageAnnotations are not laid out by site JS
(app.js selector mismatch, documented); no headless export entry
exists (probe-driven verification instead).
…ng closure

X.1 P1-P6 all closed (latex insets/perceptual factor, IconMetrics optical
advance, special-index line heights, DecorationRasterizer AA, fallback fonts)
X.2 F-N1/N2/N3 closed (single-dim images, error-fallback width, scene camera)
X.3 infra: warmup race, MapN identity-hash, SideBar S1+S2 chrome pass
X.4 9 adjudications: zero real bugs, homoglyph-run FP pattern registered
X.5 follow-ups ledger: redeploy, navbar tech debt, deferred items, ratchet 55
INLINE_OPTICAL_PAD 2->0 (shared across computeContentSize/computeLayout/
renderX); computeLayout no-label inline branch aligned to tightW, removing
the hidden 16px-vs-tightW advance inconsistency. Post-kerning gap on block
icon rows eliminated (leading/trailingCell 3->1, cell 16).
P5-C: register seguisym as leading generic Emoji family so warning/caution
glyphs (emoji-classified by parley) resolve to seguisym instead of Segoe UI
Emoji COLR composites that collapse to fragments at 22px.
F4: rasterize_out_glyphs clamps oversize raw_size to MAX_RASTER_SIZE=128
(was flooding the glyph atlas with 966-1517ppem bitmaps); non-finite or
<=0 raw_size (fontScale=0 injection) falls back to DEFAULT_RASTER_SIZE=22
instead of rendering raw outline coordinates. Diagnostic WARN retained.
…l injection

Ghosting root (live): regular-row y (scrolls) vs sticky-row y (pinned near
body top) used two independent y sources with no screen-space avoidance;
scrolled rows passing through sticky bands drew same-position text. Add
overlapsStickyRow band-intersection skip in both mirror loops (live render
and headless collectPrimitives), stickyRowY Math.max(bodyY,...) lower bound
(closes sticky z-order item). Headless scrollY injection via
-Dguidenh.renderpage.navscroll (default byte-identical) enables scrolled-
state regression renders; build.gradle.kts forwards the property.
drawPageTitle titleX lacked the navbar reserved width (59cd3e2 predates
sidebar integration) - title rendered inside the navbar column in live.
titleX now adds getNavigationReservedWidth() and availableW subtracts it
(right edge unchanged); mirrors recomputePanelBounds baseContentX.
…escape

F7b-2/3: Block Stats name/count and load-state text migrated from MC pixel
font (VanillaRenderContext.drawString) to engine vector font via
GuideText.emitText in collectPrimitives; legacy drawText gated by suppress
flags mirroring F7b-1; layout geometry extracted to context-free shared
methods. Fixture: code/blocks.md L113 raw <>&"§ HTML-entity escaped
(no longer triggers MDX parse error; heading content unchanged).
Lightweight whole-image query (--files + --prompt, free-text answer plus
best-effort findings extraction) complementing vlm coarse screening; model
name docs corrected to qwen3-vl-plus.
…olchain

X.6: six-report fix wave 1-3 + regression closure. X.7: user re-verification
(1 title NOT-FIXED, ItemLink kerning NOT-FIXED, Latex stretch SUSPECTED).
F4: glyph-atlas flood root cause (fontScale=0 -> size=0 bypassed clamp ->
outline-space bitmaps), bleed-stop + fallback fix, zoom-collapse root
(visualZoom uninitialized). PF34: parallel background agents x headless
renders = native OOM - render is a shared resource, lock protocol.
WORKFLOW: ask toolchain reference.
…fect fixes

T1: -Dguidenh.renderpage.mermaidzoom/mermaidoffset injection (RenderPageService
applyMermaidInjection -> setHeadlessInjection, HEADLESS branch honors injection
else historical fitZoom byte-identical); updateVisualState() per-frame driver at
computePrimitives entry (raw zoom/offset -> visual values, wheel-zoom + drag now
render); scaled-style cache invalidated on zoom change (fontScale follows zoom).
Wave: A edge-label wrap budget = edge path length (was node gap 20px -> per-char
vertical stack; R3-13 no-overlap preserved); B shape minNodeRect/contentBounds
zoom-consistent (NODE_SIZE_ROUNDING_MARGIN, contentBounds(zoom) across 14 shapes,
SubprocessShape frame scaled, Cloud/Circle ceil); C canvas explicit width clamped
to available page width (100-node canvas 1709->480, dead-code clamp activated).
…ature pattern 1)

Paging: MAX_PAGES=8 x 2048^2 (16MB/page, 128MB cap; was single 2048^2 drop-new-
on-full) - 44ppem glyphs at render_scale=4 (6763-hanzi stress page + residue)
previously overflowed 4 pages -> silent whole-page LRU eviction -> stored-run
glyph loss. LRU per-page eviction (access-stamped, glDeleteTextures, never full
clear - C-3 red line); failure set (structurally-unplaceable keys silent + one
WARN, cleared on eviction); eviction WARN 1s-throttled (sentinel overflow fixed).
Render: DrawGlyphRun atlasId removed, per-key page resolve + flush/rebind;
tessColor-after-startDrawing regression fixed. Zoom quantization: getActiveZoom
-> nearest 1.1^n (interactive/injected; no-inject fit path exempt, byte-identical
md5 preserved) - animation no longer re-shapes/re-uploads per frame (jitter root
removed). guiscale headless injection added (capped headless, registered).
…ics) + headless channels

X.10 dual-defect fix: y overflow (HEADING1 line 26px in 16px toolbar -> painted
over content; now TOOLBAR_TITLE_STYLE fontScale 0.8, line 14, in-band) and x
alignment; then user verdict: plain title-bar semantics - title at toolbar left
edge (panelX+PANEL_PADDING, no content-column offset), navbar confirmed below
toolbar (navY=panelY+TOOLBAR_H+1) so no overlap; availableW no longer subtracts
navReservedWidth. Headless channels: -Dguidenh.renderpage.title (drawPageTitle-
equivalent path, default strict no-op, byte-identical md5) + guiscale injection;
gradle forwards title/guiscale. Verified: title bbox x=8.0-8.5 (bearing), bottom
15.5<16, zero-change md5 e8430e19/1ac40ee4; reviewer double-pass ACCEPT.
…strations

mermaid/ directions shapes links deep-subgraphs stress-large mindmap-variants
rich-text node-content-tags mixed-multi (all non-stub rendered); stress/stress-
glyphs.md (GB2312 6763 hanzi + 6985 unique chars + mermaid) - paging regression
carrier. ISSUES.md X.8 (mermaid wave), X.9 (atlas paging), X.10 (title + capacity/
jitter), X.11 (typesetting red lines), X.12 (title fine-tune awaiting multimodal).
@Windorain
Windorain marked this pull request as draft August 7, 2026 16:08
@Windorain Windorain changed the title Refractor layout and render system [WIP]: Refractor layout and render system Aug 7, 2026
…ine)

display calibration merged into inline standard: displayScaleFactor =
xHeight x 1.2 x userScale / (refH-4), W/H share one ratio (no stretch);
legacyPaddingDiff removed (was compensating pre-fix baseline); constants
promoted package-private, inline logic untouched. display/inline same-
formula ratio 0.93 (1px = style icon diff), inline anchor byte-identical,
latex ratchet 6/6. Site export kept (site formulas already shared).
Per-glyph cumulative advance compensation in layout emission (mature
synthetic-italic practice, equivalent to true-italic hmtx): x'_i = x_i +
sum_{j<i} K*(baseY - y_top_j), baseY = run max(q.y+q.h) same basis as
engine GLYPH_SHEAR_K=0.25 (cross-referenced); line-boundary reset;
SpanStyle.italic + measure passthrough; single-style italic paragraphs now
shear+compensate (base_italic, was always false). Upright zero-change
verified (md5 + JSON byte-identical); overhang eliminated (per-glyph shift
matches formula exactly). DLL rebuilt (3004f893).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants