fix(mtext): correct imported-text size in the editor (#394)

The editor preview mis-sized imported MTEXT badly (tiny, huge, or
collapsed). Two root causes:

- The entity's base height often isn't the visible text height: relative
  `\H…x;` codes compound, so an entity of height 1 whose body is `\H0.2x`
  renders at 0.2. Normalize the preview scale by the DOMINANT rendered run
  height (the mode of glyph-box heights, skipping zero-width caret slots),
  not `height_value()`, so the body lands at a fixed readable size and
  headings keep their proportions. EM_PX = 15.
- The bug that made it "completely wrong" was in the serializer, fixed in
  acadrust 389b31a (\H heights decayed on the parse→serialize→parse the
  editor does via folded_value); bumped here.

Also render multi-column MTEXT with its columns side by side (as the
drawing does) and let the preview scroll horizontally to reach them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-19 17:16:53 +03:00
1 parent 57ff14d3f3
commit 4479ba8c29
2 files changed
+66 -11

No files matched your search

Generated
+2 -2
View File
@@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#9d2e67fb7bfc054281641228e3543e8ac26d7f08"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#389b31ac5865e72f403d16d81eec958228065af0"
dependencies = [
"ahash 0.8.12",
"anyhow",
@@ -3179,7 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
dependencies = [
"bytecount",
"memchr 1.0.2",
"memchr 2.8.3",
"nom 8.0.0",
]
+64 -9
View File
@@ -261,11 +261,13 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
let collapsed = self.caret_on && self.sel.map(|(a, b)| a == b).unwrap_or(true);
if collapsed && self.boxes.is_empty() {
// Empty text: show a caret at the top-left so the user can type.
// Its height matches the preview's constant em (see EM_PX) so the
// empty-editor caret isn't tiny.
let path = Path::new(|p| {
p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD));
p.line_to(iced::Point::new(
MTEXT_PREVIEW_PAD,
(MTEXT_PREVIEW_PAD + 22.0).min(self.content_h),
(MTEXT_PREVIEW_PAD + 40.0).min(self.content_h),
));
});
frame.stroke(
@@ -570,15 +572,58 @@ pub(super) fn mtext_editor_overlay<'a>(
maxx = maxx.max(b.xmax);
maxy = maxy.max(b.ymax);
}
let h_unit = ed.height_value() as f32;
// Real text size: fixed pixels per em so more/taller text grows the
// canvas (and scrolls) instead of shrinking to fit.
let scale = (22.0 / h_unit.max(1e-3)).clamp(2.0, 600.0);
// Normalize the display by the DOMINANT rendered text height — the run
// height the most glyphs share (the body) — NOT the entity's base height
// field. That field can be many times the visible text: `\H…x;` runs
// compound, so an entity of height 1 whose body is `\H0.2x` renders at
// 0.2, and normalizing by 1 would shrink everything 5×. The mode lands
// the body at EM_PX while headings / fine print keep their proportions
// (one uniform factor, so no height is distorted). Zero-width boxes —
// the empty-line caret slots, which carry the raw entity height — are
// skipped so they can't skew it.
let h_unit = {
let mut heights: Vec<f32> = ed
.glyph_boxes
.iter()
.filter(|b| b.xmax - b.xmin > 1e-6)
.map(|b| b.ymax - b.ymin)
.filter(|h| h.is_finite() && *h > 1e-6)
.collect();
if heights.is_empty() {
ed.height_value() as f32
} else {
heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
// Largest cluster of near-equal heights (within 5%) = the body.
let (mut best_h, mut best_n, mut i) = (heights[0], 0usize, 0usize);
while i < heights.len() {
let mut j = i;
while j < heights.len() && heights[j] <= heights[i] * 1.05 {
j += 1;
}
if j - i > best_n {
best_n = j - i;
best_h = heights[(i + j - 1) / 2];
}
i = j;
}
best_h
}
};
const EM_PX: f32 = 15.0;
let scale = (EM_PX / h_unit.max(1e-6)).clamp(1e-4, 1e6);
let content_h = if maxx >= minx {
((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0)
} else {
40.0
};
// Multi-column MTEXT lays its columns out side by side, so the content
// can be wider than the editor; size the canvas to the real content
// width and let the scroll area pan horizontally to reach later columns.
let content_w = if maxx >= minx {
(maxx - minx) * scale + 2.0 * MTEXT_PREVIEW_PAD
} else {
40.0
};
let prog = MTextPreview {
segments,
boxes: ed.glyph_boxes.clone(),
@@ -591,11 +636,21 @@ pub(super) fn mtext_editor_overlay<'a>(
content_h,
};
let cv = canvas(prog)
.width(Fill)
.width(iced::Length::Fixed(content_w))
.height(iced::Length::Fixed(content_h));
// The preview fills the space the toolbars leave, so the resizable
// modal's extra height goes to the text area (it scrolls if taller).
container(iced::widget::scrollable(cv).height(Fill))
// The preview fills the space the toolbars leave; it scrolls in BOTH
// directions so a taller-than-view text scrolls vertically and a
// wider-than-view multi-column text scrolls horizontally.
use iced::widget::scrollable::{Direction, Scrollbar};
container(
iced::widget::scrollable(cv)
.direction(Direction::Both {
vertical: Scrollbar::default(),
horizontal: Scrollbar::default(),
})
.width(Fill)
.height(Fill),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(FIELD_BG)),
border: Border {