fix: correct viewport stretch and SHX plots

Anchor the opposite viewport corner during grip and STRETCH resizing.
Match plotted stroke-font pen widths to the SDF atlas unless CTB overrides.

Closes #435
This commit is contained in:
Hakan Seven 2026-07-24 19:26:10 +03:00
commit ece5598372
4 changed files with 127 additions and 26 deletions

View file

@ -1677,6 +1677,10 @@ impl OpenCADStudio {
stretched = true;
}
}
acadrust::EntityType::Viewport(vp) => {
stretched =
crate::entities::viewport::stretch(vp, win_min, win_max, delta);
}
acadrust::EntityType::Dimension(dim) => {
use acadrust::entities::Dimension;
// Move every definition point that falls inside the

View file

@ -351,24 +351,96 @@ fn apply_grip(vp: &mut Viewport, grip_id: usize, apply: GripApply) {
vp.center.z = p.z as f64;
}
(1..=4, GripApply::Absolute(p)) => {
let new_hw = (p.x as f64 - vp.center.x).abs();
let new_hh = (p.y as f64 - vp.center.y).abs();
if new_hw > 0.01 {
vp.width = new_hw * 2.0;
}
if new_hh > 0.01 {
let new_h = new_hh * 2.0;
// Keep scale constant: view_height must scale with the viewport height.
if vp.height > 1e-9 && vp.view_height.abs() > 1e-9 {
vp.view_height = vp.view_height * (new_h / vp.height);
}
vp.height = new_h;
}
let hw = vp.width * 0.5;
let hh = vp.height * 0.5;
let opposite = match grip_id {
1 => (vp.center.x - hw, vp.center.y - hh),
2 => (vp.center.x + hw, vp.center.y - hh),
3 => (vp.center.x + hw, vp.center.y + hh),
4 => (vp.center.x - hw, vp.center.y + hh),
_ => unreachable!(),
};
resize_from_corners(vp, opposite, (p.x as f64, p.y as f64));
}
_ => {}
}
}
/// Stretch a rectangular paper-space viewport by the corners captured by a
/// crossing window. Bounds that have no captured corner stay anchored.
pub fn stretch(
vp: &mut Viewport,
win_min: glam::DVec3,
win_max: glam::DVec3,
delta: glam::DVec3,
) -> bool {
let hw = vp.width * 0.5;
let hh = vp.height * 0.5;
let (left, right) = (vp.center.x - hw, vp.center.x + hw);
let (bottom, top) = (vp.center.y - hh, vp.center.y + hh);
let inside =
|x: f64, y: f64| x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y;
let bottom_left = inside(left, bottom);
let bottom_right = inside(right, bottom);
let top_left = inside(left, top);
let top_right = inside(right, top);
if !(bottom_left || bottom_right || top_left || top_right) {
return false;
}
let moved_left = if bottom_left || top_left {
left + delta.x
} else {
left
};
let moved_right = if bottom_right || top_right {
right + delta.x
} else {
right
};
let moved_bottom = if bottom_left || bottom_right {
bottom + delta.y
} else {
bottom
};
let moved_top = if top_left || top_right {
top + delta.y
} else {
top
};
resize_from_corners(vp, (moved_left, moved_bottom), (moved_right, moved_top))
}
/// Rebuild center and size from two opposite corners while preserving viewport
/// scale as its paper-space height changes.
fn resize_from_corners(vp: &mut Viewport, a: (f64, f64), b: (f64, f64)) -> bool {
let new_width = (b.0 - a.0).abs();
let new_height = (b.1 - a.1).abs();
let mut changed = false;
if new_width > 0.01 {
let new_center_x = (a.0 + b.0) * 0.5;
changed |=
(vp.center.x - new_center_x).abs() > 1e-12 || (vp.width - new_width).abs() > 1e-12;
vp.center.x = new_center_x;
vp.width = new_width;
}
if new_height > 0.01 {
let old_height = vp.height;
let new_center_y = (a.1 + b.1) * 0.5;
changed |=
(vp.center.y - new_center_y).abs() > 1e-12 || (old_height - new_height).abs() > 1e-12;
vp.center.y = new_center_y;
if old_height > 1e-9 && vp.view_height.abs() > 1e-9 {
vp.view_height *= new_height / old_height;
}
vp.height = new_height;
}
changed
}
fn apply_transform(vp: &mut Viewport, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(vp, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(

View file

@ -635,9 +635,6 @@ fn emit_text(
.map(|mm| (mm * MM_TO_PT).max(0.1) / scale.max(1e-6));
}
}
let lw_pt = lw_override
.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1) / scale.max(1e-6));
let mut gi = 0;
while gi + 6 <= verts.len() {
let quad = &verts[gi..gi + 6];
@ -703,13 +700,33 @@ fn emit_text(
});
}
} else {
// Stroke (LFF pen) font or hollow glyph: polylines. Bold bakes
// at a 1.7× pen over the same centrelines, so widen to match or
// a bold run prints at regular weight.
// Stroke (LFF/SHX pen) font or hollow glyph: polylines.
// Match the SDF atlas' nominal glyph-space pen instead of
// borrowing the entity lineweight: Roman Duplex and similar
// multi-stroke faces rely on that band to close the narrow
// gaps between parallel centrelines. An explicit CTB
// lineweight still wins and stays absolute under the plot CTM.
ops.push(Op::SetOutlineColor {
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
col: Color::Rgb(Rgb {
r,
g,
b,
icc_profile: None,
}),
});
let pen = if ge.bold { lw_pt * 1.7 } else { lw_pt };
let pen = if let Some(ctb_pen) = lw_override {
if ge.bold {
ctb_pen * 1.7
} else {
ctb_pen
}
} else {
let glyph_unit_mm = (((tl[0] - bl[0]).powi(2) + (tl[1] - bl[1]).powi(2))
.sqrt()
/ sy.abs() as f64) as f32;
(2.0 * sdf_atlas::stroke_pen_half_units(ge.bold) * glyph_unit_mm * MM_TO_PT)
.max(0.1)
};
ops.push(Op::SetOutlineThickness { pt: Pt(pen) });
for stroke in &ge.strokes {
if stroke.len() < 2 {

View file

@ -105,6 +105,18 @@ const SPREAD_UNITS: f32 = 1.5;
/// built around. Scales with text height like the strokes themselves.
const PEN_HALF_UNITS: f32 = 0.35;
/// Nominal half-width of the SDF pen for a stroke glyph.
///
/// CPU exporters cannot sample the atlas, so they use this to stroke the same
/// LFF/SHX centrelines at the thickness shown on screen.
pub(crate) fn stroke_pen_half_units(bold: bool) -> f32 {
if bold {
PEN_HALF_UNITS * 1.7
} else {
PEN_HALF_UNITS
}
}
/// Upper bound on a single glyph tile's dimension (texels), a guard against a
/// pathologically wide/tall glyph blowing up the atlas.
const MAX_TILE_PX: u32 = 512;
@ -264,11 +276,7 @@ impl GlyphAtlas {
return *cached;
}
// Bold stroke glyphs bake with a wider pen band (same shapes, thicker).
let pen_half = if bold {
PEN_HALF_UNITS * 1.7
} else {
PEN_HALF_UNITS
};
let pen_half = stroke_pen_half_units(bold);
let face = Face::resolve(family);
let entry = face
.glyph(ch)