forked from 1b-agent/opencadstudio-upstream
fix(blocks): prevent circular block nesting and cut render cycles
Introduce would_create_cycle in block_service to prevent recursive block definitions with case-insensitive matching across block records. Enforce cycle refusal at INSERT commit in BEDIT and during block redefinition (redefine_block_raw) with a shared error message. Ensure render-time recursion truncates safely with a one-time session warning on cyclic documents.
This commit is contained in:
1 parent
59bb6faabd
commit
9c08e8a3ed
8 files changed
+435
-24
No files matched your search
@@ -982,6 +982,35 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
pub(super) fn handle_attreq_needed(&mut self, block_name: String) -> Option<Task<Message>> {
|
pub(super) fn handle_attreq_needed(&mut self, block_name: String) -> Option<Task<Message>> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
// When active space is a block editor session (BEDIT), refuse inserting
|
||||||
|
// any block that would create a circular nesting dependency.
|
||||||
|
if let Some(edit_handle) = self.tabs[i].scene.block_edit_block {
|
||||||
|
let container_name = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|br| br.handle == edit_handle)
|
||||||
|
.map(|br| br.name.clone());
|
||||||
|
if let Some(container) = container_name {
|
||||||
|
if crate::scene::block_service::would_create_cycle(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&container,
|
||||||
|
&block_name,
|
||||||
|
) {
|
||||||
|
self.command_line.push_error(
|
||||||
|
&crate::scene::block_service::circular_dependency_error(
|
||||||
|
&container,
|
||||||
|
&block_name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Collect the full AttributeDefinitions owned by this block
|
// Collect the full AttributeDefinitions owned by this block
|
||||||
// record so each created attribute keeps its geometry (#255).
|
// record so each created attribute keeps its geometry (#255).
|
||||||
let mut attdefs: Vec<codec::entities::AttributeDefinition> = {
|
let mut attdefs: Vec<codec::entities::AttributeDefinition> = {
|
||||||
|
|||||||
@@ -1519,3 +1519,47 @@ fn locked_attedit_attribute_flags_cases() {
|
|||||||
));
|
));
|
||||||
assert_eq!(preset_entity.get_value(), "edited_preset");
|
assert_eq!(preset_entity.get_value(), "edited_preset");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_bedit_insert_refuses_cycle() {
|
||||||
|
let mut app = app();
|
||||||
|
let name_a = "BEDIT_CYC_A";
|
||||||
|
let name_b = "BEDIT_CYC_B";
|
||||||
|
|
||||||
|
define_block(&mut app, name_a);
|
||||||
|
|
||||||
|
// B contains an insert of A
|
||||||
|
let ins_a = Insert::new(name_a.to_string(), Vector3::ZERO);
|
||||||
|
let i = tab(&app);
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.define_block_raw(name_b, Vector3::ZERO, vec![EntityType::Insert(ins_a)]);
|
||||||
|
|
||||||
|
// Now open BEDIT for block A
|
||||||
|
let insert_a = add_insert(&mut app, name_a);
|
||||||
|
begin_bedit(&mut app, insert_a);
|
||||||
|
assert!(bedit_open(&app));
|
||||||
|
|
||||||
|
// Try committing an INSERT of B into A inside BEDIT space:
|
||||||
|
// Inserting B into A would create A -> B -> A cycle
|
||||||
|
let _ = app.dispatch_command("-INSERT");
|
||||||
|
let mut feed = |text: &str| {
|
||||||
|
let _ = app.update(crate::app::Message::CommandInput(text.to_string()));
|
||||||
|
let _ = app.update(crate::app::Message::CommandSubmit);
|
||||||
|
};
|
||||||
|
feed(name_b);
|
||||||
|
feed("0,0");
|
||||||
|
feed("1");
|
||||||
|
feed("1");
|
||||||
|
feed("0");
|
||||||
|
|
||||||
|
// Verify refusal error on command line
|
||||||
|
let errs = errors(&app);
|
||||||
|
assert!(
|
||||||
|
errs.iter()
|
||||||
|
.any(|e| e.contains("circular dependency detected")),
|
||||||
|
"expected cycle error in errors: {errs:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||||
|
}
|
||||||
@@ -427,7 +427,11 @@ impl OpenCADStudio {
|
|||||||
for def in deps.blocks {
|
for def in deps.blocks {
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
if redefine && def.name.eq_ignore_ascii_case(name) {
|
if redefine && def.name.eq_ignore_ascii_case(name) {
|
||||||
scene.redefine_block_raw(&def.name, def.base_point, def.entities);
|
if let Err(err) = scene.redefine_block_raw(&def.name, def.base_point, def.entities)
|
||||||
|
{
|
||||||
|
self.command_line.push_error(&err);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
} else if scene.document.block_records.get(&def.name).is_none() {
|
} else if scene.document.block_records.get(&def.name).is_none() {
|
||||||
scene.define_block_raw(&def.name, def.base_point, def.entities);
|
scene.define_block_raw(&def.name, def.base_point, def.entities);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -580,11 +580,14 @@ impl OpenCADStudio {
|
|||||||
.define_block_raw(&def.name, def.base_point, def.entities);
|
.define_block_raw(&def.name, def.base_point, def.entities);
|
||||||
}
|
}
|
||||||
if redefine {
|
if redefine {
|
||||||
self.tabs[i].scene.redefine_block_raw(
|
if let Err(err) = self.tabs[i].scene.redefine_block_raw(
|
||||||
&name,
|
&name,
|
||||||
codec::types::Vector3::new(base.x, base.y, base.z),
|
codec::types::Vector3::new(base.x, base.y, base.z),
|
||||||
entities,
|
entities,
|
||||||
);
|
) {
|
||||||
|
self.command_line.push_error(&err);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|||||||
@@ -538,14 +538,16 @@ fn locked_redefine_with_a_changed_base_moves_the_instance() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// The redefine path: the same contents, but a new base point (0,20,0).
|
// The redefine path: the same contents, but a new base point (0,20,0).
|
||||||
scene.redefine_block_raw(
|
assert!(scene
|
||||||
NAME,
|
.redefine_block_raw(
|
||||||
v(0.0, 20.0, 0.0),
|
NAME,
|
||||||
vec![EntityType::Line(Line::from_points(
|
v(0.0, 20.0, 0.0),
|
||||||
v(0.0, 0.0, 0.0),
|
vec![EntityType::Line(Line::from_points(
|
||||||
v(20.0, 0.0, 0.0),
|
v(0.0, 0.0, 0.0),
|
||||||
))],
|
v(20.0, 0.0, 0.0),
|
||||||
);
|
))],
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
assert_close(
|
assert_close(
|
||||||
block_base_point(&scene.document, NAME),
|
block_base_point(&scene.document, NAME),
|
||||||
[0.0, 20.0, 0.0],
|
[0.0, 20.0, 0.0],
|
||||||
@@ -616,14 +618,16 @@ fn locked_noop_redefine_leaves_the_instance_where_it_is() {
|
|||||||
assert_close(end, [62.990381, 27.5, 0.0], 1e-6, "line end before");
|
assert_close(end, [62.990381, 27.5, 0.0], 1e-6, "line end before");
|
||||||
|
|
||||||
// No-op redefine: identical contents, same base.
|
// No-op redefine: identical contents, same base.
|
||||||
scene.redefine_block_raw(
|
assert!(scene
|
||||||
NAME,
|
.redefine_block_raw(
|
||||||
base,
|
NAME,
|
||||||
vec![EntityType::Line(Line::from_points(
|
base,
|
||||||
v(6.0, 4.0, 0.0),
|
vec![EntityType::Line(Line::from_points(
|
||||||
v(16.0, 4.0, 0.0),
|
v(6.0, 4.0, 0.0),
|
||||||
))],
|
v(16.0, 4.0, 0.0),
|
||||||
);
|
))],
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
assert_close(
|
assert_close(
|
||||||
block_base_point(&scene.document, NAME),
|
block_base_point(&scene.document, NAME),
|
||||||
[6.0, 4.0, 0.0],
|
[6.0, 4.0, 0.0],
|
||||||
@@ -1153,3 +1157,46 @@ fn locked_nested_insert_with_attributes_exploded_one_level() {
|
|||||||
"outer attdef insertion point",
|
"outer attdef insertion point",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_redefine_block_raw_refuses_cycle() {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let blk_a = "REDEF_A";
|
||||||
|
let blk_b = "REDEF_B";
|
||||||
|
|
||||||
|
scene.define_block_raw(
|
||||||
|
blk_a,
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![EntityType::Line(Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
|
||||||
|
// B contains A
|
||||||
|
scene.define_block_raw(
|
||||||
|
blk_b,
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![EntityType::Insert(Insert::new(
|
||||||
|
blk_a.to_string(),
|
||||||
|
Vector3::ZERO,
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Redefining A to contain B would create cycle A -> B -> A; must refuse
|
||||||
|
let res = scene.redefine_block_raw(
|
||||||
|
blk_a,
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![EntityType::Insert(Insert::new(
|
||||||
|
blk_b.to_string(),
|
||||||
|
Vector3::ZERO,
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(res.is_err());
|
||||||
|
let err = res.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.contains("circular dependency detected"),
|
||||||
|
"expected cycle error, got: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -598,7 +598,88 @@ fn record_of<'a>(
|
|||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| missing(name))
|
.ok_or_else(|| missing(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether inserting `inserted_block` into `container_block` would create a circular
|
||||||
|
/// block nesting dependency, directly or transitively.
|
||||||
|
///
|
||||||
|
/// Matching is case-insensitive, consistent with the rest of the block service.
|
||||||
|
///
|
||||||
|
/// Xrefs (`|` in name or xref flags) and anonymous blocks (`*` prefix) are handled as follows:
|
||||||
|
/// - Anonymous blocks (`*U...`) are fully traversed like normal blocks because their geometry
|
||||||
|
/// and nested inserts live in the document.
|
||||||
|
/// - Externally referenced blocks (xrefs) have definitions that come from external files and
|
||||||
|
/// are not edited inline in the host document, but if an xref name is tested, it is matched
|
||||||
|
/// case-insensitively and its local block record entities (if any) are traversed.
|
||||||
|
pub fn would_create_cycle(
|
||||||
|
document: &CadDocument,
|
||||||
|
container_block: &str,
|
||||||
|
inserted_block: &str,
|
||||||
|
) -> bool {
|
||||||
|
let container = container_block.trim();
|
||||||
|
let inserted = inserted_block.trim();
|
||||||
|
if container.is_empty() || inserted.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if container.eq_ignore_ascii_case(inserted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visited: HashSet<String> = HashSet::default();
|
||||||
|
let mut stack: Vec<String> = vec![inserted.to_ascii_uppercase()];
|
||||||
|
|
||||||
|
while let Some(current_upper) = stack.pop() {
|
||||||
|
if !visited.insert(current_upper.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find block record matching current_upper case-insensitively
|
||||||
|
let Some(record) = document.block_records.get(¤t_upper) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check entities owned by this block record: either registered in entity_handles
|
||||||
|
// or having owner_handle == record.handle in the document.
|
||||||
|
let mut sub_inserts = Vec::new();
|
||||||
|
for &handle in &record.entity_handles {
|
||||||
|
if let Some(EntityType::Insert(sub_ins)) = document.get_entity(handle) {
|
||||||
|
sub_inserts.push(sub_ins.block_name.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for entity in document.entities() {
|
||||||
|
if entity.common().owner_handle == record.handle {
|
||||||
|
if let EntityType::Insert(sub_ins) = entity {
|
||||||
|
sub_inserts.push(sub_ins.block_name.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for sub_name in sub_inserts {
|
||||||
|
if sub_name.eq_ignore_ascii_case(container) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let sub_upper = sub_name.to_ascii_uppercase();
|
||||||
|
if !visited.contains(&sub_upper) {
|
||||||
|
stack.push(sub_upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CIRCULAR_BLOCK_DEPENDENCY_ERROR: &str =
|
||||||
|
"Cannot insert block: circular dependency detected.";
|
||||||
|
|
||||||
|
/// Standard command-line error text when block insertion would create a circular dependency.
|
||||||
|
pub fn circular_dependency_error(container_block: &str, inserted_block: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"Cannot insert block \"{}\" into block \"{}\": circular dependency detected.",
|
||||||
|
inserted_block, container_block
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use codec::entities::{
|
use codec::entities::{
|
||||||
@@ -1097,4 +1178,64 @@ mod tests {
|
|||||||
"block \"A\" is open in the block editor; close the editor first"
|
"block \"A\" is open in the block editor; close the editor first"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_would_create_cycle_self_insert() {
|
||||||
|
let mut scene = scene();
|
||||||
|
define(&mut scene, "BLK_A");
|
||||||
|
assert!(would_create_cycle(&scene.document, "BLK_A", "BLK_A"));
|
||||||
|
assert!(would_create_cycle(&scene.document, "blk_a", "BLK_A"));
|
||||||
|
assert!(would_create_cycle(&scene.document, "BLK_A", "blk_a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_would_create_cycle_direct_pair() {
|
||||||
|
let mut scene = scene();
|
||||||
|
let blk_a = define(&mut scene, "BLK_A");
|
||||||
|
define(&mut scene, "BLK_B");
|
||||||
|
// A contains B
|
||||||
|
add_insert(&mut scene, "BLK_B", Some(blk_a));
|
||||||
|
// Inserting A into B would create a cycle
|
||||||
|
assert!(would_create_cycle(&scene.document, "BLK_B", "BLK_A"));
|
||||||
|
// Inserting B into A is not allowed either since A already inserts B
|
||||||
|
assert!(!would_create_cycle(&scene.document, "BLK_A", "BLK_B"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_would_create_cycle_three_level_chain() {
|
||||||
|
let mut scene = scene();
|
||||||
|
let blk_a = define(&mut scene, "BLK_A");
|
||||||
|
let blk_b = define(&mut scene, "BLK_B");
|
||||||
|
define(&mut scene, "BLK_C");
|
||||||
|
// A contains B, B contains C
|
||||||
|
add_insert(&mut scene, "BLK_B", Some(blk_a));
|
||||||
|
add_insert(&mut scene, "BLK_C", Some(blk_b));
|
||||||
|
// Inserting A into C creates cycle C -> A -> B -> C
|
||||||
|
assert!(would_create_cycle(&scene.document, "BLK_C", "BLK_A"));
|
||||||
|
// Inserting B into C creates cycle C -> B -> C
|
||||||
|
assert!(would_create_cycle(&scene.document, "BLK_C", "BLK_B"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_would_create_cycle_shared_child_not_a_cycle() {
|
||||||
|
let mut scene = scene();
|
||||||
|
let blk_a = define(&mut scene, "BLK_A");
|
||||||
|
let blk_b = define(&mut scene, "BLK_B");
|
||||||
|
define(&mut scene, "BLK_SHARED");
|
||||||
|
// A contains SHARED, B contains SHARED. Inserting B into A is fine (diamond/DAG, not cycle).
|
||||||
|
add_insert(&mut scene, "BLK_SHARED", Some(blk_a));
|
||||||
|
add_insert(&mut scene, "BLK_SHARED", Some(blk_b));
|
||||||
|
assert!(!would_create_cycle(&scene.document, "BLK_A", "BLK_B"));
|
||||||
|
assert!(!would_create_cycle(&scene.document, "BLK_B", "BLK_A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_would_create_cycle_case_differences() {
|
||||||
|
let mut scene = scene();
|
||||||
|
let blk_a = define(&mut scene, "BlockA");
|
||||||
|
let _blk_b = define(&mut scene, "BlockB");
|
||||||
|
add_insert(&mut scene, "BlockB", Some(blk_a));
|
||||||
|
assert!(would_create_cycle(&scene.document, "blockb", "BLOCKA"));
|
||||||
|
assert!(!would_create_cycle(&scene.document, "blocka", "BLOCKB"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Vendored
+121
-2
@@ -178,6 +178,7 @@ pub struct BlockCache {
|
|||||||
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
|
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
|
||||||
>,
|
>,
|
||||||
clip_prototypes: std::sync::Mutex<HashMap<(u64, Vec<u64>), u64>>,
|
clip_prototypes: std::sync::Mutex<HashMap<(u64, Vec<u64>), u64>>,
|
||||||
|
cycle_warnings: std::sync::Mutex<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
@@ -219,6 +220,22 @@ impl BlockCache {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_cycle_warning(&self, block_name: &str) -> bool {
|
||||||
|
let mut warnings = self
|
||||||
|
.cycle_warnings
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
warnings.insert(block_name.to_ascii_uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drain_cycle_warnings(&self) -> Vec<String> {
|
||||||
|
let mut warnings = self
|
||||||
|
.cycle_warnings
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
warnings.drain().collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn defn(&self, block_name: &str) -> Option<&Arc<BlockDefn>> {
|
pub fn defn(&self, block_name: &str) -> Option<&Arc<BlockDefn>> {
|
||||||
self.defns.get(block_name).or_else(|| {
|
self.defns.get(block_name).or_else(|| {
|
||||||
self.defns
|
self.defns
|
||||||
@@ -1105,7 +1122,7 @@ pub fn expand_insert(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let mut batches = Batches::default();
|
let mut batches = Batches::default();
|
||||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
let mut visited: Vec<String> = vec![ins.block_name.clone()];
|
||||||
|
|
||||||
let offsets = crate::scene::render_graph::array_offsets(ins);
|
let offsets = crate::scene::render_graph::array_offsets(ins);
|
||||||
let cell_xform = |offset: &[f64; 3]| {
|
let cell_xform = |offset: &[f64; 3]| {
|
||||||
@@ -1952,7 +1969,16 @@ fn expand_defn(
|
|||||||
emit_wire(attachment, &attachment_transform, &inner_ctx, out, d_range);
|
emit_wire(attachment, &attachment_transform, &inner_ctx, out, d_range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if visited.iter().any(|n| n == &nref.block_name) {
|
if visited
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.eq_ignore_ascii_case(&nref.block_name))
|
||||||
|
{
|
||||||
|
if ctx.cache.record_cycle_warning(&nref.block_name) {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: circular block reference detected for block \"{}\"; cutting cycle",
|
||||||
|
nref.block_name
|
||||||
|
);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
||||||
@@ -3484,4 +3510,97 @@ mod metrics_depth_tests {
|
|||||||
assert_ne!(root.metrics.aabb_local, [0.0; 4]);
|
assert_ne!(root.metrics.aabb_local, [0.0; 4]);
|
||||||
assert_eq!(root.metrics.aabb_local, tail.metrics.aabb_local);
|
assert_eq!(root.metrics.aabb_local, tail.metrics.aabb_local);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_cyclic_block_document_renders_without_hanging_and_warns_once() {
|
||||||
|
// Document built in code with pre-existing cycle:
|
||||||
|
// A contains B, B contains A
|
||||||
|
let mut document = CadDocument::new();
|
||||||
|
let blk_a = add_block(&mut document, "CYC_A");
|
||||||
|
let blk_b = add_block(&mut document, "CYC_B");
|
||||||
|
|
||||||
|
add_owned(
|
||||||
|
&mut document,
|
||||||
|
blk_a,
|
||||||
|
EntityType::Insert(Insert::new("CYC_B", Vector3::ZERO)),
|
||||||
|
);
|
||||||
|
add_owned(
|
||||||
|
&mut document,
|
||||||
|
blk_b,
|
||||||
|
EntityType::Insert(Insert::new("CYC_A", Vector3::ZERO)),
|
||||||
|
);
|
||||||
|
// Add a line in A so there is some actual geometry
|
||||||
|
add_owned(
|
||||||
|
&mut document,
|
||||||
|
blk_a,
|
||||||
|
EntityType::Line(Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Model space references CYC_A
|
||||||
|
document
|
||||||
|
.add_entity(EntityType::Insert(Insert::new("CYC_A", Vector3::ZERO)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Building the cache computes defns and metrics without hanging
|
||||||
|
let cache = BlockCache::build(
|
||||||
|
&document,
|
||||||
|
1.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
None,
|
||||||
|
&HashMap::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify defns exist
|
||||||
|
assert!(cache.defns.contains_key("CYC_A"));
|
||||||
|
assert!(cache.defns.contains_key("CYC_B"));
|
||||||
|
|
||||||
|
// Expanding / tessellating into wires does not hang and cuts the cycle
|
||||||
|
let ins = Insert::new("CYC_A", Vector3::ZERO);
|
||||||
|
let inherit_style = crate::scene::view::render::InheritStyle {
|
||||||
|
color: [1.0, 1.0, 1.0, 1.0],
|
||||||
|
pat_len: 1.0,
|
||||||
|
pat: [0.0; 8],
|
||||||
|
lw_px: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = expand_insert(
|
||||||
|
&document,
|
||||||
|
&cache,
|
||||||
|
&ins,
|
||||||
|
Handle::new(1),
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
7,
|
||||||
|
1.0,
|
||||||
|
[0.0; 8],
|
||||||
|
1.0,
|
||||||
|
inherit_style,
|
||||||
|
7,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
1.0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
1.0,
|
||||||
|
crate::scene::BlockScalePolicy::FromInsert,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
// Warning was recorded once
|
||||||
|
let warnings = cache.drain_cycle_warnings();
|
||||||
|
assert_eq!(warnings.len(), 1);
|
||||||
|
assert_eq!(warnings[0], "CYC_A");
|
||||||
|
|
||||||
|
// Subsequent expansion does not warn again because warned set retains it
|
||||||
|
let warnings_after = cache.drain_cycle_warnings();
|
||||||
|
assert!(warnings_after.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+27
-3
@@ -1263,19 +1263,35 @@ impl Scene {
|
|||||||
|
|
||||||
/// Replace the contents and base point of block `name`, defining it when
|
/// Replace the contents and base point of block `name`, defining it when
|
||||||
/// absent. References keep pointing at it and show the new contents.
|
/// absent. References keep pointing at it and show the new contents.
|
||||||
|
/// Refuses and returns Err if the new contents lead back to `name` directly or transitively.
|
||||||
pub fn redefine_block_raw(
|
pub fn redefine_block_raw(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
base_point: codec::types::Vector3,
|
base_point: codec::types::Vector3,
|
||||||
entities: Vec<EntityType>,
|
entities: Vec<EntityType>,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
|
for entity in &entities {
|
||||||
|
if let EntityType::Insert(ins) = entity {
|
||||||
|
if crate::scene::block_service::would_create_cycle(
|
||||||
|
&self.document,
|
||||||
|
name,
|
||||||
|
&ins.block_name,
|
||||||
|
) {
|
||||||
|
return Err(crate::scene::block_service::circular_dependency_error(
|
||||||
|
name,
|
||||||
|
&ins.block_name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some((br_handle, block_handle)) = self
|
let Some((br_handle, block_handle)) = self
|
||||||
.document
|
.document
|
||||||
.block_records
|
.block_records
|
||||||
.get(name)
|
.get(name)
|
||||||
.map(|br| (br.handle, br.block_entity_handle))
|
.map(|br| (br.handle, br.block_entity_handle))
|
||||||
else {
|
else {
|
||||||
return self.define_block_raw(name, base_point, entities);
|
self.define_block_raw(name, base_point, entities);
|
||||||
|
return Ok(());
|
||||||
};
|
};
|
||||||
let owned: Vec<Handle> = self
|
let owned: Vec<Handle> = self
|
||||||
.document
|
.document
|
||||||
@@ -1299,6 +1315,7 @@ impl Scene {
|
|||||||
let _ = self.document.add_entity(entity);
|
let _ = self.document.add_entity(entity);
|
||||||
}
|
}
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn synced_hatch_models(
|
pub(super) fn synced_hatch_models(
|
||||||
@@ -2228,7 +2245,14 @@ impl Scene {
|
|||||||
.colors
|
.colors
|
||||||
.get(i)
|
.get(i)
|
||||||
.and_then(|e| e.color.rgb())
|
.and_then(|e| e.color.rgb())
|
||||||
.map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, color[3]])
|
.map(|(r, g, b)| {
|
||||||
|
[
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
color[3],
|
||||||
|
]
|
||||||
|
})
|
||||||
};
|
};
|
||||||
gradient_color1 = stop(0);
|
gradient_color1 = stop(0);
|
||||||
let color1 = gradient_color1.unwrap_or(color);
|
let color1 = gradient_color1.unwrap_or(color);
|
||||||
|
|||||||
Reference in new issue
Block a user