fix: correct paper-space grip projection and MTEXT attachment point rendering

- Grip points in paper space now use the 2-D linear to_px transform
  (tx/ty/ortho_size) instead of the 3-D view_proj matrix, matching the
  coordinate system used by PaperCanvas
- world_offset is no longer subtracted from paper-space entity grip
  positions — paper-space coordinates are already unoffset (same rule
  as wire tessellation in wires_for_block)
- MTEXT v_offset corrected for all attachment points: CXF glyphs sit
  on the baseline and extend upward, so Top attachment needs v_offset=-h,
  Bottom needs (n_lines-1)*line_h, Middle is the midpoint of the two

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-01 12:16:46 +03:00
commit 01943c5b8d
5 changed files with 93 additions and 11 deletions

View file

@ -244,7 +244,10 @@ impl H7CAD {
/// Rebuild the cached selected_grips from the current entity selection. /// Rebuild the cached selected_grips from the current entity selection.
pub(super) fn refresh_selected_grips(&mut self) { pub(super) fn refresh_selected_grips(&mut self) {
let i = self.active_tab; let i = self.active_tab;
let wo = self.tabs[i].scene.world_offset; let is_paper = self.tabs[i].scene.current_layout != "Model";
// Paper-space entity coordinates are NOT offset by world_offset (same rule
// as wire tessellation in wires_for_block). Only subtract in model space.
let wo = if is_paper { [0.0f64; 3] } else { self.tabs[i].scene.world_offset };
let (new_handle, new_grips) = { let (new_handle, new_grips) = {
let selected = self.tabs[i].scene.selected_entities(); let selected = self.tabs[i].scene.selected_entities();
if selected.len() == 1 { if selected.len() == 1 {

View file

@ -1,7 +1,7 @@
use super::{H7CAD, Message, POLY_START_DELAY_MS}; use super::{H7CAD, Message, POLY_START_DELAY_MS};
use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain, ucs_to_wcs, ucs_z_axis}; use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain, ucs_to_wcs, ucs_z_axis};
use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX}; use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
use crate::scene::grip::{find_hit_grip, GripEdit}; use crate::scene::grip::{find_hit_grip, find_hit_grip_paper, GripEdit};
use crate::scene::object::GripApply; use crate::scene::object::GripApply;
use crate::modules::ModuleEvent; use crate::modules::ModuleEvent;
use crate::ui::PropertiesPanel; use crate::ui::PropertiesPanel;
@ -1362,8 +1362,20 @@ impl H7CAD {
if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() { if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() {
if let Some(handle) = self.tabs[i].selected_handle { if let Some(handle) = self.tabs[i].selected_handle {
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let is_paper = self.tabs[i].scene.current_layout != "Model";
let grip_hit = find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds); let grip_hit = if is_paper {
let cam = self.tabs[i].scene.camera.borrow();
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
let half_h = cam.ortho_size();
let half_w = half_h * aspect;
let tx = cam.target.x;
let ty = cam.target.y;
drop(cam);
find_hit_grip_paper(p, &self.tabs[i].selected_grips, tx, ty, half_w, half_h, bounds)
} else {
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds)
};
if let Some((grip_id, is_translate, world)) = grip_hit { if let Some((grip_id, is_translate, world)) = grip_hit {
self.tabs[i].active_grip = Some(GripEdit { self.tabs[i].active_grip = Some(GripEdit {
handle, handle,

View file

@ -3,7 +3,7 @@ use super::document::DocumentTab;
use super::history::history_dropdown_labels; use super::history::history_dropdown_labels;
use super::helpers::grid_plane_from_camera; use super::helpers::grid_plane_from_camera;
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
use crate::scene::grip::grips_to_screen; use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
use crate::scene::paper_canvas::PaperCanvas; use crate::scene::paper_canvas::PaperCanvas;
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane}; use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
use crate::ui::overlay; use crate::ui::overlay;
@ -138,9 +138,21 @@ impl H7CAD {
let bounds = iced::Rectangle { let bounds = iced::Rectangle {
x: 0.0, y: 0.0, width: vw, height: vh, x: 0.0, y: 0.0, width: vw, height: vh,
}; };
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
let sel_h = tab.selected_handle; let sel_h = tab.selected_handle;
grips_to_screen(&tab.selected_grips, vp_mat, bounds) let screen_grips = if is_paper {
let cam = tab.scene.camera.borrow();
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
let half_h = cam.ortho_size();
let half_w = half_h * aspect;
let tx = cam.target.x;
let ty = cam.target.y;
drop(cam);
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
} else {
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
};
screen_grips
.into_iter() .into_iter()
.filter(|(_, screen, _, _)| { .filter(|(_, screen, _, _)| {
screen.x.is_finite() screen.x.is_finite()

View file

@ -103,15 +103,23 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity {
// distance, which is 5/3 × text_height (≈ 1.667). factor = 1.0 → single // distance, which is 5/3 × text_height (≈ 1.667). factor = 1.0 → single
// spacing, factor = 2.0 → double spacing, etc. // spacing, factor = 2.0 → double spacing, etc.
let line_h = t.height as f32 * ls_factor * (5.0 / 3.0) * font.line_spacing; let line_h = t.height as f32 * ls_factor * (5.0 / 3.0) * font.line_spacing;
let total_h = line_h * n_lines; let h = t.height as f32;
// CXF glyphs sit on the baseline (y=0) and extend UP by `h` (cap height).
// Block top = line-0 baseline + h
// Block bottom = last-line baseline = line-0 baseline − (n_lines−1)·line_h
//
// v_offset is the Y of line-0 baseline relative to the insertion point, so:
// Top attachment → block top at insertion → v_offset = −h
// Bottom attachment → block bottom at insertion → v_offset = (n_lines−1)·line_h
// Middle attachment → block center at insertion → midpoint of the two above
let v_offset = match t.attachment_point { let v_offset = match t.attachment_point {
AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => 0.0, AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => -h,
AttachmentPoint::MiddleLeft AttachmentPoint::MiddleLeft
| AttachmentPoint::MiddleCenter | AttachmentPoint::MiddleCenter
| AttachmentPoint::MiddleRight => -total_h * 0.5, | AttachmentPoint::MiddleRight => ((n_lines - 1.0) * line_h - h) * 0.5,
AttachmentPoint::BottomLeft AttachmentPoint::BottomLeft
| AttachmentPoint::BottomCenter | AttachmentPoint::BottomCenter
| AttachmentPoint::BottomRight => -total_h, | AttachmentPoint::BottomRight => (n_lines - 1.0) * line_h,
}; };
let h_anchor = match t.attachment_point { let h_anchor = match t.attachment_point {
AttachmentPoint::TopCenter AttachmentPoint::TopCenter

View file

@ -50,6 +50,53 @@ pub fn grips_to_screen(
.collect() .collect()
} }
/// Paper-space variant: project grips using the 2-D linear `to_px` transform.
/// Parameters match the `to_px` closure in `paper_canvas.rs`.
pub fn grips_to_screen_paper(
grips: &[GripDef],
tx: f32, ty: f32,
half_w: f32, half_h: f32,
bounds: Rectangle,
) -> Vec<(usize, Point, bool, GripShape)> {
grips
.iter()
.map(|g| {
let screen = Point::new(
(g.world.x - tx + half_w) / (2.0 * half_w) * bounds.width,
(ty + half_h - g.world.y) / (2.0 * half_h) * bounds.height,
);
(g.id, screen, g.is_midpoint, g.shape)
})
.collect()
}
/// Paper-space hit-test variant (mirrors `find_hit_grip` but uses 2-D projection).
pub fn find_hit_grip_paper(
cursor: Point,
grips: &[GripDef],
tx: f32, ty: f32,
half_w: f32, half_h: f32,
bounds: Rectangle,
) -> Option<(usize, bool, Vec3)> {
let mut best_dist = GRIP_THRESHOLD_PX;
let mut best: Option<(usize, bool, Vec3)> = None;
for g in grips {
let screen = Point::new(
(g.world.x - tx + half_w) / (2.0 * half_w) * bounds.width,
(ty + half_h - g.world.y) / (2.0 * half_h) * bounds.height,
);
let dx = screen.x - cursor.x;
let dy = screen.y - cursor.y;
let d = (dx * dx + dy * dy).sqrt();
if d < best_dist {
best_dist = d;
best = Some((g.id, g.is_midpoint, g.world));
}
}
best
}
/// Find the closest grip within `GRIP_THRESHOLD_PX` pixels of `cursor`. /// Find the closest grip within `GRIP_THRESHOLD_PX` pixels of `cursor`.
/// Returns `(grip_id, is_translate, world_pos)` if found, else `None`. /// Returns `(grip_id, is_translate, world_pos)` if found, else `None`.
pub fn find_hit_grip( pub fn find_hit_grip(