From 34fa403a5d11a0597f95d08f18b41f14b7ad943e Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 04:44:43 +0800 Subject: [PATCH] fix(studio): fit default text to production font metrics --- apps/studio/src/lib/native-layout.ts | 7 +++++-- apps/studio/tests/native-layout.test.mjs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/lib/native-layout.ts b/apps/studio/src/lib/native-layout.ts index 558297b..0e8e08c 100644 --- a/apps/studio/src/lib/native-layout.ts +++ b/apps/studio/src/lib/native-layout.ts @@ -42,9 +42,12 @@ export function textSize(layout:Layout,slot:Slot,text:string):number { const [, ,w,h]=slot.box,lines=text.split('\n'); const length=(s:string)=>Array.from(s).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:/[A-Z]/.test(c)?.8:.6),0); if(slot.direction==='vertical')return Math.min(w*.75,h/Math.max(1,Array.from(text.replaceAll('\n','')).length)*.88); - if(slot.direction==='rotated')return Math.min(w*.85,h/Math.max(1,length(text))); + // Linux's fallback glyphs extend beyond the nominal vertical line box. + if(slot.direction==='rotated')return Math.min(w*.78,h/Math.max(1,length(text))); if(slot.direction==='curved')return Math.min(2.2,Math.max(w,h)*1.1/Math.max(1,length(text))); - const fitted=Math.min(h/(lines.length*1.15),w/Math.max(1,...lines.map(length)))*.92; + // Budget for fallback glyph ascent/descent, not just the CSS 1.2 line-height. + // The production Chromium/Noto stack needs about 1.33 em for a visible line. + const fitted=Math.min(h/(lines.length*1.4),w/Math.max(1,...lines.map(length)))*.92; return /微字|脚注|图注|标签/.test(slot.role)?Math.min(fitted,2.2):fitted; } export function imageShape(layout:Layout,slot:Slot):CSSProperties { diff --git a/apps/studio/tests/native-layout.test.mjs b/apps/studio/tests/native-layout.test.mjs index b4e3f29..300cc4b 100644 --- a/apps/studio/tests/native-layout.test.mjs +++ b/apps/studio/tests/native-layout.test.mjs @@ -17,3 +17,15 @@ test('geometric templates retain their distinct silhouettes without bitmap masks }); test('element IDs work on HTTP origins without secure-context randomUUID',()=>{const saved=Object.getOwnPropertyDescriptor(globalThis,'crypto');let calls=0;Object.defineProperty(globalThis,'crypto',{configurable:true,value:{getRandomValues(array){for(let i=0;i{ + const cases=[['ref-07','T02',97/73.7143],['ref-08','T01',112/85.2389],['ref-19','T01',287/217.055],['ref-19','T02',298/225.274],['ref-19','T03',313/237.451],['ref-19','T04',306/231.363],['ref-21','T01',195/160.307]]; + for(const [id,slotId,glyphExtent] of cases){ + const layout=layouts.find(l=>l.id===id),slot=layout.elements.find(s=>s.id===slotId); + const size=textSize(layout,slot,defaultText(layout,slot))*860/layout.width; + const crossAxis=slot.direction==='rotated'?slot.box[2]:slot.box[3]; + assert.ok(Math.ceil(size*glyphExtent)<=crossAxis*860/layout.width+2,`${id}/${slotId}: fallback glyphs must fit`); + } +});