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.layout_names(),
|
||||||
tab.scene.current_layout.clone(),
|
tab.scene.current_layout.clone(),
|
||||||
self.layout_rename_state.as_ref(),
|
self.layout_rename_state.as_ref(),
|
||||||
|
tab.scene.first_viewport_scale(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,58 @@ impl Scene {
|
||||||
.unwrap_or(Handle::NULL)
|
.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.
|
/// Sorted list of layout names: "Model" first, then paper layouts by tab order.
|
||||||
pub fn layout_names(&self) -> Vec<String> {
|
pub fn layout_names(&self) -> Vec<String> {
|
||||||
let mut names = vec!["Model".to_string()];
|
let mut names = vec!["Model".to_string()];
|
||||||
|
|
@ -134,6 +186,10 @@ impl Scene {
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let mut wires: Vec<WireModel> = self.wires_for_block(layout_block);
|
let mut wires: Vec<WireModel> = self.wires_for_block(layout_block);
|
||||||
if self.current_layout != "Model" {
|
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.extend(self.viewport_content_wires(layout_block));
|
||||||
}
|
}
|
||||||
wires
|
wires
|
||||||
|
|
@ -1121,3 +1177,30 @@ impl Default for Scene {
|
||||||
Self::new()
|
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,
|
current_layout: String,
|
||||||
// If `Some((original, edit_value))`, the named tab shows a text input.
|
// If `Some((original, edit_value))`, the named tab shows a text input.
|
||||||
rename_state: Option<&'a (String, String)>,
|
rename_state: Option<&'a (String, String)>,
|
||||||
|
// Scale of the first user viewport in the active paper layout.
|
||||||
|
viewport_scale: Option<f64>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
||||||
.on_press(Message::Command("MENU".into()))
|
.on_press(Message::Command("MENU".into()))
|
||||||
|
|
@ -57,6 +59,7 @@ impl StatusBar {
|
||||||
} else {
|
} else {
|
||||||
"LAYOUT"
|
"LAYOUT"
|
||||||
};
|
};
|
||||||
|
let scale_label = format_scale(viewport_scale);
|
||||||
let right_status = row![
|
let right_status = row![
|
||||||
tip(
|
tip(
|
||||||
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
||||||
|
|
@ -76,7 +79,7 @@ impl StatusBar {
|
||||||
),
|
),
|
||||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
||||||
status_pill(space_label),
|
status_pill(space_label),
|
||||||
status_pill("1:1"),
|
status_pill(scale_label),
|
||||||
]
|
]
|
||||||
.spacing(2);
|
.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> {
|
fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
|
||||||
container(text(label).size(10).color(Color {
|
container(text(label.into()).size(10).color(Color {
|
||||||
r: 0.65,
|
r: 0.65,
|
||||||
g: 0.65,
|
g: 0.65,
|
||||||
b: 0.65,
|
b: 0.65,
|
||||||
|
|
@ -460,3 +463,40 @@ const SNAP_OFF_HOVER: Color = Color {
|
||||||
b: 0.22,
|
b: 0.22,
|
||||||
a: 1.0,
|
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