forked from 1b-agent/opencadstudio-upstream
feat(scene): reference-only entities refuse the edit paths (API v9 host)
Host side of HostApi::set_reference_only: plugin-owned display geometry that renders and snaps normally but must not be edited or picked by the person at the screen, while the owning plugin keeps full write access through the host. - Storage: a human-readable marker value in an XDATA record under the host-owned OCS APPID (registered on first use; foreign values survive verbatim; stale verbatim DWG EED is dropped on rewrite), so the flag round-trips DWG and DXF with the entity and cannot collide with plugin APPID namespaces. - Grips: refresh_selected_grips yields none for a reference-only entity and an active grip edit on a newly designated target is cancelled like a locked-layer one. - Picking: a reference-only wire flag extends the snap_only skips in every hit test, so a click passes through to the editable entity underneath, and box, window, lasso, fence and select-all gathers skip the designation; fills, solids and instanced meshes are excluded through the shared candidate handle set. - Write policy: built-in user paths treat a reference-only entity like a locked-layer one (Scene::update_entity / update_entities refuse, erase_entities skips), while the plugin-owned variants (update_entity_plugin / erase_entities_plugin, mirroring the remove_entities editor split) and the whole HostSession surface write through. The MCP entities read exposes a top-level reference_only boolean and set_properties refuses with the reference_only failure code. - Undo: the entity-write contract - a change joins a plugin-opened push_undo scope, a scope-less change is one undo step of its own, refusals and idempotent re-marks record nothing. - Known limit: entities nested inside block instances (the block cache rebuilds instance wires without the flag). Evidence: marking_flags_and_unmarking_clears, foreign_values_under_the_host_app_id_survive, other_apps_are_untouched, reference_only_refusals_record_no_undo_step, reference_only_designation_composes_undo_like_entity_writes, reference_only_survives_dxf_and_dwg_round_trips, user_paths_refuse_and_plugin_paths_write_reference_only_entities, select_all_visible_skips_reference_only_entities, clicks_pass_through_reference_only_wires, box_selections_skip_reference_only_wires, entities_records_expose_the_reference_only_designation.
This commit is contained in:
1 parent
1f552038a8
commit
d24adc39b5
16 files changed
+806
-61
No files matched your search
@@ -0,0 +1,149 @@
|
||||
//! Reference-only designation (API v9).
|
||||
//!
|
||||
//! A reference-only entity is plugin-owned display geometry: it renders and
|
||||
//! snaps normally, but the built-in edit paths treat it like an entity on a
|
||||
//! locked layer — no grips, no click picking (a click passes through to the
|
||||
//! editable entity underneath), and user-driven writes refuse it. The owning
|
||||
//! plugin keeps full write access through the plugin host API, so it can
|
||||
//! refresh derived geometry in place.
|
||||
//!
|
||||
//! The designation persists as a marker value inside an XDATA record under a
|
||||
//! host-owned APPID, so it round-trips DWG and DXF with the entity and never
|
||||
//! collides with plugin APPID namespaces (plugins register their own
|
||||
//! application names through their manifest).
|
||||
|
||||
use codec::entities::EntityType;
|
||||
use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||||
|
||||
/// Host-owned APPID carrying the designation. The name belongs to the host
|
||||
/// alone; plugin records live under the plugin's own registered APPIDs.
|
||||
pub const REFERENCE_ONLY_APP_ID: &str = "OCS";
|
||||
|
||||
/// Marker value (DXF group code 1000) that flags an entity reference-only.
|
||||
/// Human-readable so an exported file explains itself.
|
||||
const REFERENCE_ONLY_MARKER: &str = "REFERENCE_ONLY";
|
||||
|
||||
/// True when `value` is the reference-only marker.
|
||||
fn is_marker(value: &XDataValue) -> bool {
|
||||
matches!(value, XDataValue::String(text) if text.as_str() == REFERENCE_ONLY_MARKER)
|
||||
}
|
||||
|
||||
/// True when `entity` carries the reference-only designation. Tolerant read:
|
||||
/// a record without the marker (foreign or damaged content) is not
|
||||
/// reference-only.
|
||||
pub fn entity_is_reference_only(entity: &EntityType) -> bool {
|
||||
let Some(record) = entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(REFERENCE_ONLY_APP_ID)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
record.values.iter().any(is_marker)
|
||||
}
|
||||
|
||||
/// Set (or clear) the designation on `entity`, returning whether the entity
|
||||
/// changed. Only the marker value is added or removed; foreign values under
|
||||
/// the host APPID are preserved verbatim, and a record left without values is
|
||||
/// dropped so a cleared flag leaves no empty record behind.
|
||||
pub fn set_entity_reference_only(entity: &mut EntityType, reference_only: bool) -> bool {
|
||||
if entity_is_reference_only(entity) == reference_only {
|
||||
return false;
|
||||
}
|
||||
let xd = &mut entity.common_mut().extended_data;
|
||||
if reference_only {
|
||||
let mut record = xd
|
||||
.get_record(REFERENCE_ONLY_APP_ID)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ExtendedDataRecord::new(REFERENCE_ONLY_APP_ID));
|
||||
record
|
||||
.values
|
||||
.push(XDataValue::String(REFERENCE_ONLY_MARKER.to_owned()));
|
||||
xd.upsert_record(record);
|
||||
} else {
|
||||
let Some(mut record) = xd.get_record(REFERENCE_ONLY_APP_ID).cloned() else {
|
||||
return false;
|
||||
};
|
||||
record.values.retain(|value| !is_marker(value));
|
||||
if record.values.is_empty() {
|
||||
xd.remove_record(REFERENCE_ONLY_APP_ID);
|
||||
} else {
|
||||
xd.upsert_record(record);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codec::entities::Line;
|
||||
|
||||
fn entity() -> EntityType {
|
||||
EntityType::Line(Line::new())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marking_flags_and_unmarking_clears() {
|
||||
let mut entity = entity();
|
||||
assert!(!entity_is_reference_only(&entity));
|
||||
assert!(set_entity_reference_only(&mut entity, true));
|
||||
assert!(entity_is_reference_only(&entity));
|
||||
// Idempotent: a second mark changes nothing and reports it.
|
||||
assert!(!set_entity_reference_only(&mut entity, true));
|
||||
assert!(set_entity_reference_only(&mut entity, false));
|
||||
assert!(!entity_is_reference_only(&entity));
|
||||
assert!(!set_entity_reference_only(&mut entity, false));
|
||||
// A cleared flag leaves no empty record behind.
|
||||
assert!(entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(REFERENCE_ONLY_APP_ID)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_values_under_the_host_app_id_survive() {
|
||||
let mut entity = entity();
|
||||
let mut record = ExtendedDataRecord::new(REFERENCE_ONLY_APP_ID);
|
||||
record.values.push(XDataValue::Integer16(7));
|
||||
entity.common_mut().extended_data.add_record(record);
|
||||
// A record without the marker is not reference-only.
|
||||
assert!(!entity_is_reference_only(&entity));
|
||||
assert!(set_entity_reference_only(&mut entity, true));
|
||||
let record = entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(REFERENCE_ONLY_APP_ID)
|
||||
.expect("record exists");
|
||||
assert_eq!(
|
||||
record.values,
|
||||
vec![
|
||||
XDataValue::Integer16(7),
|
||||
XDataValue::String(REFERENCE_ONLY_MARKER.to_owned()),
|
||||
]
|
||||
);
|
||||
assert!(set_entity_reference_only(&mut entity, false));
|
||||
let record = entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(REFERENCE_ONLY_APP_ID)
|
||||
.expect("record survives");
|
||||
assert_eq!(record.values, vec![XDataValue::Integer16(7)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_apps_are_untouched() {
|
||||
let mut entity = entity();
|
||||
assert!(set_entity_reference_only(&mut entity, true));
|
||||
let mut foreign = ExtendedDataRecord::new("CVIFC");
|
||||
foreign.values.push(XDataValue::String("road".to_owned()));
|
||||
entity.common_mut().extended_data.add_record(foreign);
|
||||
assert!(set_entity_reference_only(&mut entity, false));
|
||||
assert!(entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record("CVIFC")
|
||||
.is_some_and(|record| record.values.len() == 1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user