forked from 1b-agent/opencadstudio-upstream
Merge branch 'main' into feature/parametric-constraint-system
Brings in 54 commits from origin/main: a selection-performance overhaul, surface thickening, GPU-native wide/tapered stroke rendering, continued ocs_doc_api evolution, and assorted fixes/polish (block double-click editing, DWG hatch/database repair, clipboard, collapsible Properties sections). Six real conflicts, all where this branch had independently re-applied a fix upstream later revised or already carried natively: - src/app/automation.rs, src/app/command_driver.rs: combined both sides — kept this branch's sketch-constraint/named-parameter loading on "new"/"open", adopted main's Scene::clear()/material_base_dir and its newer rebuild_derived_caches() over the older bump_geometry(). Scene::clear() (src/scene/entity.rs) now also resets sketch_constraints/named_parameters, so every caller gets this for free instead of each call site repeating it. - src/io/mod.rs, src/scene/render_graph.rs: adopted main's design outright (commit973af87d, authored by Hakan directly) — a large MINSERT is no longer purged as "corrupt" at load, only its rendering is bounded, preserving the source data. This branch's own earlier design (reject at load) predates that revision. - src/scene/pipeline/mesh_gpu.rs: adopted main's valid_rgba_size() guard (a real robustness improvement over this branch's version). - src/snap.rs: replaced wholesale with main's version — this branch's copy was an explicit verbatim snapshot from an earlier upstream PR (commit4c264b8c) with zero constraint-system-specific content, and main has since evolved it considerably further. Also removed two now-dead duplicates the merge surfaced without a conflict marker (both branches had independently fixed the same things): the OPTIONS/OP command dispatch/autocomplete entries, and automation.rs's open_finalizes_and_purges_like_the_ui_open_path test (kept main's version, which covers strictly more: solid_models/ material_base_dir reset on "new"). Full workspace suite passes (812 lib tests + this branch's own 24 constraint/persistence integration tests) except the one pre-existing, unrelated font-test failure already present on main. Zero clippy findings in the hand-written merge resolution itself; the ~1,171 warnings clippy reports are pre-existing in the 54 absorbed commits, not introduced here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
d675244fb8
89 files changed
+5700
-1101
No files matched your search
+220
-1
@@ -353,7 +353,32 @@ impl OpenCADStudio {
|
||||
self.reset_tracking_after_point();
|
||||
self.push_ucs_to_cmd(i);
|
||||
}
|
||||
if let StepInput::EntityPick(handle, _) = &input {
|
||||
if let StepInput::EntityPick(handle, point) = &input {
|
||||
let solid_pick = matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(
|
||||
acadrust::EntityType::Solid3D(_)
|
||||
| acadrust::EntityType::Surface(_)
|
||||
| acadrust::EntityType::Region(_)
|
||||
| acadrust::EntityType::Body(_)
|
||||
)
|
||||
);
|
||||
let direction = if solid_pick {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.solid_planar_face_normal_at(*handle, *point)
|
||||
} else {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.and_then(crate::entities::curve::entity_curve)
|
||||
.and_then(|curve| curve.plane.normal())
|
||||
.map(glam::DVec3::from_array)
|
||||
};
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.set_entity_pick_direction(direction);
|
||||
}
|
||||
if self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.inject_before_entity_pick())
|
||||
{
|
||||
@@ -4099,6 +4124,102 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
CmdResult::ThickenEntities { handles, distance } => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("No surfaces selected.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
if !distance.is_finite() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Requires numeric distance or two points.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if distance.abs() <= f64::EPSILON {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
let pending = self.begin_undo(i, "THICKEN", handles.len(), true);
|
||||
let mut created = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut self_intersections = 0usize;
|
||||
for handle in handles {
|
||||
let Some(acadrust::EntityType::Surface(surface)) =
|
||||
self.tabs[i].scene.document.get_entity(handle)
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let kernel_distance = if surface.kind
|
||||
== acadrust::entities::SurfaceKind::Revolved
|
||||
{
|
||||
-distance
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
let body = self.tabs[i]
|
||||
.scene
|
||||
.solid_models
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||
});
|
||||
let Some(body) = body else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let solid = match cadkernel::brep::thicken(&body, kernel_distance) {
|
||||
Ok(solid) => solid,
|
||||
Err(cadkernel::brep::ThickenError::SelfIntersection) => {
|
||||
failed += 1;
|
||||
self_intersections += 1;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let history = crate::scene::model::solid_history::brep_op(&solid);
|
||||
if self
|
||||
.add_solid_model(empty_solid3d(), solid, history)
|
||||
.is_null()
|
||||
{
|
||||
failed += 1;
|
||||
} else {
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if created > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
if self_intersections > 0 {
|
||||
self.command_line.push_error(&crate::tf!(
|
||||
"{} surface(s) cannot be thickened because the offset intersects itself.",
|
||||
self_intersections
|
||||
));
|
||||
}
|
||||
if failed > 0 {
|
||||
self.command_line.push_output(&crate::tf!(
|
||||
"THICKEN: created {} solid(s); {} surface(s) could not be thickened exactly.",
|
||||
created, failed
|
||||
));
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
CmdResult::PresspullPick { handle, point, offset, multiple: _ } => {
|
||||
self.presspull_pick(handle, point, offset);
|
||||
}
|
||||
@@ -6345,3 +6466,101 @@ mod sketch_constraint_undo_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod thicken_tests {
|
||||
use super::*;
|
||||
use acadrust::entities::{Surface, SurfaceKind};
|
||||
use cadkernel::geom2d::{Circle, Curve, NurbsCurve};
|
||||
use cadkernel::space::{Parameterization, Plane};
|
||||
|
||||
#[test]
|
||||
fn thicken_preserves_sources_and_round_trips_results_with_one_undo() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let body = cadkernel::brep::planar_region(Plane::XY, &[
|
||||
vec![Curve::Circle(Circle { centre: [0.0; 2], radius: 3.0 })],
|
||||
]).unwrap();
|
||||
let source = app.add_surface_model(acadrust::EntityType::Surface(Surface::new(SurfaceKind::Plane)), body);
|
||||
assert!(!source.is_null());
|
||||
let i = app.active_tab;
|
||||
let invalid = app.tabs[i].scene.add_entity(acadrust::EntityType::Surface(Surface::new(SurfaceKind::Generic)));
|
||||
let original = serde_json::to_value(app.tabs[i].scene.document.get_entity(source).unwrap()).unwrap();
|
||||
let before = app.tabs[i].history.undo_stack.len();
|
||||
let _ = app.apply_cmd_result(CmdResult::ThickenEntities { handles: vec![source], distance: 0.0 });
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), before);
|
||||
let _ = app.apply_cmd_result(CmdResult::ThickenEntities { handles: vec![invalid, source], distance: 2.0 });
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), before + 1);
|
||||
let result = app.tabs[i].scene.document.entities().find_map(|entity| {
|
||||
matches!(entity, acadrust::EntityType::Solid3D(_)).then_some(entity.common().handle)
|
||||
}).unwrap();
|
||||
assert!(app.tabs[i].scene.document.solid_history_operation(result).is_some());
|
||||
assert_eq!(serde_json::to_value(app.tabs[i].scene.document.get_entity(source).unwrap()).unwrap(), original);
|
||||
let expected = cadkernel::brep::analytic_mass_properties(&app.tabs[i].scene.solid_models[&result]).unwrap().volume;
|
||||
let bytes = crate::io::save_to_bytes(&app.tabs[i].scene.document, "dwg", app.tabs[i].scene.document.version).unwrap();
|
||||
let document = crate::io::load_bytes("thicken.dwg", bytes).unwrap();
|
||||
let mut restored = crate::scene::Scene::new();
|
||||
restored.document = document;
|
||||
restored.restore_solid_models(&[result]);
|
||||
let actual = cadkernel::brep::analytic_mass_properties(&restored.solid_models[&result]).unwrap().volume;
|
||||
assert!((expected - actual).abs() < 1e-8);
|
||||
app.undo_active_tab();
|
||||
assert!(app.tabs[i].scene.document.get_entity(result).is_none());
|
||||
assert!(app.tabs[i].scene.document.solid_history_operation(result).is_none());
|
||||
assert!(app.tabs[i].scene.document.get_entity(source).is_some());
|
||||
assert!(app.tabs[i].scene.document.get_entity(invalid).is_some());
|
||||
app.redo_active_tab();
|
||||
assert!(app.tabs[i].scene.document.get_entity(result).is_some());
|
||||
assert!(app.tabs[i].scene.document.solid_history_operation(result).is_some());
|
||||
assert_eq!(serde_json::to_value(app.tabs[i].scene.document.get_entity(source).unwrap()).unwrap(), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thicken_creates_a_solid_from_a_free_form_surface() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let profile = NurbsCurve::interpolate(
|
||||
&[[0.0, 0.0], [0.7, 0.15], [1.3, -0.1], [2.0, 0.0]],
|
||||
None,
|
||||
None,
|
||||
Parameterization::Chord,
|
||||
)
|
||||
.unwrap();
|
||||
let body = cadkernel::brep::extrude_surface(
|
||||
Plane::XY,
|
||||
&[Curve::Nurbs(profile)],
|
||||
[0.0, 0.0, 2.0],
|
||||
)
|
||||
.unwrap();
|
||||
let source = app.add_surface_model(
|
||||
acadrust::EntityType::Surface(Surface::new(SurfaceKind::Generic)),
|
||||
body,
|
||||
);
|
||||
let i = app.active_tab;
|
||||
|
||||
let _ = app.apply_cmd_result(CmdResult::ThickenEntities {
|
||||
handles: vec![source],
|
||||
distance: 0.15,
|
||||
});
|
||||
|
||||
let solids = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|entity| {
|
||||
matches!(entity, acadrust::EntityType::Solid3D(_))
|
||||
.then_some(entity.common().handle)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(solids.len(), 1);
|
||||
let solid = &app.tabs[i].scene.solid_models[&solids[0]];
|
||||
assert!(solid.validate().is_empty());
|
||||
assert!(solid.edges.iter().all(|(_, edge)| edge.coedges.len() == 2));
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.solid_history_operation(solids[0])
|
||||
.is_some());
|
||||
assert!(app.tabs[i].scene.document.get_entity(source).is_some());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user