Attach analytical arc to polyline Arc mode rubber-band preview

This commit is contained in:
Karim Jerbi committed 2026-09-08 13:16:49 +01:00
1 parent ec3b6b89f6
commit a754fac6d4
4 files changed
+223 -8

No files matched your search

+59
View File
@@ -1265,4 +1265,63 @@ mod tests {
let _ = std::fs::remove_file(sidecar);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_pline_line_then_arc() {
use crate::app::Message;
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
{
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
}
// Start PLINE
let _ = app.update(Message::CommandInput("PLINE".to_string()));
let _ = app.update(Message::CommandSubmit);
// Click first point (100, 100)
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 100.0)));
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
// Move to (200, 100) and click second point (draw line)
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
// Switch to arc: Option 1 - CommandOptionPick("A")
let _ = app.update(Message::CommandOptionPick("A".to_string()));
let _ = app.view(wid);
println!("ENTITIES: {}", app.tabs[0].scene.document.entities().count());
println!("CMD: {:?}", app.tabs[0].active_cmd.as_ref().map(|c| c.name()));
// Move mouse!
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(201.0, 100.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 150.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 150.0)));
let _ = app.view(wid);
// Click arc point
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
// Move mouse again
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 160.0)));
// Switch to line
let _ = app.update(Message::CommandOptionPick("L".to_string()));
// Move mouse again
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 150.0)));
// Finish
let _ = app.update(Message::CommandOptionPick(String::new()));
}
}
+62 -5
View File
@@ -17,7 +17,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
// ── Ribbon definition ──────────────────────────────────────────────────────
@@ -443,6 +443,7 @@ impl CadCommand for PlineCommand {
let pt = self.plane.to_local(pt_world).as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new();
let mut tangent_geom = None;
match self.mode {
SegMode::Line => {
pts.push([last.x, last.y, last.z]);
@@ -456,8 +457,44 @@ impl CadCommand for PlineCommand {
.map(|t| t.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(a, tangent, b);
let arc_pts = arc_sample_points(last, bulge, pt, 16);
pts.extend_from_slice(&arc_pts);
if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 {
if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge)
{
for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) {
pts.push([s[0] as f32, s[1] as f32, last.z]);
}
let (sa, ea) = if arc.sweep >= 0.0 {
(arc.start_angle, arc.end_angle)
} else {
(arc.end_angle, arc.start_angle)
};
let world_center = self
.plane
.to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64));
if arc.radius > 0.0
&& arc.radius.is_finite()
&& arc.radius <= 1e6
&& sa.is_finite()
&& ea.is_finite()
{
tangent_geom = Some(TangentGeom::Arc {
center: world_center.to_array(),
axis_x: self.plane.x.to_array(),
axis_y: self.plane.y.to_array(),
radius: arc.radius,
start_angle: sa,
end_angle: ea,
});
}
} else {
let arc_pts = arc_sample_points(last, bulge, pt, 64);
pts.extend_from_slice(&arc_pts);
}
} else {
pts.push([last.x, last.y, last.z]);
pts.push([pt.x, pt.y, pt.z]);
}
}
}
@@ -465,12 +502,16 @@ impl CadCommand for PlineCommand {
.into_iter()
.map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array())
.collect();
Some(WireModel::solid(
let mut wire = WireModel::solid(
"rubber_band".into(),
world_points,
WireModel::CYAN,
false,
))
);
if let Some(tg) = tangent_geom {
wire.tangent_geoms.push(tg);
}
Some(wire)
}
}
@@ -548,4 +589,20 @@ mod tests {
CmdResult::FinalizeLiveEntity(h) if h == handle
));
}
#[test]
fn test_draw_line_then_arc() {
let mut cmd = PlineCommand::new();
cmd.on_point(DVec3::new(0.0, 0.0, 0.0));
let res = cmd.on_point(DVec3::new(10.0, 0.0, 0.0));
assert!(matches!(res, CmdResult::CommitLiveEntity(_)));
cmd.set_live_handle(Handle::new(1));
cmd.on_text_input("A");
let wire = cmd.on_mouse_move(DVec3::new(10.0, 5.0, 0.0)).expect("arc wire");
assert_eq!(wire.tangent_geoms.len(), 1);
assert!(matches!(wire.tangent_geoms[0], TangentGeom::Arc { .. }));
assert!(wire.points.len() >= 16);
let res2 = cmd.on_point(DVec3::new(10.0, 5.0, 0.0));
assert!(matches!(res2, CmdResult::UpdateLiveEntity { .. }));
}
}
+29 -3
View File
@@ -219,7 +219,33 @@ fn split_mixed_polyline(
// Emit each arc segment as an analytical wire (CircleGpu target)
for tg in tangent_geoms {
if matches!(tg, TangentGeom::Arc { .. }) {
if let TangentGeom::Arc {
center,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} = *tg
{
let mut arc_pts = Vec::with_capacity(17);
let n = 16;
let sweep = if end_angle >= start_angle {
end_angle - start_angle
} else {
end_angle - start_angle + std::f64::consts::TAU
};
for s in 0..=n {
let frac = s as f64 / n as f64;
let ang = start_angle + frac * sweep;
let p = [
center[0] + radius * (ang.cos() * axis_x[0] + ang.sin() * axis_y[0]),
center[1] + radius * (ang.cos() * axis_x[1] + ang.sin() * axis_y[1]),
center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]),
];
arc_pts.push(p);
}
let (points, points_low) = points_to_ds(arc_pts);
out.push(WireModel {
bg_adapt: None,
point_marker: None,
@@ -238,8 +264,8 @@ fn split_mixed_polyline(
dash_align_end: None,
text_verts: Vec::new(),
name: name.to_string(),
points: Vec::new(),
points_low: Vec::new(),
points,
points_low,
color,
selected,
pattern_length,
+73
View File
@@ -408,3 +408,76 @@ fn late_device_errors_invalidate_uploaded_content_and_targets() {
"the same error must not invalidate twice"
);
}
#[test]
#[ignore = "requires a GPU adapter"]
fn test_pline_arc_switch_preview_and_render() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
let depth = rustc_hash::FxHashMap::default();
use crate::command::CadCommand;
let mut cmd = crate::modules::draw::draw::polyline::PlineCommand::new();
cmd.on_point(glam::DVec3::new(0.0, 0.0, 0.0));
let _res = cmd.on_point(glam::DVec3::new(10.0, 0.0, 0.0));
cmd.set_live_handle(acadrust::Handle::new(1));
cmd.on_text_input("A");
let test_points = [
glam::DVec3::new(10.0, 0.0, 0.0),
glam::DVec3::new(10.0000001, 0.0, 0.0),
glam::DVec3::new(10.0, 0.0000001, 0.0),
glam::DVec3::new(10.0, -0.0000001, 0.0),
glam::DVec3::new(9.9999999, 0.0, 0.0),
glam::DVec3::new(15.0, 5.0, 0.0),
glam::DVec3::new(10.0, 10.0, 0.0),
glam::DVec3::new(5.0, 5.0, 0.0),
glam::DVec3::new(0.0, 0.0, 0.0),
];
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("test_target"),
size: wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
for pt in test_points {
let wire = cmd.on_mouse_move(pt);
if let Some(w) = wire {
pipeline.upload_preview_wires(&device, &queue, &[w], &depth);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("test_encoder"),
});
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
false,
false,
);
queue.submit(Some(encoder.finish()));
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
}
}
}