Fix: paper limits, Cohen-Sutherland clipping, frozen layers UI, PDF improvements

Paper limits:
- Change A4 fallback from 12×9 to 297×210 mm in paper_limits()

Context menu:
- Translate layout context menu labels to English (Rename / Delete)

Viewport clipping:
- Replace AABB-only culling with proper Cohen-Sutherland line clipping
  in viewport_content_wires(); each projected segment is now clipped at
  the exact viewport boundary instead of being included or discarded whole
- Add cs_clip() and clip_polyline_to_rect() free functions in scene/mod.rs

Frozen layers per-viewport UI:
- Inject a "Frozen Layers" EditText property in the Properties panel for
  viewport entities (layer names resolved from handles at display time)
- Handle "frozen_layers" in PropGeomCommit: parse comma-separated names,
  resolve to layer handles, update vp.frozen_layers in-place

PDF export improvements:
- Add offset_x/offset_y params to export_pdf() for model-space normalization:
  coordinates are shifted so the drawing origin maps to the page origin
- Model space export computes extents via model_space_extents() (now public)
  and adds a 5 % margin; falls back to A4 landscape for empty drawings
- Skip __paper_boundary__ wire in PDF (white background covers it)
- Map yellow (active viewport border) → black and cyan (vp border) → dark
  blue for print-friendly output

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-03-31 08:18:29 +03:00
1 parent e1d296da10
commit 845770938c
5 files changed
+262 -67

No files matched your search

+36 -22
View File
@@ -5,7 +5,8 @@
//
// Coordinate system: CAD uses mm units with origin at bottom-left and Y up.
// printpdf's Point::new(Mm, Mm) also has origin at bottom-left, so no Y-flip
// is needed — we just pass the coordinates through directly.
// is needed — we shift the coordinates by (offset_x, offset_y) to place the
// drawing origin at the paper origin.
use crate::scene::WireModel;
use printpdf::{Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument,
@@ -17,14 +18,20 @@ use std::path::Path;
/// Export `wires` to a PDF file.
///
/// `paper_w` / `paper_h` are in millimetres (drawing units assumed = mm).
/// - `paper_w` / `paper_h`: page dimensions in mm.
/// - `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.
pub fn export_pdf(
wires: &[WireModel],
paper_w: f64,
paper_h: f64,
offset_x: f32,
offset_y: f32,
path: &Path,
) -> Result<(), String> {
let bytes = build_pdf(wires, paper_w as f32, paper_h as f32);
let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y);
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
file.write_all(&bytes).map_err(|e| e.to_string())
}
@@ -43,11 +50,11 @@ 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) -> Vec<u8> {
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32) -> Vec<u8> {
let mut doc = PdfDocument::new("H7CAD Export");
let mut ops: Vec<Op> = Vec::new();
// White page background rectangle.
// White page background.
ops.push(Op::SetFillColor {
col: Color::Rgb(Rgb { r: 1.0, g: 1.0, b: 1.0, icc_profile: None }),
});
@@ -55,43 +62,50 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32) -> Vec<u8> {
rectangle: printpdf::Rect::from_wh(Mm(paper_w).into(), Mm(paper_h).into()),
});
// Round line caps for CAD aesthetics.
// Round line caps/joins for CAD aesthetics.
ops.push(Op::SetLineCapStyle { cap: LineCapStyle::Round });
ops.push(Op::SetLineJoinStyle { join: LineJoinStyle::Round });
let mut last_color: Option<[f32; 4]> = None;
let mut last_color: Option<[f32; 3]> = None;
let mut last_lw: Option<f32> = None;
for wire in wires {
// Set stroke color (white → black for print; skip fully transparent).
let [mut r, mut g, mut b, a] = wire.color;
if a < 0.01 {
continue;
}
// Invert near-white to black so it prints on white paper.
if r > 0.85 && g > 0.85 && b > 0.85 {
r = 0.0;
g = 0.0;
b = 0.0;
// Skip the paper-boundary wire — the white PDF background already provides it.
if wire.name == "__paper_boundary__" {
continue;
}
let color_changed = last_color.map(|c| {
(c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01
}).unwrap_or(true);
if color_changed {
// Near-white and near-yellow (viewport active border) → dark grey for print.
let is_light = r > 0.80 && g > 0.80 && b > 0.80;
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
if is_light || is_yellow {
r = 0.0; g = 0.0; b = 0.0;
} else if is_cyan {
// Viewport border: print as dark blue.
r = 0.0; g = 0.15; b = 0.50;
}
if last_color.map(|c| {
(c[0]-r).abs() > 0.01 || (c[1]-g).abs() > 0.01 || (c[2]-b).abs() > 0.01
}).unwrap_or(true) {
ops.push(Op::SetOutlineColor {
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
});
last_color = Some([r, g, b, a]);
last_color = Some([r, g, b]);
}
// Set line width (in points; 1 mm = 2.8346 pt).
let lw_pt = (wire.line_weight_px as f32 * 0.35278).max(0.1);
// Line weight: screen px → points (approximate 1 px ≈ 0.35 pt).
let lw_pt = (wire.line_weight_px * 0.35278_f32).max(0.1);
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) });
last_lw = Some(lw_pt);
}
// Collect segments (split at NaN).
// Emit segments (NaN = pen-up).
let mut segment: Vec<LinePoint> = Vec::new();
for &[x, y, _z] in &wire.points {
if x.is_nan() || y.is_nan() {
@@ -99,7 +113,7 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32) -> Vec<u8> {
segment.clear();
} else {
segment.push(LinePoint {
p: Point::new(Mm(x), Mm(y)),
p: Point::new(Mm(x + ox), Mm(y + oy)),
bezier: false,
});
}