Merge pull request #1747 from schoeller/reference_only_entities

Reference-only entities (plugin API v9): a scene-scoped, in-memory
designation for plugin-owned display geometry, plus HostNotification
dispatch to native plugins so they can re-mark on DocumentOpened.

Resolves the new DeltaSnapshot site in block_editor.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-14 23:46:42 +03:00
commit 68330ce39e
40 files changed
+1977 -85

No files matched your search

+98 -4
View File
@@ -36,6 +36,8 @@ pub enum BlockScalePolicy {
mod block_base_point_tests;
mod boundary;
mod camera_ops;
#[cfg(test)]
mod reference_only_tests;
pub use camera_ops::{add_background, copy_background, erase_background};
pub(crate) mod centerline;
pub(crate) mod centermark;
@@ -61,6 +63,7 @@ mod page_setup;
mod paper;
mod preview;
mod project;
pub mod reference_only;
mod scene_markers;
mod selection;
pub mod viewport_dimension_pick;
@@ -84,6 +87,10 @@ pub(crate) use convert::tess::{
/// Keep construction-history curves in the same per-entity tessellation memo
/// as their parent, so navigation never rebuilds them on every frame.
///
/// `reference_only` is the scene's designation set: its membership decides
/// whether the produced wires carry the `WireModel::reference_only` click-
/// through stamp (see `scene::reference_only`).
#[allow(clippy::too_many_arguments)]
pub(crate) fn tessellate_entity(
document: &CadDocument,
@@ -97,6 +104,7 @@ pub(crate) fn tessellate_entity(
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
paper_space: bool,
reference_only: &HashSet<Handle>,
) -> Vec<WireModel> {
let mut wires = convert::tess::tessellate_entity(
document,
@@ -110,6 +118,7 @@ pub(crate) fn tessellate_entity(
view_aabb,
world_per_pixel,
paper_space,
reference_only,
);
if matches!(entity, EntityType::Solid3D(_) | EntityType::Surface(_)) {
let handle = entity.common().handle;
@@ -161,6 +170,7 @@ pub(crate) fn tessellate_entity(
view_aabb,
world_per_pixel,
paper_space,
reference_only,
)
};
for wire in &mut construction {
@@ -760,6 +770,13 @@ pub struct UndoRecording {
>,
parametric_constraints_order: Vec<parametric_constraints::ParametricScope>,
named_parameters_before: Option<named_parameters::ParameterTable>,
/// First-touch before-images of reference-only designations (API v9)
/// changed by the command — almost always empty. The designation set
/// lives on `Scene`, not in the document, so like
/// `parametric_constraints_before` it needs its own channel. Erasing a
/// designated entity drops its set entry here so undo restores entity
/// and designation together.
reference_designations_before: Vec<(Handle, bool)>,
poisoned: bool,
}
@@ -775,6 +792,7 @@ impl UndoRecording {
&& self.object_order.is_empty()
&& self.parametric_constraints_order.is_empty()
&& self.named_parameters_before.is_none()
&& self.reference_designations_before.is_empty()
}
/// Consume the recording directories in deterministic first-touch
@@ -792,6 +810,7 @@ impl UndoRecording {
parametric_constraints::ParametricConstraintSet,
)>,
Option<named_parameters::ParameterTable>,
Vec<(Handle, bool)>,
) {
let entities = self
.order
@@ -812,11 +831,13 @@ impl UndoRecording {
.map(|before| (scope, before))
})
.collect();
let reference_designations = std::mem::take(&mut self.reference_designations_before);
(
entities,
objects,
parametric_constraints,
self.named_parameters_before,
reference_designations,
)
}
@@ -2138,6 +2159,11 @@ pub struct Scene {
pub selected_constraint: Option<crate::scene::parametric_constraints::ConstraintId>,
/// Session-only ISOLATEOBJECTS / HIDEOBJECTS state. Never written to DWG/DXF.
pub object_isolation: ObjectIsolationState,
/// Scene-scoped reference-only designations (API v9): plugin-owned
/// display geometry the built-in edit paths refuse. In-memory only,
/// keyed by handle, lives and dies with this scene — never written to
/// DWG/DXF (see `scene::reference_only`).
pub(crate) reference_only: reference_only::ReferenceOnlyDesignation,
/// Entity handles temporarily removed from the base render while an
/// interactive grip preview draws their live replacement.
/// Separate from object isolation so a grip can never activate the
@@ -2782,6 +2808,7 @@ impl Scene {
selected_order: Vec::new(),
selected_constraint: None,
object_isolation: ObjectIsolationState::default(),
reference_only: Default::default(),
preview_hidden: HashSet::default(),
command_preview_hidden: HashSet::default(),
refedit_keep: None,
@@ -3434,6 +3461,26 @@ impl Scene {
}
}
/// Record a reference-only designation's value before its first change
/// within the open delta recording (API v9). The designation set lives on
/// `Scene`, outside the document, so it needs its own channel — like
/// the parametric-constraint directory above — for undo to restore the
/// designation together with the entity it belongs to. First touch wins.
pub(crate) fn record_undo_reference_designation_before(&mut self, handle: Handle) {
if let Some(recording) = self.undo_recording.as_mut() {
if !recording
.reference_designations_before
.iter()
.any(|(recorded, _)| *recorded == handle)
{
let before = self.reference_only.is_marked(handle);
recording
.reference_designations_before
.push((handle, before));
}
}
}
/// Flag the open recording as touching non-entity state (a new layer, a
/// group edit, a `*D` block record) that a pure-entity delta cannot
/// restore. The app's per-command predicate keeps this from firing.
@@ -3491,6 +3538,13 @@ impl Scene {
}
}
// History replay can apply an entity removal the designation channels
// predate (an "added" side rolled back). Sweep so the set never keeps
// an entry for an absent entity; exact designations come right after
// this from the delta's own channel.
if !self.reference_only.is_empty() {
self.reference_only.retain_present(&self.document);
}
if !changes.is_empty() {
self.invalidate_dependency_index();
}
@@ -7555,6 +7609,7 @@ impl Scene {
None,
None,
anno_scale_override.is_some(),
self.reference_only.handles(),
);
// Solid B-rep corners snap via the mesh edge list (their wire
// path is an empty shell). Enrich before the memo/assembly split
@@ -9472,6 +9527,22 @@ impl Scene {
.is_some_and(|keep| !keep.contains(&handle)))
}
/// True when `handle` addresses a reference-only entity (API v9):
/// plugin-owned display geometry the built-in edit paths treat like a
/// locked-layer entity while its owning plugin keeps write access through
/// the plugin host API. Absent entities are never reference-only.
pub fn is_reference_only(&self, handle: Handle) -> bool {
self.reference_only.is_marked(handle) && self.document.get_entity(handle).is_some()
}
/// Insert or remove `handle` in the scene-scoped designation set,
/// returning whether membership changed. Pure storage: no epoch bump, no
/// undo bookkeeping — those belong to the callers (the plugin host API
/// mutation and the history channels).
pub(crate) fn set_reference_only_membership(&mut self, handle: Handle, on: bool) -> bool {
self.reference_only.set(handle, on)
}
/// The name of the locked layer `handle` sits on, if any (for messages).
pub fn locked_layer_name(&self, handle: Handle) -> Option<String> {
let name = self
@@ -10763,6 +10834,21 @@ impl Scene {
pub fn interaction_candidate_handles(
&self,
candidates: &crate::scene::pick::interaction_index::InteractionCandidates,
) -> Option<HashSet<Handle>> {
let handles = self.interaction_candidate_handles_all(candidates);
// Reference-only entities (API v9) are plugin-owned display geometry:
// their fills, solids and instanced meshes are not pickable, so the
// click/hover fallback paths that consume this set skip them and a
// click passes through to the editable entity underneath.
handles.map(|mut handles| {
handles.retain(|h| !self.is_reference_only(*h));
handles
})
}
fn interaction_candidate_handles_all(
&self,
candidates: &crate::scene::pick::interaction_index::InteractionCandidates,
) -> Option<HashSet<Handle>> {
let mut handles: HashSet<Handle> = candidates
.iter()
@@ -11008,7 +11094,7 @@ impl Scene {
candidate_handles.as_ref(),
));
}
handles.retain(|&h| self.passes_selection_filter(h));
handles.retain(|&h| self.passes_selection_filter(h) && !self.is_reference_only(h));
handles
}
@@ -11684,6 +11770,9 @@ impl Scene {
// `world_offset` subtraction even though `current_layout != "Model"`.
// Decide based on the block being tessellated, not the layout.
let is_model_block = block_handle == self.model_space_block_handle();
// The reference-only designation set drives the wire stamp; captured
// as a shared ref like `doc`/`sel` so the rayon workers can read it.
let reference_only_set = self.reference_only.handles();
let bg = if self.current_layout == "Model" {
self.bg_color
} else {
@@ -11800,6 +11889,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
);
(e.common().handle, Arc::new(w))
})
@@ -11846,6 +11936,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
)
})
.collect();
@@ -11880,6 +11971,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
)),
Ok(None) => Some(Vec::new()),
Err(()) => None,
@@ -12900,6 +12992,7 @@ vis_index={:.1} visible_probe={:.1}",
None,
None,
paper,
self.reference_only.handles(),
);
// Same solid-corner snaps as the resident paths, so one-off consumers
// (overlay changed-wires, edit previews) agree with hit-test/snap.
@@ -13220,7 +13313,8 @@ pub(crate) fn drop_layout_viewports(document: &mut codec::CadDocument, block: Ha
}
/// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a viewport.
static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
pub fn layout_create_viewport() -> bool {
LAYOUT_CREATE_VIEWPORT.load(std::sync::atomic::Ordering::Relaxed)
@@ -14507,7 +14601,7 @@ mod delta_undo_tests {
let handle = scene.add_entity(EntityType::RasterImage(image));
let rec = scene.take_undo_recording().unwrap();
assert!(!rec.is_poisoned());
let (entities, objects, _parametric_constraints, _named_parameters) =
let (entities, objects, _parametric_constraints, _named_parameters, _designations) =
rec.into_recorded_images();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, handle);
@@ -14532,7 +14626,7 @@ mod delta_undo_tests {
scene.erase_entities(&[h1]);
let rec = scene.take_undo_recording().unwrap();
assert!(!rec.is_poisoned());
let (entities, objects, _parametric_constraints, _named_parameters) =
let (entities, objects, _parametric_constraints, _named_parameters, _designations) =
rec.into_recorded_images();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, h1);