Merge pull request #1553 from ramox81/codex/pointcloud-attach

Point clouds: drawn from their scans, attached, cropped and stylized.

The fork's codec patch is resolved to the upstream pins: the point cloud
records landed in opencadcodec 5ef9376, carried through opencadkernel
68eeda1 and opencadgraph e204966.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-01 01:49:25 +03:00
commit 3dcebf0dc6
81 files changed
+9162 -305

No files matched your search

+20
View File
@@ -2369,6 +2369,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
return self.on_dynamic_dimension_choice(field, &value);
}
let i = self.active_tab;
if field == "ext_pcx_locked" {
let handles = self.point_cloud_lock_targets(i);
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(codec::EntityType::Extended(extended)) =
app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::entities::traits::PropertyEditable::apply_geom_prop(extended, field, &value);
}
});
self.tabs[i].properties.edit_choice_open = false;
return Task::none();
}
let handles = self.property_target_handles(i);
// The Annotative Yes/No list drives the per-object annotative toggle
// (MTEXT's own flag, the annotation context for the rest).
@@ -3607,6 +3619,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
};
// Underlay rows: reject what the reference rejects, and
// write a Width / Height as the scale that produces it.
if field.starts_with("ext_pcx_") {
if let Err(message) =
crate::entities::extended::validate_point_cloud_property(field, &val)
{
self.command_line.push_error(message);
return Task::none();
}
}
if field.starts_with("ul_") {
if let Err(message) =
crate::entities::underlay::validate_property(field, &val)
+18
View File
@@ -630,6 +630,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::NodeGraph => {
self.show_node_graph = false;
}
PanelId::PointCloudManager => {
self.pc_manager.show = false;
}
PanelId::Properties => {
self.show_properties = false;
self.ribbon.set_properties(false);
@@ -734,6 +737,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
}
}
@@ -1017,6 +1021,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
}
self.tabs[i].scene.reseed_underlays();
// Point clouds redraw as unloaded (box and saved path).
self.tabs[i].scene.bump_geometry();
}
XrefPaletteOp::Reload => {
let base_dir: std::path::PathBuf = host
@@ -1058,6 +1064,18 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.tabs[i].xref_unloaded.remove(key);
}
self.tabs[i].scene.reseed_underlays();
} else if *kind == crate::io::xref_model::RefKind::PointCloud {
// A point cloud reloads by setting its definition loaded.
for (key, row_name, row_kind, _) in &picked {
if row_kind == kind && row_name == name {
crate::io::xref::set_point_cloud_loaded(
&mut self.tabs[i].scene.document,
codec::types::Handle::new(*key),
true,
);
}
}
self.tabs[i].scene.bump_geometry();
} else if *kind != crate::io::xref_model::RefKind::DwgXref {
self.command_line.push_error(crate::tf!(
"{}: reload applies to drawing references only.",
+7
View File
@@ -1429,6 +1429,7 @@ impl OpenCADStudio {
}
message @ (Message::AttachPick
| Message::UnderlayAttachPick(_)
| Message::PointCloudAttachPick
| Message::AttachPickResult(_)
| Message::XrefAttach(_)
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
@@ -1805,6 +1806,7 @@ impl OpenCADStudio {
}
Message::PdfDialog(message) => self.update_pdf_dialog(message),
Message::PcManager(message) => self.update_pc_manager(message),
Message::RibbonSelectTab(idx) => {
self.ribbon.select(idx);
Task::none()
@@ -8494,6 +8496,11 @@ impl OpenCADStudio {
}
Message::CloseModal => {
// Cancelling the section settings drops the waiting extraction.
if self.active_modal == Some(super::ModalKind::PcSection) {
crate::modules::insert::pc_extract::drop_job();
self.pc_section = None;
}
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
return self.update(Message::RecoveryDecline);
}
+101 -2
View File
@@ -167,6 +167,29 @@ fn cursor_on_projected_axis(
.then_some(origin + direction * distance as f64)
}
/// The point under `cursor` on the plane through `origin` with `normal`.
fn cursor_on_plane(
cursor: Point,
bounds: iced::Rectangle,
view: glam::Mat4,
eye: glam::DVec3,
origin: glam::DVec3,
normal: glam::DVec3,
) -> Option<glam::DVec3> {
let inverse = view.as_dmat4().inverse();
let x = cursor.x as f64 / bounds.width as f64 * 2.0 - 1.0;
let y = 1.0 - cursor.y as f64 / bounds.height as f64 * 2.0;
let near = inverse.project_point3(glam::DVec3::new(x, y, 0.0));
let far = inverse.project_point3(glam::DVec3::new(x, y, 1.0));
let direction = far - near;
let along = direction.dot(normal);
if along.abs() <= 1e-12 {
return None;
}
let t = ((origin - eye) - near).dot(normal) / along;
t.is_finite().then(|| eye + near + direction * t)
}
fn is_added_polyline_vertex(
original: &AcadEntityType,
current: &AcadEntityType,
@@ -635,6 +658,36 @@ impl OpenCADStudio {
.find(|(owner, grip)| *owner == handle && grip.id == grip_id)
.and_then(|(_, grip)| grip.axis);
// A move-gizmo part moves its cloud alone, along its axis or plane.
let gizmo_shape = self.tabs[i]
.selected_grip_handles
.iter()
.copied()
.zip(self.tabs[i].selected_grips.iter())
.find(|(owner, grip)| *owner == handle && grip.id == grip_id)
.map(|(_, grip)| grip.shape)
.filter(|shape| {
matches!(
shape,
crate::scene::model::object::GripShape::GizmoAxis(_)
| crate::scene::model::object::GripShape::GizmoPlane(_)
)
});
if let Some(shape) = gizmo_shape {
self.tabs[i].hot_grips.clear();
let mut edit = GripEdit::single(handle, grip_id, true, world);
edit.axis = axis;
edit.gizmo = true;
if let crate::scene::model::object::GripShape::GizmoPlane(k) = shape {
let (a, b) = crate::scene::pick::grip::gizmo_plane_axes(k);
edit.plane = Some(
crate::scene::pick::grip::gizmo_axis(a)
.cross(crate::scene::pick::grip::gizmo_axis(b)),
);
}
return edit;
}
let clicked_is_hot = self.tabs[i].hot_grips.contains(&(handle, grip_id));
let mut targets: Vec<GripTarget> = if clicked_is_hot {
@@ -716,6 +769,9 @@ impl OpenCADStudio {
axis,
targets,
rectangle_frame: None,
plane: None,
grab: None,
gizmo: false,
}
}
@@ -927,6 +983,10 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("Visual style: {label}").as_ref());
// The point cloud move gizmo follows the visual style.
if i == self.active_tab {
self.refresh_selected_grips();
}
Task::none()
}
@@ -1599,6 +1659,28 @@ impl OpenCADStudio {
}
}
if let Some(normal) = grip.plane {
if snap_hit.is_none_or(|hit| hit.snap_type == crate::snap::SnapType::Grid) {
snapped = cursor_on_plane(p, bounds, view_rot, eye, grip.origin_world, normal)
.unwrap_or(snapped);
}
}
if grip.gizmo {
// The cloud follows the cursor by the offset it was grabbed
// at (measured on the press; the first move as a fallback).
let grab = match grip.grab {
Some(grab) => grab,
None => {
let grab = snapped - grip.origin_world;
if let Some(active) = self.tabs[i].active_grip.as_mut() {
active.grab = Some(grab);
}
grab
}
};
snapped -= grab;
}
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
// The overlay builds the active tracking guide from `otrack_active.base`
// to `last_cursor_world`. Keep it synchronized with the actual point used
@@ -3557,8 +3639,25 @@ impl OpenCADStudio {
self.grip_popup = None;
return Task::none();
}
self.tabs[i].active_grip =
Some(self.grip_edit_for_hit(i, handle, grip_id, is_translate, world));
let mut edit = self.grip_edit_for_hit(i, handle, grip_id, is_translate, world);
if edit.gizmo {
// Where the gizmo part was grabbed: the drag keeps
// that offset from the cloud's insertion point.
let (view_rot, eye) = match &edit_cam {
Some(cam) => (cam.view_proj_rte(bounds), cam.eye()),
None => {
let cam = self.tabs[i].scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
}
};
let grabbed = match (edit.axis, edit.plane) {
(Some(axis), _) => cursor_on_projected_axis(p, bounds, view_rot, eye, world, axis),
(_, Some(normal)) => cursor_on_plane(p, bounds, view_rot, eye, world, normal),
_ => None,
};
edit.grab = grabbed.map(|point| point - world);
}
self.tabs[i].active_grip = Some(edit);
self.grip_hover = None;
self.grip_popup = None;
self.tabs[i]