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

+164 -7
View File
@@ -2186,6 +2186,11 @@ pub struct Scene {
frozen_hatch_cache: RefCell<HashMap<(Handle, String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
frozen_wipeout_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>,
/// Point clouds shown by the model content, keyed by target block and
/// frozen-layer signature, per geometry epoch.
point_cloud_cache: RefCell<
HashMap<(Handle, u64), (u64, Arc<crate::scene::model::point_cloud::PointCloudSet>)>,
>,
frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
/// Viewports that carry layer color/alpha/linetype/lineweight overrides.
/// Cached per geometry epoch so ordinary viewports can share render data.
@@ -2648,6 +2653,7 @@ impl Scene {
frozen_hatch_cache: RefCell::new(HashMap::default()),
frozen_wipeout_cache: RefCell::new(HashMap::default()),
frozen_image_cache: RefCell::new(HashMap::default()),
point_cloud_cache: RefCell::new(HashMap::default()),
frozen_mesh_cache: RefCell::new(HashMap::default()),
viewport_style_override_cache: RefCell::new(None),
insert_hatch_cache: RefCell::new(None),
@@ -8338,6 +8344,135 @@ impl Scene {
arc
}
/// Point clouds of the model content, with a content viewport's frozen
/// layers removed. The paper sheet shows none. The set is rebuilt per
/// geometry epoch; unchanged clouds keep their placed points (and GPU
/// buffers) by key.
pub(super) fn point_clouds_for_viewport(
&self,
frozen: &HashSet<Handle>,
paper_sheet: bool,
) -> Arc<crate::scene::model::point_cloud::PointCloudSet> {
use crate::scene::model::point_cloud;
static EMPTY: std::sync::OnceLock<Arc<point_cloud::PointCloudSet>> =
std::sync::OnceLock::new();
let empty = || Arc::clone(EMPTY.get_or_init(Default::default));
if paper_sheet {
return empty();
}
let target_block = self.content_render_block_handle();
let key = (target_block, Self::frozen_layers_sig(frozen));
if let Some((epoch, arc)) = self.point_cloud_cache.borrow().get(&key) {
if *epoch == self.geometry_epoch {
return Arc::clone(arc);
}
}
let arc = if point_cloud::any_definition(&self.document) {
let depths = rustc_hash::FxHashMap::default();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
(!frozen.is_empty()).then_some(frozen),
None,
true,
&depths,
);
let mut clouds = Vec::new();
graph.walk_root(
self.render_scene_root(target_block),
|entity, context| {
context.is_instanced() || !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
let EntityType::Extended(extended) = entity else {
return;
};
let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data
else {
return;
};
let transform = context.is_instanced().then_some(&context.transform);
// Object Color stylization draws the cloud in its own colour.
let common = entity.common();
let color = if matches!(common.color, codec::types::Color::ByLayer) {
self.document
.layers
.get(&common.layer)
.map(|layer| layer.color.clone())
.unwrap_or(common.color.clone())
} else {
common.color.clone()
};
let object_color = color.rgb().map_or([255; 3], |(r, g, b)| [r, g, b]);
clouds.extend(point_cloud::placed(
&self.document,
data,
object_color,
transform,
&point_cloud::hidden(data),
));
},
);
if clouds.is_empty() {
empty()
} else {
Arc::new(point_cloud::PointCloudSet {
point_size: point_cloud::point_size(&self.document),
clouds,
})
}
} else {
empty()
};
self.point_cloud_cache
.borrow_mut()
.insert(key, (self.geometry_epoch, Arc::clone(&arc)));
arc
}
/// Wireframe 2D draws no cloud points: each shown cloud whose file is
/// found draws its extents box and a message instead, unless
/// POINTCLOUD2DVSDISPLAY is 1.
// ponytail: model-space clouds only, rebuilt per frame; cache by epoch if
// drawings with many clouds make it show.
pub(super) fn point_cloud_2d_wires(&self) -> Vec<WireModel> {
use crate::scene::model::point_cloud::{resolve_source, setting, setting_value};
if !crate::scene::model::point_cloud::any_definition(&self.document)
|| setting("POINTCLOUD2DVSDISPLAY").is_some_and(|s| setting_value(&self.document, s) == 1)
{
return Vec::new();
}
let mut wires = Vec::new();
for entity in self.document.entities() {
let EntityType::Extended(extended) = entity else {
continue;
};
let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data else {
continue;
};
let common = entity.common();
if common.invisible
|| self.entity_temporarily_hidden(common.handle)
|| self.document.layers.get(&common.layer).is_some_and(|layer| layer.is_off() || layer.is_frozen())
|| resolve_source(&self.document, data).is_none()
{
continue;
}
let color = if matches!(common.color, codec::types::Color::ByLayer) {
self.document.layers.get(&common.layer).map(|layer| layer.color.clone()).unwrap_or(common.color.clone())
} else {
common.color.clone()
};
let (r, g, b) = color.rgb().unwrap_or((255, 255, 255));
wires.push(WireModel::solid_f64(
common.handle.value().to_string(),
crate::entities::extended::point_cloud_2d_style_lines(data),
[f32::from(r) / 255.0, f32::from(g) / 255.0, f32::from(b) / 255.0, 1.0],
false,
));
}
wires
}
/// Image / OLE models for a content viewport, with its frozen layers removed.
pub(super) fn images_for_viewport(
&self,
@@ -8408,14 +8543,19 @@ impl Scene {
arc
}
/// True when `handle`'s entity sits on a locked layer. Locked objects stay
/// visible, snappable and selectable, but mutation paths must skip them.
/// True when `handle`'s entity sits on a locked layer, or is a point cloud
/// locked on its own. Locked objects stay visible, snappable and
/// selectable, but mutation paths must skip them.
pub fn is_layer_locked(&self, handle: Handle) -> bool {
self.document
.get_entity(handle)
.map(|e| e.common().layer.clone())
.and_then(|name| self.document.layers.get(&name).map(|l| l.is_locked()))
.unwrap_or(false)
let Some(entity) = self.document.get_entity(handle) else {
return false;
};
is_locked_point_cloud(entity)
|| self
.document
.layers
.get(&entity.common().layer)
.is_some_and(|l| l.is_locked())
}
/// The name of the locked layer `handle` sits on, if any (for messages).
@@ -13833,3 +13973,20 @@ mod update_entities_batch_tests {
}
}
}
pub(crate) fn is_point_cloud(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Extended(extended)
if matches!(extended.data, codec::entities::ExtendedEntityData::PointCloudEx(_))
)
}
/// A point cloud whose Locked property is set: edits leave it alone.
pub(crate) fn is_locked_point_cloud(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Extended(extended)
if matches!(&extended.data, codec::entities::ExtendedEntityData::PointCloudEx(data) if data.locked)
)
}