Aşama 2: Paper space görsel düzenleme
- Kağıt sınırı dikdörtgeni: paper layout'ta Layout.min/max_limits'ten beyaz çerçeve wire'ı oluşturulur, entity_wires()'a eklenir - first_viewport_scale(): paper layout'taki ilk kullanıcı viewport'un ölçeğini döner - paper_limits(): mevcut layout'un kağıt sınırlarını döner, sıfır boyutlu layout için varsayılan 12×9 kullanılır - Status bar scale göstergesi: hardcoded "1:1" yerine aktif viewport ölçeği dinamik olarak "1:50", "2:1" vb. formatında gösterilir - Overall viewport (ID=1) zaten gri renkte render ediliyor (tessellate_one) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b14719ec5
commit
474b38f4fb
3 changed files with 127 additions and 3 deletions
|
|
@ -165,6 +165,7 @@ impl H7CAD {
|
|||
tab.scene.layout_names(),
|
||||
tab.scene.current_layout.clone(),
|
||||
self.layout_rename_state.as_ref(),
|
||||
tab.scene.first_viewport_scale(),
|
||||
)
|
||||
]
|
||||
.width(Fill)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,58 @@ impl Scene {
|
|||
.unwrap_or(Handle::NULL)
|
||||
}
|
||||
|
||||
/// Returns `(min, max)` paper-space limits for the current layout, or `None`
|
||||
/// when in Model space. Falls back to `(0,0)-(12,9)` if the layout has
|
||||
/// zero-size limits (common in freshly-created layouts).
|
||||
pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
||||
if self.current_layout == "Model" {
|
||||
return None;
|
||||
}
|
||||
self.document.objects.values().find_map(|obj| {
|
||||
if let ObjectType::Layout(l) = obj {
|
||||
if l.name == self.current_layout {
|
||||
let (min, max) = (l.min_limits, l.max_limits);
|
||||
// Guard against degenerate limits.
|
||||
let w = (max.0 - min.0).abs();
|
||||
let h = (max.1 - min.1).abs();
|
||||
if w < 1e-6 || h < 1e-6 {
|
||||
return Some(((0.0, 0.0), (12.0, 9.0)));
|
||||
}
|
||||
return Some((min, max));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Scale of the first user viewport (id > 1) in the current paper layout,
|
||||
/// used for the status-bar display. Returns `None` in Model space or if
|
||||
/// no user viewport exists.
|
||||
pub fn first_viewport_scale(&self) -> Option<f64> {
|
||||
if self.current_layout == "Model" {
|
||||
return None;
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
if layout_block.is_null() {
|
||||
return None;
|
||||
}
|
||||
self.document.entities().find_map(|e| {
|
||||
if let EntityType::Viewport(vp) = e {
|
||||
if vp.id > 1 && vp.common.owner_handle == layout_block {
|
||||
let scale = if vp.custom_scale.abs() > 1e-9 {
|
||||
vp.custom_scale
|
||||
} else if vp.view_height.abs() > 1e-9 {
|
||||
vp.height / vp.view_height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
return Some(scale);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Sorted list of layout names: "Model" first, then paper layouts by tab order.
|
||||
pub fn layout_names(&self) -> Vec<String> {
|
||||
let mut names = vec!["Model".to_string()];
|
||||
|
|
@ -134,6 +186,10 @@ impl Scene {
|
|||
let layout_block = self.current_layout_block_handle();
|
||||
let mut wires: Vec<WireModel> = self.wires_for_block(layout_block);
|
||||
if self.current_layout != "Model" {
|
||||
// Draw the paper boundary rectangle first (rendered beneath everything else).
|
||||
if let Some(((x0, y0), (x1, y1))) = self.paper_limits() {
|
||||
wires.insert(0, paper_boundary_wire(x0 as f32, y0 as f32, x1 as f32, y1 as f32));
|
||||
}
|
||||
wires.extend(self.viewport_content_wires(layout_block));
|
||||
}
|
||||
wires
|
||||
|
|
@ -1121,3 +1177,30 @@ impl Default for Scene {
|
|||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paper boundary wire ────────────────────────────────────────────────────
|
||||
|
||||
/// A thin white rectangle wire that represents the printable-area boundary
|
||||
/// of the active paper layout. Rendered beneath all other paper-space
|
||||
/// geometry so it acts as a visual "page" backdrop.
|
||||
fn paper_boundary_wire(x0: f32, y0: f32, x1: f32, y1: f32) -> WireModel {
|
||||
WireModel {
|
||||
name: "__paper_boundary__".to_string(),
|
||||
points: vec![
|
||||
[x0, y0, 0.0],
|
||||
[x1, y0, 0.0],
|
||||
[x1, y1, 0.0],
|
||||
[x0, y1, 0.0],
|
||||
[x0, y0, 0.0],
|
||||
],
|
||||
// Near-white so it stands out against the dark paper-space background.
|
||||
color: [0.95, 0.95, 0.95, 1.0],
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.5,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ impl StatusBar {
|
|||
current_layout: String,
|
||||
// If `Some((original, edit_value))`, the named tab shows a text input.
|
||||
rename_state: Option<&'a (String, String)>,
|
||||
// Scale of the first user viewport in the active paper layout.
|
||||
viewport_scale: Option<f64>,
|
||||
) -> Element<'a, Message> {
|
||||
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
||||
.on_press(Message::Command("MENU".into()))
|
||||
|
|
@ -57,6 +59,7 @@ impl StatusBar {
|
|||
} else {
|
||||
"LAYOUT"
|
||||
};
|
||||
let scale_label = format_scale(viewport_scale);
|
||||
let right_status = row![
|
||||
tip(
|
||||
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
||||
|
|
@ -76,7 +79,7 @@ impl StatusBar {
|
|||
),
|
||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
||||
status_pill(space_label),
|
||||
status_pill("1:1"),
|
||||
status_pill(scale_label),
|
||||
]
|
||||
.spacing(2);
|
||||
|
||||
|
|
@ -353,8 +356,8 @@ fn space_tab<'a>(label: String, is_active: bool, rename_edit: Option<&'a str>) -
|
|||
}
|
||||
}
|
||||
|
||||
fn status_pill(label: &str) -> Element<'_, Message> {
|
||||
container(text(label).size(10).color(Color {
|
||||
fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
|
||||
container(text(label.into()).size(10).color(Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
|
|
@ -460,3 +463,40 @@ const SNAP_OFF_HOVER: Color = Color {
|
|||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
// ── Scale display ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Formats a viewport scale factor as a human-readable ratio string.
|
||||
///
|
||||
/// - `None` → "1:1" (model space or no viewport yet)
|
||||
/// - `1.0` → "1:1"
|
||||
/// - `0.02` → "1:50"
|
||||
/// - `2.0` → "2:1"
|
||||
fn format_scale(scale: Option<f64>) -> String {
|
||||
let s = match scale {
|
||||
None => return "1:1".to_string(),
|
||||
Some(v) if v <= 0.0 => return "1:1".to_string(),
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
// Try to express as a clean integer ratio.
|
||||
if s >= 1.0 {
|
||||
let n = s.round() as u32;
|
||||
if (s - n as f64).abs() < 0.01 * s {
|
||||
return if n == 1 {
|
||||
"1:1".to_string()
|
||||
} else {
|
||||
format!("{}:1", n)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let inv = (1.0 / s).round() as u32;
|
||||
if (s - 1.0 / inv as f64).abs() < 0.01 * s {
|
||||
return format!("1:{}", inv);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to a decimal string.
|
||||
format!("{:.4}", s).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue