forked from 1b-agent/opencadstudio-upstream
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:
1 parent
06ee0798a8
commit
50db12b169
17 files changed
+623
-232
No files matched your search
+70
-5
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user