fix(polyline): correct wide-polyline fill band arc center + add LWDISPLAY toggle
Wide LWPolyline/Polyline2D fill bands now sit on the centerline: - polyline_segment_fill arc center matched explode.rs / acadrust convention (signed d_perp = r·(1-b²)/(1+b²), handles major arcs correctly) - per-vertex hw0/hw1 taper applied along arc; tess density 24/circle - synced_hatch_models subtracts hatch_offset from band boundary so it lives in the same offset-rel space as the wire tessellation LWDISPLAY: - new command `LWDISPLAY [ON|OFF]` toggles header.lineweight_display - LWT status-bar pill (between ORTHO and POLAR) drives the same flag - bump_geometry() on change so the viewport re-tess immediately Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2790182067
commit
fb3dd58d09
6 changed files with 91 additions and 21 deletions
|
|
@ -4506,6 +4506,38 @@ impl H7CAD {
|
||||||
self.command_line.push_error("Usage: LTSCALE [value]");
|
self.command_line.push_error("Usage: LTSCALE [value]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cmd if cmd == "LWDISPLAY" || cmd.starts_with("LWDISPLAY ") => {
|
||||||
|
let val_str = cmd.trim_start_matches("LWDISPLAY").trim();
|
||||||
|
let parsed: Result<Option<bool>, ()> =
|
||||||
|
match val_str.to_ascii_uppercase().as_str() {
|
||||||
|
"" => Ok(None),
|
||||||
|
"ON" | "1" | "TRUE" => Ok(Some(true)),
|
||||||
|
"OFF" | "0" | "FALSE" => Ok(Some(false)),
|
||||||
|
_ => Err(()),
|
||||||
|
};
|
||||||
|
match parsed {
|
||||||
|
Err(_) => self
|
||||||
|
.command_line
|
||||||
|
.push_error("Usage: LWDISPLAY [ON|OFF]"),
|
||||||
|
Ok(Some(v)) => {
|
||||||
|
self.push_undo_snapshot(i, "LWDISPLAY");
|
||||||
|
self.tabs[i].scene.document.header.lineweight_display = v;
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"LWDISPLAY {}",
|
||||||
|
if v { "ON" } else { "OFF" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
let v = self.tabs[i].scene.document.header.lineweight_display;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"LWDISPLAY = {}",
|
||||||
|
if v { "ON" } else { "OFF" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
cmd if cmd == "CELTSCALE" || cmd.starts_with("CELTSCALE ") => {
|
cmd if cmd == "CELTSCALE" || cmd.starts_with("CELTSCALE ") => {
|
||||||
let val_str = cmd.trim_start_matches("CELTSCALE").trim();
|
let val_str = cmd.trim_start_matches("CELTSCALE").trim();
|
||||||
if val_str.is_empty() {
|
if val_str.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -401,6 +401,8 @@ pub enum Message {
|
||||||
ToggleGrid,
|
ToggleGrid,
|
||||||
/// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button.
|
/// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button.
|
||||||
ToggleOrtho,
|
ToggleOrtho,
|
||||||
|
/// Toggle LWDISPLAY header flag — LWT status-bar button.
|
||||||
|
ToggleLineweightDisplay,
|
||||||
/// Toggle polar-angle constraint — F10 / POLAR status-bar button.
|
/// Toggle polar-angle constraint — F10 / POLAR status-bar button.
|
||||||
TogglePolar,
|
TogglePolar,
|
||||||
/// Set polar tracking angle increment (right-click POLAR button).
|
/// Set polar tracking angle increment (right-click POLAR button).
|
||||||
|
|
|
||||||
|
|
@ -2509,6 +2509,16 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ToggleLineweightDisplay => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if i < self.tabs.len() {
|
||||||
|
let h = &mut self.tabs[i].scene.document.header;
|
||||||
|
h.lineweight_display = !h.lineweight_display;
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::TogglePolar => {
|
Message::TogglePolar => {
|
||||||
self.polar_mode ^= true;
|
self.polar_mode ^= true;
|
||||||
if self.polar_mode {
|
if self.polar_mode {
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,7 @@ impl H7CAD {
|
||||||
tab.scene.annotation_scale,
|
tab.scene.annotation_scale,
|
||||||
self.scale_popup_open,
|
self.scale_popup_open,
|
||||||
scale_pill_enabled,
|
scale_pill_enabled,
|
||||||
|
tab.scene.document.header.lineweight_display,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
|
|
|
||||||
|
|
@ -2631,6 +2631,9 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wide LWPolyline and Polyline2D fills
|
// Wide LWPolyline and Polyline2D fills
|
||||||
|
let [ox, oy, _] = hatch_offset;
|
||||||
|
let ox = ox as f32;
|
||||||
|
let oy = oy as f32;
|
||||||
for entity in self.document.entities() {
|
for entity in self.document.entities() {
|
||||||
let (common, fills) = match entity {
|
let (common, fills) = match entity {
|
||||||
EntityType::LwPolyline(pl) => (&pl.common, wide_lwpolyline_fills(pl)),
|
EntityType::LwPolyline(pl) => (&pl.common, wide_lwpolyline_fills(pl)),
|
||||||
|
|
@ -2653,7 +2656,14 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
base_color
|
base_color
|
||||||
};
|
};
|
||||||
for boundary in fills {
|
for mut boundary in fills {
|
||||||
|
// Wires subtract world_offset during tessellation; fills must
|
||||||
|
// match or the band drifts away from the centerline on
|
||||||
|
// drawings far from origin.
|
||||||
|
for p in boundary.iter_mut() {
|
||||||
|
p[0] -= ox;
|
||||||
|
p[1] -= oy;
|
||||||
|
}
|
||||||
models.push(HatchModel {
|
models.push(HatchModel {
|
||||||
boundary: Arc::new(boundary),
|
boundary: Arc::new(boundary),
|
||||||
pattern: hatch_model::HatchPattern::Solid,
|
pattern: hatch_model::HatchPattern::Solid,
|
||||||
|
|
@ -5899,23 +5909,29 @@ fn polyline_segment_fill(
|
||||||
[p0[0] - hw0 * nx, p0[1] - hw0 * ny],
|
[p0[0] - hw0 * nx, p0[1] - hw0 * ny],
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
// Arc segment — arc band polygon
|
// Arc segment — arc band polygon.
|
||||||
let angle = 4.0 * (bulge as f64).atan();
|
// Center math matches `bulge_to_arc` in modules/home/modify/explode.rs:
|
||||||
|
// r = chord * (1 + b²) / (4·|b|)
|
||||||
|
// d = r * (1 - b²) / (1 + b²) (signed: negative ⇒ major arc, center
|
||||||
|
// flips to the opposite side of chord)
|
||||||
|
// center = midpoint + sign(b) · d · left_perp(chord)
|
||||||
|
let b = bulge as f64;
|
||||||
|
let b2 = b * b;
|
||||||
let dx = (p1[0] - p0[0]) as f64;
|
let dx = (p1[0] - p0[0]) as f64;
|
||||||
let dy = (p1[1] - p0[1]) as f64;
|
let dy = (p1[1] - p0[1]) as f64;
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let chord_len = (dx * dx + dy * dy).sqrt();
|
||||||
if d < 1e-9 {
|
if chord_len < 1e-9 || b.abs() < 1e-12 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
let r = chord_len * (1.0 + b2) / (4.0 * b.abs());
|
||||||
|
let d_perp = r * (1.0 - b2) / (1.0 + b2);
|
||||||
let mx = ((p0[0] + p1[0]) * 0.5) as f64;
|
let mx = ((p0[0] + p1[0]) * 0.5) as f64;
|
||||||
let my = ((p0[1] + p1[1]) * 0.5) as f64;
|
let my = ((p0[1] + p1[1]) * 0.5) as f64;
|
||||||
let px = -dy / d;
|
let perp_x = -dy / chord_len;
|
||||||
let py = dx / d;
|
let perp_y = dx / chord_len;
|
||||||
let sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
let sign = b.signum();
|
||||||
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
let cx = (mx + sign * d_perp * perp_x) as f32;
|
||||||
let cx = (mx - sign * px * (r - h)) as f32;
|
let cy = (my + sign * d_perp * perp_y) as f32;
|
||||||
let cy = (my - sign * py * (r - h)) as f32;
|
|
||||||
let a0 = ((p0[1] - cy) as f32).atan2((p0[0] - cx) as f32);
|
let a0 = ((p0[1] - cy) as f32).atan2((p0[0] - cx) as f32);
|
||||||
let a1 = ((p1[1] - cy) as f32).atan2((p1[0] - cx) as f32);
|
let a1 = ((p1[1] - cy) as f32).atan2((p1[0] - cx) as f32);
|
||||||
let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) };
|
let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) };
|
||||||
|
|
@ -5923,23 +5939,26 @@ fn polyline_segment_fill(
|
||||||
ea += std::f32::consts::TAU;
|
ea += std::f32::consts::TAU;
|
||||||
}
|
}
|
||||||
let span = ea - sa;
|
let span = ea - sa;
|
||||||
let segs = ((span.abs() / std::f32::consts::TAU) * 12.0)
|
let segs = ((span.abs() / std::f32::consts::TAU) * 24.0)
|
||||||
.ceil()
|
.ceil()
|
||||||
.max(4.0) as u32;
|
.max(4.0) as u32;
|
||||||
let r = r as f32;
|
let r = r as f32;
|
||||||
let hw = (hw0 + hw1) * 0.5;
|
let r_outer = |t: f32| r + (hw0 + (hw1 - hw0) * t);
|
||||||
let r_outer = r + hw;
|
let r_inner = |t: f32| (r - (hw0 + (hw1 - hw0) * t)).max(0.0);
|
||||||
let r_inner = (r - hw).max(0.0);
|
|
||||||
let mut boundary = Vec::with_capacity((segs as usize + 1) * 2);
|
let mut boundary = Vec::with_capacity((segs as usize + 1) * 2);
|
||||||
|
let inv = 1.0 / segs as f32;
|
||||||
for j in 0..=segs {
|
for j in 0..=segs {
|
||||||
let t = sa + span * (j as f32 / segs as f32);
|
let t = j as f32 * inv;
|
||||||
boundary.push([cx + r_outer * t.cos(), cy + r_outer * t.sin()]);
|
let ang = sa + span * t;
|
||||||
|
let ro = r_outer(t);
|
||||||
|
boundary.push([cx + ro * ang.cos(), cy + ro * ang.sin()]);
|
||||||
}
|
}
|
||||||
for j in (0..=segs).rev() {
|
for j in (0..=segs).rev() {
|
||||||
let t = sa + span * (j as f32 / segs as f32);
|
let t = j as f32 * inv;
|
||||||
boundary.push([cx + r_inner * t.cos(), cy + r_inner * t.sin()]);
|
let ang = sa + span * t;
|
||||||
|
let ri = r_inner(t);
|
||||||
|
boundary.push([cx + ri * ang.cos(), cy + ri * ang.sin()]);
|
||||||
}
|
}
|
||||||
boundary.truncate(64);
|
|
||||||
Some(boundary)
|
Some(boundary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ impl StatusBar {
|
||||||
scale_popup_open: bool,
|
scale_popup_open: bool,
|
||||||
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
||||||
scale_pill_enabled: bool,
|
scale_pill_enabled: bool,
|
||||||
|
// LWDISPLAY header flag — controls lineweight visibility in the viewport.
|
||||||
|
lineweight_display: bool,
|
||||||
) -> 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()))
|
||||||
|
|
@ -103,6 +105,10 @@ impl StatusBar {
|
||||||
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
|
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
|
||||||
"Orthogonal Mode\nF8"
|
"Orthogonal Mode\nF8"
|
||||||
),
|
),
|
||||||
|
tip(
|
||||||
|
toggle_pill("LWT", lineweight_display, Message::ToggleLineweightDisplay),
|
||||||
|
"Show Lineweight\nLWDISPLAY"
|
||||||
|
),
|
||||||
polar_pill(polar_mode, polar_increment_deg),
|
polar_pill(polar_mode, polar_increment_deg),
|
||||||
tip(
|
tip(
|
||||||
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
|
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue