refactor(scene): reference-only designation becomes a scene-scoped set (API v9)

Per the upstream review of the XDATA-based storage: the host no longer writes
its own state into the saved file. The marker record under the host-owned OCS
APPID, the on-first-use APPID registration and the verbatim-EED rewrite
handling are removed; the single source of truth is a scene-scoped in-memory
set of entity handles (Scene::reference_only) that dies with the drawing tab,
so a reopened file starts undesignated and the owning plugin re-marks on the
document-opened notification.

All behaviour sites are unchanged, driven by the set: the wire stamp moves
from an XDATA read to a set lookup threaded through the tessellation
pipelines (tessellate_entity and the dimension text path take the handle set
as a plain parameter), and the pick filters, selection retains and
update/erase refusals read Scene::is_reference_only.

Undo keeps the entity-write contract: set mutations are recorded as explicit
first-touch before/after values on each history delta (the parametric-
constraints channel pattern). A designation inside a plugin push_undo scope
joins it, a scope-less change is one step of its own, an idempotent re-mark
records nothing, erasing a designated entity drops the set entry with the
entity, and undo/redo restores entity and designation together.

The round-trip test is inverted: reference_only_designation_is_not_written_
to_dwg_or_dxf now asserts the marker appears in neither format's bytes, that
no OCS APPID or marker record survives a reload, and that the designation
stays live in memory.
This commit is contained in:
Sebastian committed 2026-10-14 21:12:17 +02:00
1 parent 06ee0798a8
commit 50db12b169
17 files changed
+623 -232

No files matched your search

+70 -5
View File
@@ -82,6 +82,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,
@@ -95,6 +99,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,
@@ -108,6 +113,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;
@@ -159,6 +165,7 @@ pub(crate) fn tessellate_entity(
view_aabb,
world_per_pixel,
paper_space,
reference_only,
)
};
for wire in &mut construction {
@@ -758,6 +765,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,
}
@@ -773,6 +787,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
@@ -790,6 +805,7 @@ impl UndoRecording {
parametric_constraints::ParametricConstraintSet,
)>,
Option<named_parameters::ParameterTable>,
Vec<(Handle, bool)>,
) {
let entities = self
.order
@@ -810,11 +826,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,
)
}
@@ -2062,6 +2080,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
@@ -2694,6 +2717,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,
@@ -3341,6 +3365,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.
@@ -3398,6 +3442,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();
}
@@ -7250,6 +7301,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
@@ -9153,9 +9205,15 @@ impl Scene {
/// 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.document
.get_entity(handle)
.is_some_and(reference_only::entity_is_reference_only)
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).
@@ -11347,6 +11405,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 {
@@ -11470,6 +11531,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
);
(e.common().handle, Arc::new(w))
})
@@ -11509,6 +11571,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
)
})
.collect();
@@ -11543,6 +11606,7 @@ impl Scene {
view_aabb,
wpp,
paper,
reference_only_set,
)),
Ok(None) => Some(Vec::new()),
Err(()) => None,
@@ -12563,6 +12627,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.
@@ -14048,7 +14113,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);
@@ -14073,7 +14138,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);