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:
parent
57ff14d3f3
commit
4479ba8c29
2 changed files with 66 additions and 11 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -3179,7 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytecount",
|
"bytecount",
|
||||||
"memchr 1.0.2",
|
"memchr 2.8.3",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
let collapsed = self.caret_on && self.sel.map(|(a, b)| a == b).unwrap_or(true);
|
||||||
if collapsed && self.boxes.is_empty() {
|
if collapsed && self.boxes.is_empty() {
|
||||||
// Empty text: show a caret at the top-left so the user can type.
|
// 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| {
|
let path = Path::new(|p| {
|
||||||
p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD));
|
p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD));
|
||||||
p.line_to(iced::Point::new(
|
p.line_to(iced::Point::new(
|
||||||
MTEXT_PREVIEW_PAD,
|
MTEXT_PREVIEW_PAD,
|
||||||
(MTEXT_PREVIEW_PAD + 22.0).min(self.content_h),
|
(MTEXT_PREVIEW_PAD + 40.0).min(self.content_h),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
|
|
@ -570,15 +572,58 @@ pub(super) fn mtext_editor_overlay<'a>(
|
||||||
maxx = maxx.max(b.xmax);
|
maxx = maxx.max(b.xmax);
|
||||||
maxy = maxy.max(b.ymax);
|
maxy = maxy.max(b.ymax);
|
||||||
}
|
}
|
||||||
let h_unit = ed.height_value() as f32;
|
// Normalize the display by the DOMINANT rendered text height — the run
|
||||||
// Real text size: fixed pixels per em so more/taller text grows the
|
// height the most glyphs share (the body) — NOT the entity's base height
|
||||||
// canvas (and scrolls) instead of shrinking to fit.
|
// field. That field can be many times the visible text: `\H…x;` runs
|
||||||
let scale = (22.0 / h_unit.max(1e-3)).clamp(2.0, 600.0);
|
// 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 {
|
let content_h = if maxx >= minx {
|
||||||
((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0)
|
((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0)
|
||||||
} else {
|
} else {
|
||||||
40.0
|
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 {
|
let prog = MTextPreview {
|
||||||
segments,
|
segments,
|
||||||
boxes: ed.glyph_boxes.clone(),
|
boxes: ed.glyph_boxes.clone(),
|
||||||
|
|
@ -591,11 +636,21 @@ pub(super) fn mtext_editor_overlay<'a>(
|
||||||
content_h,
|
content_h,
|
||||||
};
|
};
|
||||||
let cv = canvas(prog)
|
let cv = canvas(prog)
|
||||||
.width(Fill)
|
.width(iced::Length::Fixed(content_w))
|
||||||
.height(iced::Length::Fixed(content_h));
|
.height(iced::Length::Fixed(content_h));
|
||||||
// The preview fills the space the toolbars leave, so the resizable
|
// The preview fills the space the toolbars leave; it scrolls in BOTH
|
||||||
// modal's extra height goes to the text area (it scrolls if taller).
|
// directions so a taller-than-view text scrolls vertically and a
|
||||||
container(iced::widget::scrollable(cv).height(Fill))
|
// 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 {
|
.style(move |_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(FIELD_BG)),
|
background: Some(Background::Color(FIELD_BG)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue