fix(layout): faithful paper-space round-trip and page setup (#156)
Builds on the acadrust viewport/PlotSettings round-trip fixes: - Write the sheet viewport's view position into view_center (DCS) with view_target at the origin, matching AutoCAD; the old view_target form shifted the layout. - PAGESETUP now writes paper size, rotation, origin and units onto the Layout's embedded PlotSettings (not just a side object), and bumps geometry, so an edit shows on the sheet immediately and survives a save. - Draw the printable-area guide (paper inset by plot margins, rotation-aware) as a dashed rectangle, matching AutoCAD's layout view. - Plot/PDF export falls back to the Layout's embedded PlotSettings via effective_plot_settings(), so a loaded file's rotation/origin/scale apply. Bumps acadrust to pick up the viewport-status and PlotSettings round-trip fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9d5ee75bcc
commit
ba834fb166
3 changed files with 148 additions and 30 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -70,7 +70,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
source = "git+https://github.com/HakanSeven12/acadrust?branch=main#3a1b66d7ec79fe576da465de48210d002dd52985"
|
source = "git+https://github.com/HakanSeven12/acadrust?branch=main#1b97dd3d52729ddf68ad6a542efab1eac3ebe9c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
||||||
|
|
@ -6461,7 +6461,13 @@ impl OpenCADStudio {
|
||||||
let rotation: i16 = self.page_setup_rotation.parse().unwrap_or(0);
|
let rotation: i16 = self.page_setup_rotation.parse().unwrap_or(0);
|
||||||
let scale_str = self.page_setup_scale.clone();
|
let scale_str = self.page_setup_scale.clone();
|
||||||
|
|
||||||
// Update the Layout object's limits.
|
// Update the Layout object's limits AND its embedded
|
||||||
|
// PlotSettings fields. `paper_limits()` (sheet rendering) and
|
||||||
|
// the DWG writer both read these from the Layout, so a page
|
||||||
|
// setup that only touched a side PlotSettings object would not
|
||||||
|
// reflect on screen or survive a save. The dialog's w/h are
|
||||||
|
// the final sheet dimensions, so store them verbatim with no
|
||||||
|
// further rotation swap (#156).
|
||||||
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
||||||
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||||
if l.name == layout_name {
|
if l.name == layout_name {
|
||||||
|
|
@ -6469,6 +6475,14 @@ impl OpenCADStudio {
|
||||||
l.max_limits = (w, h);
|
l.max_limits = (w, h);
|
||||||
l.min_extents = (0.0, 0.0, 0.0);
|
l.min_extents = (0.0, 0.0, 0.0);
|
||||||
l.max_extents = (w, h, 0.0);
|
l.max_extents = (w, h, 0.0);
|
||||||
|
l.paper_width = w;
|
||||||
|
l.paper_height = h;
|
||||||
|
l.plot_rotation = 0;
|
||||||
|
l.plot_paper_units = 1; // millimetres
|
||||||
|
l.plot_origin_x = offset_x;
|
||||||
|
l.plot_origin_y = offset_y;
|
||||||
|
// Custom dimensions no longer match a named size.
|
||||||
|
l.paper_size = String::new();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6542,6 +6556,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
// The paper sheet fill is cached; bump geometry so the new
|
||||||
|
// sheet size re-tessellates and shows immediately.
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
self.command_line.push_info(&format!(
|
self.command_line.push_info(&format!(
|
||||||
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
||||||
center={center} rot={rotation}°"
|
center={center} rot={rotation}°"
|
||||||
|
|
@ -6569,24 +6586,13 @@ impl OpenCADStudio {
|
||||||
Message::PlotExportPath(Some(path)) => {
|
Message::PlotExportPath(Some(path)) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let layout_name = scene.current_layout.clone();
|
|
||||||
let wires = scene.entity_wires();
|
let wires = scene.entity_wires();
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_canvas_hatches();
|
||||||
let wipeouts = scene.paper_canvas_wipeouts();
|
let wipeouts = scene.paper_canvas_wipeouts();
|
||||||
|
|
||||||
// Read PlotSettings for current layout (if available).
|
// Read PlotSettings for current layout (if available).
|
||||||
use acadrust::objects::{ObjectType, PlotType};
|
use acadrust::objects::PlotType;
|
||||||
let ps_snap = scene.document.objects.values().find_map(|obj| {
|
let ps_snap = scene.effective_plot_settings();
|
||||||
if let ObjectType::PlotSettings(ps) = obj {
|
|
||||||
if ps.page_name == layout_name {
|
|
||||||
Some(ps.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine paper size and drawing offset.
|
// Determine paper size and drawing offset.
|
||||||
let (paper_w, paper_h, mut draw_ox, mut draw_oy, rotation_deg) =
|
let (paper_w, paper_h, mut draw_ox, mut draw_oy, rotation_deg) =
|
||||||
|
|
@ -6691,22 +6697,11 @@ impl OpenCADStudio {
|
||||||
Message::PrintToPrinter => {
|
Message::PrintToPrinter => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let layout_name = scene.current_layout.clone();
|
|
||||||
let wires = scene.entity_wires();
|
let wires = scene.entity_wires();
|
||||||
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
||||||
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||||
use acadrust::objects::{ObjectType, PlotType};
|
use acadrust::objects::PlotType;
|
||||||
let ps_snap = scene.document.objects.values().find_map(|obj| {
|
let ps_snap = scene.effective_plot_settings();
|
||||||
if let ObjectType::PlotSettings(ps) = obj {
|
|
||||||
if ps.page_name == layout_name {
|
|
||||||
Some(ps.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let (paper_w, paper_h, draw_ox, draw_oy, rotation_deg) =
|
let (paper_w, paper_h, draw_ox, draw_oy, rotation_deg) =
|
||||||
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
||||||
let (pw, ph) = (x1 - x0, y1 - y0);
|
let (pw, ph) = (x1 - x0, y1 - y0);
|
||||||
|
|
|
||||||
127
src/scene/mod.rs
127
src/scene/mod.rs
|
|
@ -1435,6 +1435,119 @@ impl Scene {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dashed rectangle marking the printable area — the paper inset by the
|
||||||
|
/// layout's plot margins. AutoCAD draws this guide on every layout; with the
|
||||||
|
/// margins now preserved we can reflect it too. `None` in model space, when
|
||||||
|
/// the layout has no margins, or when the inset would be degenerate.
|
||||||
|
pub(super) fn printable_area_wire(&self) -> Option<WireModel> {
|
||||||
|
if self.current_layout == "Model" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ((x0, y0), (x1, y1)) = self.paper_limits()?;
|
||||||
|
let (left, bottom, right, top, rot) =
|
||||||
|
self.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::Layout(l) = obj {
|
||||||
|
if l.name == self.current_layout {
|
||||||
|
return Some((
|
||||||
|
l.plot_margin_left,
|
||||||
|
l.plot_margin_bottom,
|
||||||
|
l.plot_margin_right,
|
||||||
|
l.plot_margin_top,
|
||||||
|
l.plot_rotation,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
// `paper_limits()` already swaps the sheet for a 90°/270° rotation, so the
|
||||||
|
// margins must rotate to the same edges: a margin on a physical side moves
|
||||||
|
// to the displayed side that side rotates onto.
|
||||||
|
let (ml, mb, mr, mt) = match rot {
|
||||||
|
1 | 3 => (bottom, left, top, right),
|
||||||
|
2 => (right, top, left, bottom),
|
||||||
|
_ => (left, bottom, right, top),
|
||||||
|
};
|
||||||
|
// Nothing to show when there are no margins (printable area == sheet).
|
||||||
|
if ml <= 0.0 && mb <= 0.0 && mr <= 0.0 && mt <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (px0, py0, px1, py1) = (x0 + ml, y0 + mb, x1 - mr, y1 - mt);
|
||||||
|
if px1 - px0 < 1e-3 || py1 - py0 < 1e-3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (px0, py0, px1, py1) = (px0 as f32, py0 as f32, px1 as f32, py1 as f32);
|
||||||
|
let mut wire = WireModel::solid(
|
||||||
|
"paper_printable_area".to_string(),
|
||||||
|
vec![
|
||||||
|
[px0, py0, 0.0],
|
||||||
|
[px1, py0, 0.0],
|
||||||
|
[px1, py1, 0.0],
|
||||||
|
[px0, py1, 0.0],
|
||||||
|
[px0, py0, 0.0],
|
||||||
|
],
|
||||||
|
[0.5, 0.5, 0.5, 1.0],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// Dashed: 4 mm dash, 3 mm gap.
|
||||||
|
wire.pattern_length = 7.0;
|
||||||
|
wire.pattern = [4.0, -3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||||
|
Some(wire)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The effective plot settings for the current layout: a standalone
|
||||||
|
/// PlotSettings page setup if one exists, otherwise the settings embedded in
|
||||||
|
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
|
||||||
|
/// AutoCAD files keep their settings embedded, so without this fallback the
|
||||||
|
/// plot/PDF path would ignore the file's rotation, origin and scale.
|
||||||
|
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
||||||
|
use acadrust::objects::{
|
||||||
|
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
|
||||||
|
PlotWindow, ScaledType,
|
||||||
|
};
|
||||||
|
let name = &self.current_layout;
|
||||||
|
if let Some(ps) = self.document.objects.values().find_map(|o| {
|
||||||
|
if let ObjectType::PlotSettings(ps) = o {
|
||||||
|
if &ps.page_name == name {
|
||||||
|
return Some(ps.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}) {
|
||||||
|
return Some(ps);
|
||||||
|
}
|
||||||
|
self.document.objects.values().find_map(|o| {
|
||||||
|
let ObjectType::Layout(l) = o else { return None };
|
||||||
|
if &l.name != name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut ps = PlotSettings::new(l.name.clone());
|
||||||
|
ps.paper_width = l.paper_width;
|
||||||
|
ps.paper_height = l.paper_height;
|
||||||
|
ps.paper_size = l.paper_size.clone();
|
||||||
|
ps.margins = PaperMargin::new(
|
||||||
|
l.plot_margin_left,
|
||||||
|
l.plot_margin_bottom,
|
||||||
|
l.plot_margin_right,
|
||||||
|
l.plot_margin_top,
|
||||||
|
);
|
||||||
|
ps.origin_x = l.plot_origin_x;
|
||||||
|
ps.origin_y = l.plot_origin_y;
|
||||||
|
ps.plot_window = PlotWindow::new(
|
||||||
|
l.plot_window_min_x,
|
||||||
|
l.plot_window_min_y,
|
||||||
|
l.plot_window_max_x,
|
||||||
|
l.plot_window_max_y,
|
||||||
|
);
|
||||||
|
ps.paper_units = PlotPaperUnits::from_code(l.plot_paper_units);
|
||||||
|
ps.rotation = PlotRotation::from_code(l.plot_rotation);
|
||||||
|
ps.plot_type = PlotType::from_code(l.plot_type);
|
||||||
|
ps.scale_type = ScaledType::from_code(l.plot_scale_type);
|
||||||
|
ps.scale_numerator = l.plot_scale_numerator;
|
||||||
|
ps.scale_denominator = l.plot_scale_denominator;
|
||||||
|
Some(ps)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -1869,6 +1982,10 @@ impl Scene {
|
||||||
self.paper_bg_color
|
self.paper_bg_color
|
||||||
};
|
};
|
||||||
self.apply_refedit_fade(&mut wires, bg);
|
self.apply_refedit_fade(&mut wires, bg);
|
||||||
|
// Printable-area guide (paper inset by plot margins), paper space only.
|
||||||
|
if let Some(pa) = self.printable_area_wire() {
|
||||||
|
wires.push(pa);
|
||||||
|
}
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
*self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
*self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
|
|
@ -6333,8 +6450,14 @@ impl Scene {
|
||||||
|
|
||||||
if let Some(handle) = sheet_handle {
|
if let Some(handle) = sheet_handle {
|
||||||
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
||||||
vp.view_target = target_wcs;
|
// AutoCAD stores the paper-space view position in
|
||||||
vp.view_center = acadrust::types::Vector3::ZERO;
|
// `view_center` (DCS) with `view_target` at the origin —
|
||||||
|
// writing it the other way round shifts the layout and
|
||||||
|
// crashes nothing but renders the sheet off-place. Paper
|
||||||
|
// space is always a plan view, so DCS == WCS XY here.
|
||||||
|
vp.view_center =
|
||||||
|
acadrust::types::Vector3::new(target_wcs.x, target_wcs.y, 0.0);
|
||||||
|
vp.view_target = acadrust::types::Vector3::ZERO;
|
||||||
vp.view_direction = vd3;
|
vp.view_direction = vd3;
|
||||||
vp.view_height = view_height as f64;
|
vp.view_height = view_height as f64;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue