forked from 1b-agent/opencadstudio-upstream
Feat: Phase 2 — expanded PageSetup panel and PlotSettings integration
PageSetup panel: - Paper size presets (A4/A3/A2/A1/A0 portrait+landscape, Letter) - Plot area selector: Layout vs Extents - Center-on-page toggle - Plot offset X/Y fields - Rotation picker: 0° / 90° / 180° / 270° PlotSettings (acadrust object): - PageSetupCommit finds or creates a PlotSettings object in the document - Writes paper size, plot type, centering, offset, rotation to it - DXF round-trip: PlotSettings persisted to file PDF export: - Reads PlotSettings for the active layout - Applies centering (auto-computes offset from wire extents) - Applies offset X/Y from PlotSettings - Applies rotation via PDF CTM transformation matrix - Swaps paper dimensions for 90°/270° rotation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
73b224c191
commit
617d7ba56a
4 files changed
+410
-44
No files matched your search
@@ -71,6 +71,16 @@ pub(super) struct H7CAD {
|
||||
page_setup_w: String,
|
||||
/// Editable paper height buffer for the Page Setup panel (string while typing).
|
||||
page_setup_h: String,
|
||||
/// Plot area type: "Layout" | "Extents".
|
||||
page_setup_plot_area: String,
|
||||
/// Center the drawing on the page when exporting.
|
||||
page_setup_center: bool,
|
||||
/// Plot offset X in mm (applied after optional centering).
|
||||
page_setup_offset_x: String,
|
||||
/// Plot offset Y in mm.
|
||||
page_setup_offset_y: String,
|
||||
/// Plot rotation in degrees: "0" | "90" | "180" | "270".
|
||||
page_setup_rotation: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -259,6 +269,18 @@ pub enum Message {
|
||||
PageSetupWidthEdit(String),
|
||||
/// Live-edit of the paper height field.
|
||||
PageSetupHeightEdit(String),
|
||||
/// User selected a paper size preset (e.g. "A4 Portrait").
|
||||
PageSetupPreset(String),
|
||||
/// User changed the plot area type ("Layout" or "Extents").
|
||||
PageSetupPlotArea(String),
|
||||
/// Toggle center-on-page.
|
||||
PageSetupCenterToggle,
|
||||
/// Live-edit of plot offset X.
|
||||
PageSetupOffsetXEdit(String),
|
||||
/// Live-edit of plot offset Y.
|
||||
PageSetupOffsetYEdit(String),
|
||||
/// User changed plot rotation.
|
||||
PageSetupRotation(String),
|
||||
/// Apply the changes entered in Page Setup.
|
||||
PageSetupCommit,
|
||||
// ── Plot / Export ─────────────────────────────────────────────────────
|
||||
@@ -300,6 +322,11 @@ impl H7CAD {
|
||||
page_setup_open: false,
|
||||
page_setup_w: String::new(),
|
||||
page_setup_h: String::new(),
|
||||
page_setup_plot_area: "Layout".to_string(),
|
||||
page_setup_center: true,
|
||||
page_setup_offset_x: "0.0".to_string(),
|
||||
page_setup_offset_y: "0.0".to_string(),
|
||||
page_setup_rotation: "0".to_string(),
|
||||
};
|
||||
app.sync_ribbon_layers();
|
||||
app
|
||||
|
||||
+173
-10
@@ -1779,12 +1779,60 @@ impl H7CAD {
|
||||
self.page_setup_h = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupPreset(name) => {
|
||||
// Paper size presets defined in view.rs — mirror them here.
|
||||
let sizes: &[(&str, f64, f64)] = &[
|
||||
("A4 Portrait", 210.0, 297.0),
|
||||
("A4 Landscape", 297.0, 210.0),
|
||||
("A3 Portrait", 297.0, 420.0),
|
||||
("A3 Landscape", 420.0, 297.0),
|
||||
("A2 Portrait", 420.0, 594.0),
|
||||
("A2 Landscape", 594.0, 420.0),
|
||||
("A1 Portrait", 594.0, 841.0),
|
||||
("A1 Landscape", 841.0, 594.0),
|
||||
("A0 Portrait", 841.0, 1189.0),
|
||||
("A0 Landscape", 1189.0, 841.0),
|
||||
("Letter Portrait", 215.9, 279.4),
|
||||
("Letter Landscape", 279.4, 215.9),
|
||||
];
|
||||
if let Some(&(_, w, h)) = sizes.iter().find(|(n, _, _)| *n == name) {
|
||||
self.page_setup_w = format!("{w:.1}");
|
||||
self.page_setup_h = format!("{h:.1}");
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupPlotArea(s) => {
|
||||
self.page_setup_plot_area = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupCenterToggle => {
|
||||
self.page_setup_center = !self.page_setup_center;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupOffsetXEdit(s) => {
|
||||
self.page_setup_offset_x = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupOffsetYEdit(s) => {
|
||||
self.page_setup_offset_y = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupRotation(s) => {
|
||||
self.page_setup_rotation = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupCommit => {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
if layout_name != "Model" {
|
||||
let w: f64 = self.page_setup_w.parse::<f64>().unwrap_or(297.0).max(1.0);
|
||||
let h: f64 = self.page_setup_h.parse::<f64>().unwrap_or(210.0).max(1.0);
|
||||
let plot_area = self.page_setup_plot_area.clone();
|
||||
let center = self.page_setup_center;
|
||||
let offset_x = self.page_setup_offset_x.parse::<f64>().unwrap_or(0.0);
|
||||
let offset_y = self.page_setup_offset_y.parse::<f64>().unwrap_or(0.0);
|
||||
let rotation: i16 = self.page_setup_rotation.parse().unwrap_or(0);
|
||||
|
||||
// Update the Layout object's limits.
|
||||
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
||||
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||
@@ -1797,9 +1845,51 @@ impl H7CAD {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create the PlotSettings object for this layout.
|
||||
use acadrust::objects::{ObjectType, PlotSettings, PlotType, PlotRotation, PlotPaperUnits};
|
||||
let plot_handle = self.tabs[i].scene.document.objects.iter()
|
||||
.find_map(|(h, obj)| {
|
||||
if let ObjectType::PlotSettings(ps) = obj {
|
||||
if ps.page_name == layout_name { Some(*h) } else { None }
|
||||
} else { None }
|
||||
});
|
||||
|
||||
let ps_entry = if let Some(h) = plot_handle {
|
||||
self.tabs[i].scene.document.objects.get_mut(&h)
|
||||
} else {
|
||||
// Create a new PlotSettings object and insert it.
|
||||
let mut ps = PlotSettings::new(layout_name.clone());
|
||||
ps.handle = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
|
||||
let h = ps.handle;
|
||||
self.tabs[i].scene.document.objects.insert(h, ObjectType::PlotSettings(ps));
|
||||
self.tabs[i].scene.document.objects.get_mut(&h)
|
||||
};
|
||||
|
||||
if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
|
||||
ps.paper_width = w;
|
||||
ps.paper_height = h;
|
||||
ps.paper_units = PlotPaperUnits::Millimeters;
|
||||
ps.plot_type = if plot_area == "Extents" {
|
||||
PlotType::Extents
|
||||
} else {
|
||||
PlotType::Layout
|
||||
};
|
||||
ps.flags.plot_centered = center;
|
||||
ps.origin_x = offset_x;
|
||||
ps.origin_y = offset_y;
|
||||
ps.rotation = match rotation {
|
||||
90 => PlotRotation::Degrees90,
|
||||
180 => PlotRotation::Degrees180,
|
||||
270 => PlotRotation::Degrees270,
|
||||
_ => PlotRotation::None,
|
||||
};
|
||||
}
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_info(&format!(
|
||||
"Page size set to {w:.1} × {h:.1} mm."
|
||||
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
||||
center={center} rot={rotation}°"
|
||||
));
|
||||
}
|
||||
self.page_setup_open = false;
|
||||
@@ -1823,24 +1913,97 @@ impl H7CAD {
|
||||
Message::PlotExportPath(None) => Task::none(),
|
||||
Message::PlotExportPath(Some(path)) => {
|
||||
let i = self.active_tab;
|
||||
let wires = self.tabs[i].scene.entity_wires(); // full scene for PDF
|
||||
let (paper_w, paper_h, offset_x, offset_y) =
|
||||
if let Some(((x0, y0), (x1, y1))) = self.tabs[i].scene.paper_limits() {
|
||||
(x1 - x0, y1 - y0, -x0, -y0)
|
||||
let scene = &self.tabs[i].scene;
|
||||
let layout_name = scene.current_layout.clone();
|
||||
let wires = scene.entity_wires();
|
||||
|
||||
// Read PlotSettings for current layout (if available).
|
||||
use acadrust::objects::{ObjectType, PlotType, PlotRotation};
|
||||
let ps_snap = scene.document.objects.values().find_map(|obj| {
|
||||
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.
|
||||
let (paper_w, paper_h, mut draw_ox, mut draw_oy, rotation_deg) =
|
||||
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
||||
let (pw, ph) = (x1 - x0, y1 - y0);
|
||||
|
||||
// If PlotSettings says Extents, use model space extents instead.
|
||||
let use_extents = ps_snap.as_ref()
|
||||
.map(|ps| matches!(ps.plot_type, PlotType::Extents))
|
||||
.unwrap_or(false);
|
||||
|
||||
let (ox, oy) = if use_extents {
|
||||
if let Some((mn, mx)) = scene.model_space_extents() {
|
||||
(-mn.x as f64, -mn.y as f64)
|
||||
} else {
|
||||
(-x0, -y0)
|
||||
}
|
||||
} else {
|
||||
(-x0, -y0)
|
||||
};
|
||||
|
||||
let rot = ps_snap.as_ref()
|
||||
.map(|ps| ps.rotation.to_degrees() as i32)
|
||||
.unwrap_or(0);
|
||||
|
||||
(pw, ph, ox, oy, rot)
|
||||
} else {
|
||||
// Model space: fit wires to computed extents with 5 % margin.
|
||||
// Model space: fit with 5% margin.
|
||||
let margin = 1.05_f64;
|
||||
if let Some((mn, mx)) = self.tabs[i].scene.model_space_extents() {
|
||||
if let Some((mn, mx)) = scene.model_space_extents() {
|
||||
let w = ((mx.x - mn.x) as f64 * margin).max(1.0);
|
||||
let h = ((mx.y - mn.y) as f64 * margin).max(1.0);
|
||||
let pad_x = (w - (mx.x - mn.x) as f64) * 0.5;
|
||||
let pad_y = (h - (mx.y - mn.y) as f64) * 0.5;
|
||||
(w, h, -(mn.x as f64) + pad_x, -(mn.y as f64) + pad_y)
|
||||
(w, h, -(mn.x as f64) + pad_x, -(mn.y as f64) + pad_y, 0)
|
||||
} else {
|
||||
(297.0, 210.0, 0.0, 0.0)
|
||||
(297.0, 210.0, 0.0, 0.0, 0)
|
||||
}
|
||||
};
|
||||
match crate::io::pdf_export::export_pdf(&wires, paper_w, paper_h, offset_x as f32, offset_y as f32, &path) {
|
||||
|
||||
// Apply PlotSettings offset and centering.
|
||||
if let Some(ref ps) = ps_snap {
|
||||
if ps.flags.plot_centered {
|
||||
// Centering: compute wire extents and re-centre.
|
||||
let all_x: Vec<f32> = wires.iter()
|
||||
.flat_map(|w| w.points.iter().map(|p| p[0]))
|
||||
.filter(|v| !v.is_nan())
|
||||
.collect();
|
||||
let all_y: Vec<f32> = wires.iter()
|
||||
.flat_map(|w| w.points.iter().map(|p| p[1]))
|
||||
.filter(|v| !v.is_nan())
|
||||
.collect();
|
||||
if let (Some(&min_x), Some(&max_x), Some(&min_y), Some(&max_y)) = (
|
||||
all_x.iter().copied().reduce(f32::min).as_ref(),
|
||||
all_x.iter().copied().reduce(f32::max).as_ref(),
|
||||
all_y.iter().copied().reduce(f32::min).as_ref(),
|
||||
all_y.iter().copied().reduce(f32::max).as_ref(),
|
||||
) {
|
||||
let cx = (min_x + max_x) as f64 / 2.0;
|
||||
let cy = (min_y + max_y) as f64 / 2.0;
|
||||
draw_ox += paper_w / 2.0 - cx;
|
||||
draw_oy += paper_h / 2.0 - cy;
|
||||
}
|
||||
} else {
|
||||
draw_ox += ps.origin_x;
|
||||
draw_oy += ps.origin_y;
|
||||
}
|
||||
}
|
||||
|
||||
// For rotation: swap paper dimensions and note angle for export.
|
||||
let (eff_w, eff_h) = match rotation_deg {
|
||||
90 | 270 => (paper_h, paper_w),
|
||||
_ => (paper_w, paper_h),
|
||||
};
|
||||
|
||||
match crate::io::pdf_export::export_pdf(
|
||||
&wires, eff_w, eff_h,
|
||||
draw_ox as f32, draw_oy as f32,
|
||||
rotation_deg, &path,
|
||||
) {
|
||||
Ok(()) => self.command_line.push_info(&format!(
|
||||
"Exported: {}",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
|
||||
+175
-28
@@ -5,7 +5,7 @@ use super::helpers::grid_plane_from_camera;
|
||||
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
||||
use crate::scene::grip::grips_to_screen;
|
||||
use crate::ui::overlay;
|
||||
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, text_input, Row};
|
||||
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, text_input, Row, Space};
|
||||
use iced::window;
|
||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||
|
||||
@@ -200,7 +200,15 @@ impl H7CAD {
|
||||
};
|
||||
|
||||
let page_setup_layer: Element<'_, Message> = if self.page_setup_open {
|
||||
page_setup_overlay(&self.page_setup_w, &self.page_setup_h)
|
||||
page_setup_overlay(
|
||||
&self.page_setup_w,
|
||||
&self.page_setup_h,
|
||||
&self.page_setup_plot_area,
|
||||
self.page_setup_center,
|
||||
&self.page_setup_offset_x,
|
||||
&self.page_setup_offset_y,
|
||||
&self.page_setup_rotation,
|
||||
)
|
||||
} else {
|
||||
iced::widget::Space::new().width(0).height(0).into()
|
||||
};
|
||||
@@ -453,13 +461,41 @@ fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
||||
// ── Page Setup overlay ──────────────────────────────────────────────────────
|
||||
|
||||
/// Modal panel for editing paper width / height of the current layout.
|
||||
fn page_setup_overlay<'a>(w_buf: &'a str, h_buf: &'a str) -> Element<'a, Message> {
|
||||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||||
// ── Paper size presets ────────────────────────────────────────────────────
|
||||
|
||||
const PAPER_PRESETS: &[(&str, f64, f64)] = &[
|
||||
("A4 Portrait", 210.0, 297.0),
|
||||
("A4 Landscape", 297.0, 210.0),
|
||||
("A3 Portrait", 297.0, 420.0),
|
||||
("A3 Landscape", 420.0, 297.0),
|
||||
("A2 Portrait", 420.0, 594.0),
|
||||
("A2 Landscape", 594.0, 420.0),
|
||||
("A1 Portrait", 594.0, 841.0),
|
||||
("A1 Landscape", 841.0, 594.0),
|
||||
("A0 Portrait", 841.0, 1189.0),
|
||||
("A0 Landscape", 1189.0, 841.0),
|
||||
("Letter Portrait", 215.9, 279.4),
|
||||
("Letter Landscape", 279.4, 215.9),
|
||||
("Custom", 0.0, 0.0),
|
||||
];
|
||||
|
||||
fn page_setup_overlay<'a>(
|
||||
w_buf: &'a str,
|
||||
h_buf: &'a str,
|
||||
plot_area: &'a str,
|
||||
center: bool,
|
||||
offset_x: &'a str,
|
||||
offset_y: &'a str,
|
||||
rotation: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||||
const BORDER_COL: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||||
const TEXT_COLOR: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||||
const DIM_COLOR: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||||
|
||||
let label = |s: &'static str| text(s).size(12).color(TEXT_COLOR);
|
||||
let lbl = |s: &'static str| text(s).size(11).color(DIM_COLOR).width(110);
|
||||
|
||||
let field_style = |_: &Theme, _: text_input::Status| text_input::Style {
|
||||
background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }),
|
||||
@@ -489,52 +525,163 @@ fn page_setup_overlay<'a>(w_buf: &'a str, h_buf: &'a str) -> Element<'a, Message
|
||||
}
|
||||
};
|
||||
|
||||
let pill_style = |active: bool| {
|
||||
move |_: &Theme, status: button::Status| button::Style {
|
||||
background: Some(Background::Color(match (active, status) {
|
||||
(true, _) => ACTIVE_BG,
|
||||
(false, button::Status::Hovered | button::Status::Pressed) => {
|
||||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
|
||||
}
|
||||
_ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 },
|
||||
})),
|
||||
text_color: TEXT_COLOR,
|
||||
border: Border { color: BORDER_COL, width: 1.0, radius: 3.0.into() },
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
}
|
||||
};
|
||||
|
||||
let divider = || container(Space::new().width(Fill).height(1))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BORDER_COL)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.height(1);
|
||||
|
||||
// ── Paper size presets ────────────────────────────────────────────────
|
||||
let preset_row1 = row![
|
||||
button(text("A4 P").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A4 Portrait".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("A4 L").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A4 Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("A3 P").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A3 Portrait".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("A3 L").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A3 Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
].spacing(4);
|
||||
|
||||
let preset_row2 = row![
|
||||
button(text("A2 L").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A2 Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("A1 L").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A1 Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("A0 L").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("A0 Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
button(text("Letter").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPreset("Letter Landscape".into()))
|
||||
.style(pill_style(false)).padding([3, 6]),
|
||||
].spacing(4);
|
||||
|
||||
// ── Plot area buttons ─────────────────────────────────────────────────
|
||||
let area_row = row![
|
||||
button(text("Layout").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPlotArea("Layout".into()))
|
||||
.style(pill_style(plot_area == "Layout")).padding([3, 8]),
|
||||
button(text("Extents").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupPlotArea("Extents".into()))
|
||||
.style(pill_style(plot_area == "Extents")).padding([3, 8]),
|
||||
].spacing(6);
|
||||
|
||||
// ── Center toggle ─────────────────────────────────────────────────────
|
||||
let center_btn = button(
|
||||
text(if center { "✓ Center on page" } else { " Center on page" }).size(11).color(TEXT_COLOR)
|
||||
)
|
||||
.on_press(Message::PageSetupCenterToggle)
|
||||
.style(pill_style(center))
|
||||
.padding([4, 10]);
|
||||
|
||||
// ── Rotation buttons ──────────────────────────────────────────────────
|
||||
let rot_row = row![
|
||||
button(text("0°").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupRotation("0".into()))
|
||||
.style(pill_style(rotation == "0")).padding([3, 8]),
|
||||
button(text("90°").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupRotation("90".into()))
|
||||
.style(pill_style(rotation == "90")).padding([3, 8]),
|
||||
button(text("180°").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupRotation("180".into()))
|
||||
.style(pill_style(rotation == "180")).padding([3, 8]),
|
||||
button(text("270°").size(10).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupRotation("270".into()))
|
||||
.style(pill_style(rotation == "270")).padding([3, 8]),
|
||||
].spacing(4);
|
||||
|
||||
let panel = container(
|
||||
column![
|
||||
text("Page Setup").size(14).color(TEXT_COLOR),
|
||||
container(iced::widget::Space::new().width(Fill).height(1))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BORDER_COL)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill),
|
||||
divider(),
|
||||
// Paper size
|
||||
text("Paper Size").size(11).color(DIM_COLOR),
|
||||
preset_row1,
|
||||
preset_row2,
|
||||
row![
|
||||
label("Width (mm)"),
|
||||
lbl("Width (mm)"),
|
||||
text_input("297", w_buf)
|
||||
.on_input(Message::PageSetupWidthEdit)
|
||||
.on_submit(Message::PageSetupCommit)
|
||||
.style(field_style)
|
||||
.width(90)
|
||||
.size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
.width(80).size(12),
|
||||
].spacing(6).align_y(iced::Alignment::Center),
|
||||
row![
|
||||
label("Height (mm)"),
|
||||
lbl("Height (mm)"),
|
||||
text_input("210", h_buf)
|
||||
.on_input(Message::PageSetupHeightEdit)
|
||||
.on_submit(Message::PageSetupCommit)
|
||||
.style(field_style)
|
||||
.width(90)
|
||||
.size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
.width(80).size(12),
|
||||
].spacing(6).align_y(iced::Alignment::Center),
|
||||
divider(),
|
||||
// Plot area
|
||||
text("Plot Area").size(11).color(DIM_COLOR),
|
||||
area_row,
|
||||
divider(),
|
||||
// Position
|
||||
text("Position").size(11).color(DIM_COLOR),
|
||||
center_btn,
|
||||
row![
|
||||
lbl("Offset X (mm)"),
|
||||
text_input("0", offset_x)
|
||||
.on_input(Message::PageSetupOffsetXEdit)
|
||||
.style(field_style)
|
||||
.width(80).size(12),
|
||||
].spacing(6).align_y(iced::Alignment::Center),
|
||||
row![
|
||||
lbl("Offset Y (mm)"),
|
||||
text_input("0", offset_y)
|
||||
.on_input(Message::PageSetupOffsetYEdit)
|
||||
.style(field_style)
|
||||
.width(80).size(12),
|
||||
].spacing(6).align_y(iced::Alignment::Center),
|
||||
divider(),
|
||||
// Rotation
|
||||
text("Rotation").size(11).color(DIM_COLOR),
|
||||
rot_row,
|
||||
divider(),
|
||||
// Buttons
|
||||
row![
|
||||
button(text("Cancel").size(12).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupClose)
|
||||
.style(btn_style(false))
|
||||
.padding([5, 14]),
|
||||
Space::new().width(Fill).height(0),
|
||||
button(text("OK").size(12).color(TEXT_COLOR))
|
||||
.on_press(Message::PageSetupCommit)
|
||||
.style(btn_style(true))
|
||||
.padding([5, 20]),
|
||||
]
|
||||
.spacing(8),
|
||||
.align_y(iced::Alignment::Center),
|
||||
]
|
||||
.spacing(10)
|
||||
.spacing(8)
|
||||
.padding(16)
|
||||
.width(240),
|
||||
.width(290),
|
||||
)
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(PANEL_BG)),
|
||||
@@ -544,7 +691,7 @@ fn page_setup_overlay<'a>(w_buf: &'a str, h_buf: &'a str) -> Element<'a, Message
|
||||
|
||||
// Click-catcher to close on outside click.
|
||||
let catcher = mouse_area(
|
||||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||||
container(Space::new().width(Fill).height(Fill))
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
)
|
||||
|
||||
+35
-6
@@ -18,20 +18,20 @@ use std::path::Path;
|
||||
|
||||
/// Export `wires` to a PDF file.
|
||||
///
|
||||
/// - `paper_w` / `paper_h`: page dimensions in mm.
|
||||
/// - `paper_w` / `paper_h`: page dimensions in mm (already swapped for 90°/270° by caller).
|
||||
/// - `offset_x` / `offset_y`: added to every wire coordinate so the drawing
|
||||
/// origin maps to the bottom-left corner of the page. Pass 0/0 for paper
|
||||
/// space (coordinates are already relative to page origin); pass a computed
|
||||
/// shift for model space to normalise the extents.
|
||||
/// origin maps to the bottom-left corner of the page.
|
||||
/// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page.
|
||||
pub fn export_pdf(
|
||||
wires: &[WireModel],
|
||||
paper_w: f64,
|
||||
paper_h: f64,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
rotation_deg: i32,
|
||||
path: &Path,
|
||||
) -> Result<(), String> {
|
||||
let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y);
|
||||
let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y, rotation_deg);
|
||||
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
|
||||
file.write_all(&bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -50,7 +50,7 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
|
||||
|
||||
// ── PDF builder ───────────────────────────────────────────────────────────
|
||||
|
||||
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32) -> Vec<u8> {
|
||||
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32) -> Vec<u8> {
|
||||
let mut doc = PdfDocument::new("H7CAD Export");
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
|
||||
@@ -66,6 +66,31 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32)
|
||||
ops.push(Op::SetLineCapStyle { cap: LineCapStyle::Round });
|
||||
ops.push(Op::SetLineJoinStyle { join: LineJoinStyle::Round });
|
||||
|
||||
// Apply rotation transform if needed.
|
||||
// PDF uses mm-based coordinate system with origin at bottom-left.
|
||||
// We save state, apply a CTM, then restore it after drawing.
|
||||
let needs_rotation = rotation_deg != 0;
|
||||
if needs_rotation {
|
||||
let (cos_a, sin_a, tx, ty) = match rotation_deg {
|
||||
90 => ( 0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => ( 0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
_ => ( 1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
};
|
||||
// PDF CTM: [a b c d e f] = [cos sin -sin cos tx ty]
|
||||
ops.push(Op::SaveGraphicsState);
|
||||
// Convert mm translation to points (1 mm = 2.834645 pt).
|
||||
let tx_pt = (tx * 2.834645) as f32;
|
||||
let ty_pt = (ty * 2.834645) as f32;
|
||||
ops.push(Op::SetTransformationMatrix {
|
||||
matrix: printpdf::CurTransMat::Raw([
|
||||
cos_a as f32, sin_a as f32,
|
||||
-(sin_a as f32), cos_a as f32,
|
||||
tx_pt, ty_pt,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
let mut last_color: Option<[f32; 3]> = None;
|
||||
let mut last_lw: Option<f32> = None;
|
||||
|
||||
@@ -121,6 +146,10 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32)
|
||||
flush_line(&mut ops, &segment);
|
||||
}
|
||||
|
||||
if needs_rotation {
|
||||
ops.push(Op::RestoreGraphicsState);
|
||||
}
|
||||
|
||||
let page = PdfPage::new(Mm(paper_w), Mm(paper_h), ops);
|
||||
doc.pages.push(page);
|
||||
|
||||
|
||||
Reference in new issue
Block a user