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

+24
View File
@@ -766,6 +766,7 @@ impl HistorySnapshot {
.saturating_add(d.named_parameters.as_ref().map_or(0, |(before, after)| {
before.len().saturating_add(after.len()).saturating_mul(128)
}))
.saturating_add(d.reference_designations.len().saturating_mul(16))
.saturating_add(d.label.len()),
HistorySnapshot::ObjectVisibility(v) => v
.before
@@ -818,6 +819,12 @@ pub(super) struct DeltaSnapshot {
/// `document.objects`, so it needs its own channel here rather than
/// riding along with `entities`/`structure`. Almost always empty.
pub(super) parametric_constraints: Vec<ParametricConstraintsEntryDelta>,
/// Reference-only designations (API v9) this same command changed — set
/// state on `Scene` (see `scene::reference_only`), recorded as explicit
/// first-touch before/afters so undo/redo restores the designation with
/// the entity it belongs to (e.g. a plugin scope that both erases and
/// re-designates). Almost always empty.
pub(super) reference_designations: Vec<ReferenceDesignationDelta>,
/// Drawing-wide parameter table changed by this transaction.
pub(super) named_parameters: Option<(
crate::scene::named_parameters::ParameterTable,
@@ -892,6 +899,17 @@ pub(super) struct ParametricConstraintsEntryDelta {
pub(super) after: crate::scene::parametric_constraints::ParametricConstraintSet,
}
/// One reference-only designation's before/after value within an entity delta
/// (API v9). The designation set lives on `Scene`, not in the document, so
/// the value rides its own channel — symmetric like every other delta field,
/// undo applies the before value, redo the after value.
#[derive(Clone)]
pub(super) struct ReferenceDesignationDelta {
pub(super) handle: Handle,
pub(super) before: bool,
pub(super) after: bool,
}
#[derive(Default)]
pub(super) struct HistoryState {
pub(super) undo_stack: Vec<HistorySnapshot>,
@@ -907,6 +925,12 @@ pub(super) struct PendingHistorySnapshot {
pub(super) dirty_before: bool,
pub(super) structure_before: CadDocument,
pub(super) recorder: Arc<codec::document::EntityChangeRecorder>,
/// First-touch before-values of reference-only designations (API v9)
/// mutated inside this plugin scope (see `scene::reference_only`). Set
/// mutations are not document writes, so the scope closes by reading the
/// set's current state as the after-image and keeping entries whose value
/// actually flipped.
pub(super) reference_designations_before: Vec<(Handle, bool)>,
/// Kept as an undo step even when nothing changed.
pub(super) keep_empty: bool,
}
+84 -5
View File
@@ -1,8 +1,8 @@
use super::{
document::{
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
ParametricConstraintsEntryDelta, PendingHistorySnapshot, StructureSnapshot,
TableEntryDelta,
ParametricConstraintsEntryDelta, PendingHistorySnapshot, ReferenceDesignationDelta,
StructureSnapshot, TableEntryDelta,
},
OpenCADStudio,
};
@@ -226,6 +226,7 @@ impl OpenCADStudio {
active_layer: None,
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: label.into(),
};
@@ -313,8 +314,26 @@ impl OpenCADStudio {
let selected_after: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let active_layer_after = self.tabs[i].active_layer.clone();
// The designation set is scene state mutated through the plugin API:
// a scope that (un)marked entities carries the first-touch before
// values here; the after value is whatever the set holds now, and
// only untouched-value entries drop out (an idempotent re-mark never
// got recorded in the first place).
let reference_designations: Vec<_> = pending
.reference_designations_before
.into_iter()
.filter_map(|(handle, before)| {
let after = self.tabs[i].scene.reference_only.is_marked(handle);
(before != after).then_some(ReferenceDesignationDelta {
handle,
before,
after,
})
})
.collect();
if !pending.keep_empty
&& entities.is_empty()
&& reference_designations.is_empty()
&& !structure_changed
&& pending.selected_before == selected_after
&& pending.dirty_before == dirty_after
@@ -335,6 +354,7 @@ impl OpenCADStudio {
structure: structure_changed
.then_some(StructureSnapshot::Full(pending.structure_before)),
parametric_constraints: Vec::new(),
reference_designations,
named_parameters: None,
label: pending.label,
};
@@ -375,10 +395,33 @@ impl OpenCADStudio {
dirty_before,
structure_before,
recorder,
reference_designations_before: Vec::new(),
keep_empty: false,
});
}
/// Record the pre-mutation value of a reference-only designation (API v9)
/// into the open plugin pending scope — first touch wins. No-op when no
/// scope is open: like the XRecord methods, a designation change must open
/// its own scope before mutating, so by the time this runs a scope either
/// covers it or the caller owns one.
pub(super) fn record_reference_designation_before_pending(
&mut self,
i: usize,
handle: Handle,
before: bool,
) {
if let Some(pending) = self.tabs[i].history.pending.as_mut() {
if !pending
.reference_designations_before
.iter()
.any(|(recorded, _)| *recorded == handle)
{
pending.reference_designations_before.push((handle, before));
}
}
}
/// [`Self::push_undo_snapshot`] for a command that is an undo step even
/// when it changes nothing (UNDO and U then name it and undo nothing).
pub(super) fn push_undo_mark(&mut self, i: usize, label: impl Into<String>) {
@@ -466,6 +509,7 @@ impl OpenCADStudio {
active_layer: None,
structure: Some(StructureSnapshot::Layers(entries)),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: pending.label,
};
@@ -523,6 +567,7 @@ impl OpenCADStudio {
active_layer: None,
structure: Some(StructureSnapshot::TextStyles(entries)),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: pending.label,
};
@@ -580,6 +625,7 @@ impl OpenCADStudio {
active_layer: None,
structure: Some(StructureSnapshot::DimStyles(entries)),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: pending.label,
};
@@ -645,6 +691,7 @@ impl OpenCADStudio {
active_layer: None,
structure: Some(StructureSnapshot::Objects(entries)),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: pending.label,
};
@@ -680,6 +727,7 @@ impl OpenCADStudio {
object_handles,
}),
parametric_constraints: Vec::new(),
reference_designations: Vec::new(),
named_parameters: None,
label: "STYLE".to_string(),
};
@@ -761,8 +809,13 @@ impl OpenCADStudio {
pending.label
);
}
let (entity_before, object_before, parametric_constraints_before, named_parameters_before) =
rec.into_recorded_images();
let (
entity_before,
object_before,
parametric_constraints_before,
named_parameters_before,
reference_designations_before,
) = rec.into_recorded_images();
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> =
entity_before
.into_iter()
@@ -808,6 +861,20 @@ impl OpenCADStudio {
let after = self.tabs[i].scene.named_parameters().clone();
(before != after).then_some((before, after))
});
// Designations recorded by `remove_entities`'s delta path (the
// first-touch before values) close symmetric with the set's current
// state as the after image.
let reference_designations: Vec<ReferenceDesignationDelta> = reference_designations_before
.into_iter()
.filter_map(|(handle, before)| {
let after = self.tabs[i].scene.reference_only.is_marked(handle);
(before != after).then_some(ReferenceDesignationDelta {
handle,
before,
after,
})
})
.collect();
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
@@ -844,6 +911,7 @@ impl OpenCADStudio {
active_layer: None,
structure,
parametric_constraints,
reference_designations,
named_parameters,
label: pending.label,
};
@@ -998,8 +1066,19 @@ impl OpenCADStudio {
}
}
}
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
let mut changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
let scene = &mut self.tabs[i].scene;
// Designations: restore the chosen side's value on the scene-owned set
// (the entity images above already applied, so a restored entity is
// present before its designation is). Each actual flip joins the
// returned per-handle change list so the deferred cache reseed and
// geometry bump re-stamp that entity's wires.
for entry in &d.reference_designations {
let value = if undo { entry.before } else { entry.after };
if scene.set_reference_only_membership(entry.handle, value) {
changes.push((entry.handle, crate::scene::ChangeKind::Modified));
}
}
if !d.parametric_constraints.is_empty() {
scene.bump_constraints_epoch();
}
+158 -75
View File
@@ -519,6 +519,28 @@ impl<'a> HostSession<'a> {
Ok(())
}
/// Erase through the plugin path while keeping reference-only designations
/// undoable with the erase (API v9). The designation set is scene state,
/// not a document write: erasing drops the set entry in
/// `Scene::remove_entities`, so its first-touch before-image must join the
/// open plugin scope here — otherwise undoing the erase would restore the
/// entity without its designation. No-op for undesignated handles and when
/// no scope is open (then the erase follows the same no-bookkeeping
/// contract as any other entity write).
fn erase_entities_recording_designations(&mut self, handles: &[Handle]) {
for &handle in handles {
if self.app.tabs[self.tab]
.scene
.reference_only
.is_marked(handle)
{
self.app
.record_reference_designation_before_pending(self.tab, handle, true);
}
}
self.app.tabs[self.tab].scene.erase_entities_plugin(handles);
}
/// Delete the entity with `handle`, keeping the scene's render caches in
/// sync. Returns `false` when the entity is absent or on a locked layer
/// (which `erase_entities` refuses to remove).
@@ -548,9 +570,7 @@ impl<'a> HostSession<'a> {
.document()
.get_entity(handle)
.map(|entity| entity.common().owner_handle);
self.app.tabs[self.tab]
.scene
.erase_entities_plugin(&[handle]);
self.erase_entities_recording_designations(&[handle]);
let removed = self.document().get_entity(handle).is_none();
if removed {
// The document keeps a block record's member list separately from the
@@ -1140,9 +1160,7 @@ impl<'a> HostSession<'a> {
return Err("the result could not be added".into());
}
if delete_source {
self.app.tabs[self.tab]
.scene
.erase_entities_plugin(&[source]);
self.erase_entities_recording_designations(&[source]);
self.publish_document_view();
}
Ok(handle)
@@ -1405,9 +1423,7 @@ impl<'a> HostSession<'a> {
self.push_undo("Delete layer");
self.document_mut().layers.remove(&name);
if !on_layer.is_empty() {
self.app.tabs[self.tab]
.scene
.erase_entities_plugin(&on_layer);
self.erase_entities_recording_designations(&on_layer);
}
self.finish_layer_change(&[name]);
Ok(handle)
@@ -2262,9 +2278,7 @@ impl<'a> HostSession<'a> {
}
}
if erase_originals {
self.app.tabs[self.tab]
.scene
.erase_entities_plugin(&entities);
self.erase_entities_recording_designations(&entities);
}
self.finish_block_change();
Ok(handle)
@@ -3134,62 +3148,64 @@ impl<'a> HostSession<'a> {
/// Mark (or unmark) an entity as reference-only (API v9, additive):
/// plugin-owned display geometry the built-in edit paths treat like a
/// locked-layer entity, while the owning plugin keeps full write access
/// through this session. The designation survives DWG/DXF save and reload.
/// through this session. The designation is a scene-scoped in-memory set
/// keyed by handle: nothing is written to the saved DWG/DXF, the set dies
/// with the tab, and a reloaded drawing starts undesignated — the owning
/// plugin re-marks its entities when the document opens, which makes the
/// in-memory designation fully equivalent to a persisted one for its use
/// case.
///
/// Every refusal — absent entity, locked layer — is decided before the
/// undo scope is touched, so a failure changes nothing. A call that would
/// not change the current designation succeeds without recording an undo
/// step. Undo composition: a change inside a plugin-opened `push_undo`
/// scope joins it (see [`HostSession::ensure_xrecord_undo_scope`]); a
/// scope-less call is one undo step of its own.
/// step, without bumping the geometry epoch and without publishing — the
/// idempotence that terminates the re-mark loop driven by the
/// document-changed dispatch. Undo composition: a change inside a
/// plugin-opened `push_undo` scope joins it (see
/// [`HostSession::ensure_xrecord_undo_scope`]); a scope-less call is one
/// undo step of its own. The set is scene state, not a document write, so
/// the undo delta restores it through the explicit designation channel
/// (`DeltaSnapshot::reference_designations`).
pub fn set_reference_only(
&mut self,
handle: Handle,
reference_only: bool,
) -> Result<(), String> {
// Refusals precede any change (and any undo step).
let Some(entity) = self.document().get_entity(handle) else {
if self.document().get_entity(handle).is_none() {
return Err(format!("entity {handle:?} does not exist"));
};
}
if self.app.tabs[self.tab].scene.is_layer_locked(handle) {
return Err(format!("entity {handle:?} is on a locked layer"));
}
if crate::scene::reference_only::entity_is_reference_only(entity) == reference_only {
if self.app.tabs[self.tab].scene.is_reference_only(handle) == reference_only {
return Ok(());
}
let own_scope = self.ensure_xrecord_undo_scope("Set Reference-Only");
// Register the host APPID before the marker is written so the record's
// application is resolvable on save.
if reference_only {
self.ensure_app_id(crate::scene::reference_only::REFERENCE_ONLY_APP_ID);
}
let app_handle = self
.document()
.app_ids
.get(crate::scene::reference_only::REFERENCE_ONLY_APP_ID)
.map(|app| app.handle.value());
let changed = self
.document_mut()
.get_entity_mut(handle)
.map(|entity| {
let changed =
crate::scene::reference_only::set_entity_reference_only(entity, reference_only);
// Drop stale verbatim EED for the host app so the fresh
// marker — not pre-edit bytes captured on a prior read —
// wins on the next save.
if changed {
if let Some(app_handle) = app_handle {
entity
.common_mut()
.extended_data
.raw_dwg_eed
.retain(|(a, _)| *a != app_handle);
}
}
changed
})
.unwrap_or(false);
debug_assert!(changed, "designation state was checked before the mutation");
// First-touch before-image for the pending scope's designation
// channel, recorded before the set changes (see
// `Scene::record_undo_reference_designation_before`).
let before = self.app.tabs[self.tab]
.scene
.reference_only
.is_marked(handle);
self.app
.record_reference_designation_before_pending(self.tab, handle, before);
self.app.tabs[self.tab]
.scene
.set_reference_only_membership(handle, reference_only);
debug_assert!(
self.app.tabs[self.tab]
.scene
.reference_only
.is_marked(handle)
== reference_only,
"designation state was checked before the mutation"
);
// The wires carry the click-through stamp baked at tessellation, so a
// designation change must rebuild them: bump_geometry clears the
// tessellation memos and resident wire sets exactly like an entity
// edit would.
self.bump_geometry();
self.set_dirty();
self.publish_document_view();
@@ -13491,9 +13507,7 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
.get_mut("0")
.map(|layer| layer.unlock());
assert!(
!crate::scene::reference_only::entity_is_reference_only(
host.document().get_entity(handle).unwrap()
),
!host.app.tabs[0].scene.is_reference_only(handle),
"refusals must not partially apply the designation"
);
assert_eq!(
@@ -13526,7 +13540,7 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
// Undo reverts the unmark, redo re-applies it.
drop(host);
app.undo_steps(1);
let mut host = HostSession::new(&mut app, 0);
let host = HostSession::new(&mut app, 0);
assert!(host.app.tabs[0].scene.is_reference_only(handle));
drop(host);
app.redo_steps(1);
@@ -13549,7 +13563,7 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
// Undoing the plugin operation reverts it whole.
app.undo_steps(1);
let mut host = HostSession::new(&mut app, 0);
let host = HostSession::new(&mut app, 0);
assert!(
host.app.tabs[0]
.scene
@@ -13565,32 +13579,101 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
}
#[test]
fn reference_only_survives_dxf_and_dwg_round_trips() {
fn reference_only_designation_is_not_written_to_dwg_or_dxf() {
let mut app = OpenCADStudio::new_for_test();
app.tabs[0].is_start = false;
let mut host = HostSession::new(&mut app, 0);
let handle = host.add_entity(reference_test_line(10.0, 0.0, Handle::NULL));
host.set_reference_only(handle, true).unwrap();
// The designation lives in the scene's in-memory set only.
assert!(host.app.tabs[0].scene.is_reference_only(handle));
// The host writes nothing of its own into the document: no APPID
// registration, no marker XDATA on the designated entity.
assert!(host.document().app_ids.get("OCS").is_none());
assert!(host
.document()
.get_entity(handle)
.expect("designated entity")
.common()
.extended_data
.get_record("OCS")
.is_none());
let contains_marker =
|bytes: &[u8]| bytes.windows(15).any(|window| window == b"REFERENCE_ONLY");
// DWG: the marker string appears nowhere in the saved bytes, and a
// reopened file carries neither the host APPID nor the marker record.
let dwg = codec::DwgWriter::write_to_vec(host.document()).unwrap();
assert!(
!contains_marker(&dwg),
"DWG save must not contain the designation marker"
);
let reloaded = crate::io::load_bytes("reference-only.dwg", dwg).unwrap();
assert!(reloaded.app_ids.get("OCS").is_none());
assert!(reloaded
.get_entity(handle)
.expect("the line survives the round trip")
.common()
.extended_data
.get_record("OCS")
.is_none());
assert!(
!contains_marker(&codec::DwgWriter::write_to_vec(&reloaded).unwrap()),
"a re-save of the reopened file stays marker-free"
);
// DXF: same contract for the text format.
let dxf = codec::DxfWriter::new(host.document())
.write_to_vec()
.unwrap();
assert!(
!contains_marker(&dxf),
"DXF save must not contain the designation marker"
);
let reloaded = crate::io::load_bytes("reference-only.dxf", dxf).unwrap();
assert!(reloaded.app_ids.get("OCS").is_none());
assert!(reloaded
.get_entity(handle)
.expect("the line survives the round trip")
.common()
.extended_data
.get_record("OCS")
.is_none());
}
#[test]
fn erasing_a_designated_entity_undoes_designation_with_it() {
let mut app = OpenCADStudio::new_for_test();
app.tabs[0].is_start = false;
let mut host = HostSession::new(&mut app, 0);
let handle = host.add_entity(reference_test_line(10.0, 0.0, Handle::NULL));
host.set_reference_only(handle, true).unwrap();
let flagged = |document: &codec::CadDocument| {
document
.get_entity(handle)
.is_some_and(crate::scene::reference_only::entity_is_reference_only)
};
// The host APPID the marker lives under travels with the record.
assert!(host.document().app_ids.get("OCS").is_some());
// The plugin erase path removes a designated entity inside a scope:
// entity and set entry leave together.
host.push_undo("CVI: replace the underlying model");
host.remove_entity(handle);
drop(host);
app.finish_pending_history(0);
let host = HostSession::new(&mut app, 0);
assert!(host.app.tabs[0].scene.document.get_entity(handle).is_none());
assert!(!host.app.tabs[0].scene.is_reference_only(handle));
drop(host);
let dwg = codec::DwgWriter::write_to_vec(host.document()).unwrap();
let reloaded = crate::io::load_bytes("reference-only.dwg", dwg).unwrap();
assert!(flagged(&reloaded), "DWG save/reopen keeps the designation");
assert!(reloaded.app_ids.get("OCS").is_some());
let dxf = codec::DxfWriter::new(host.document())
.write_to_vec()
.unwrap();
let reloaded = crate::io::load_bytes("reference-only.dxf", dxf).unwrap();
assert!(flagged(&reloaded), "DXF save/reopen keeps the designation");
assert!(reloaded.app_ids.get("OCS").is_some());
// Undoing the erase restores the entity AND its designation: the set
// mutation rode the scope as an explicit designation channel.
app.undo_steps(1);
let host = HostSession::new(&mut app, 0);
assert!(
host.app.tabs[0].scene.document.get_entity(handle).is_some(),
"undo restored the erased entity"
);
assert!(
host.app.tabs[0].scene.is_reference_only(handle),
"undo restored the designation together with its entity"
);
}
#[test]
+23 -17
View File
@@ -226,7 +226,7 @@ fn record(
value
}
fn entity_record(entity: &codec::EntityType) -> Result<Value, String> {
fn entity_record(entity: &codec::EntityType, reference_only: bool) -> Result<Value, String> {
let (kind, properties) = enum_parts(entity)?;
let mut value = record(
"entities",
@@ -245,7 +245,8 @@ fn entity_record(entity: &codec::EntityType) -> Result<Value, String> {
"record_type".into(),
json!(crate::entities::names::dxf_name(entity)),
);
let reference_only = crate::scene::reference_only::entity_is_reference_only(entity);
// Scene-scoped designation (API v9): the set membership of this entity's
// handle, resolved by the caller from the live scene.
object.insert("reference_only".into(), json!(reference_only));
Ok(value)
}
@@ -307,12 +308,13 @@ fn map_records<T: Serialize>(
.collect()
}
fn collection_records(
document: &codec::CadDocument,
collection: &str,
) -> Result<Vec<Value>, String> {
fn collection_records(scene: &crate::scene::Scene, collection: &str) -> Result<Vec<Value>, String> {
let document = &scene.document;
match collection {
"entities" => document.entities().map(entity_record).collect(),
"entities" => document
.entities()
.map(|entity| entity_record(entity, scene.is_reference_only(entity.common().handle)))
.collect(),
"objects" => {
let mut objects: Vec<_> = document.objects.iter().collect();
objects.sort_by_key(|(handle, _)| handle.value());
@@ -1387,7 +1389,7 @@ impl OpenCADStudio {
}
let definitions = referenced_definitions(&root_type);
let runtime_schema = collection.and_then(|collection| {
collection_records(&self.tabs[self.active_tab].scene.document, collection)
collection_records(&self.tabs[self.active_tab].scene, collection)
.ok()?
.into_iter()
.find(|record| {
@@ -1431,20 +1433,20 @@ impl OpenCADStudio {
}
pub(super) fn record_query(&self, request: &Value) -> Value {
let document = &self.tabs[self.active_tab].scene.document;
let scene = &self.tabs[self.active_tab].scene;
let Some(collection) = request["collection"].as_str() else {
return self.record_capabilities();
};
let mut records = Vec::new();
if collection == "all" {
for (name, _) in COLLECTIONS {
match collection_records(document, name) {
match collection_records(scene, name) {
Ok(mut values) => records.append(&mut values),
Err(error) => return failure("serialization_failed", error),
}
}
} else {
match collection_records(document, collection) {
match collection_records(scene, collection) {
Ok(values) => records = values,
Err(error) => return failure("unknown_collection", error),
}
@@ -1712,14 +1714,18 @@ mod tests {
Vector3::new(10.0, 0.0, 0.0),
),
));
fn mark(app: &mut OpenCADStudio, i: usize, handle: Handle, on: bool) {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::reference_only::set_entity_reference_only(entity, on);
}
// The designation is plugin-mutated scene state: mark through the
// same host API a plugin uses, so the read surface reflects the
// live set rather than any stored marker.
fn mark(app: &mut OpenCADStudio, handle: Handle, on: bool) {
let i = app.active_tab;
crate::app::plugin_host::HostSession::new(app, i)
.set_reference_only(handle, on)
.expect("mark through the plugin host API");
}
// The read surface reports the designation alongside the record.
mark(&mut app, i, handle, true);
mark(&mut app, handle, true);
let records = app.record_query(&json!({"collection":"entities"}));
assert_eq!(records["count"], 1);
assert_eq!(
@@ -1744,7 +1750,7 @@ mod tests {
assert_eq!(refused["code"], json!("reference_only"));
// Clearing the designation is reported too.
mark(&mut app, i, handle, false);
mark(&mut app, handle, false);
let records = app.record_query(&json!({"collection":"entities"}));
assert_eq!(records["records"][0]["reference_only"], json!(false));
}