diff --git a/crates/engine/src/cmd/mesh.rs b/crates/engine/src/cmd/mesh.rs index e5868c7..0005bb9 100644 --- a/crates/engine/src/cmd/mesh.rs +++ b/crates/engine/src/cmd/mesh.rs @@ -896,6 +896,35 @@ mod tests { assert!(e.starts_with("body `Box"), "{e}"); } + /// Persistent names grow with the design's split history: a tray floor split by joined ribs + /// names every split, past the old 256-character cap. Such a design meshes, and a set on that + /// face resolves by its full name, again after a remesh (#374). + #[test] + fn long_persistent_face_names_mesh_and_resolve() { + let mut commands = vec![ + json!({"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20, "corner": [0, 0, 0], "body_name": "Case"}}), + json!({"command": "solid.shell", "params": {"faces": [[20, 15, 20]], "thickness": 2, "direction": "inside", "body": "Case"}}), + ]; + // Ribs across the floor split it into strips, each named by every split before it. + for x in (3..=36).step_by(3) { + commands.push(json!({"command": "solid.box", "params": {"length": 1, "width": 26, "height": 5, "corner": [x, 2, 2], "operation": "join", "targets": ["Case"]}})); + } + let bytes = fieldcraft_geometry::script_design(&json!({"commands": commands}).to_string()).unwrap(); + let (_dir, path) = crate::tests::design_file(&bytes); + let mut s = Session::default(); + s.execute("geometry.open", &json!({"path": path})).unwrap(); + let bodies = s.execute("geometry.bodies", &json!({})).unwrap(); + let long = bodies[0]["faces"].as_array().unwrap().iter().filter_map(|f| f["name"].as_str()).max_by_key(|n| n.len()).unwrap().to_string(); + assert!(long.len() > 256, "the fixture needs a face name past the old cap: {long}"); + s.execute("mesh.tet.generate", &json!({"body": "Case"})).unwrap(); + s.execute("model.set.create", &json!({"name": "floor", "geometry": [{"body": "Case", "face": long}]})).unwrap(); + let first = s.model.resolve_set("floor").unwrap(); + assert!(!first.faces.is_empty()); + s.execute("mesh.tet.generate", &json!({"body": "Case", "tolerance": 0.5})).unwrap(); + let again = s.model.resolve_set("floor").unwrap(); + assert!(!again.faces.is_empty(), "the set follows the remesh by the full name"); + } + /// The quality report's one-line summary counts the inverted elements. #[test] fn quality_summary_counts_invalid_elements() { diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index 149ec8f..fdee8c7 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -179,8 +179,8 @@ pub struct Geometry { /// Most items a view hides. pub const MAX_HIDDEN: usize = 100_000; -/// Longest name of a hidden item. -const MAX_HIDDEN_NAME: usize = 1024; +/// Longest name of a hidden item (a body's name is a geometry name). +const MAX_HIDDEN_NAME: usize = fieldcraft_model::geometry::MAX_GEOMETRY_NAME_LEN; /// The kinds of items the viewport draws one by one, and so can hide. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] diff --git a/crates/model/src/datums.rs b/crates/model/src/datums.rs index e17a819..db185b6 100644 --- a/crates/model/src/datums.rs +++ b/crates/model/src/datums.rs @@ -11,14 +11,13 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; +use crate::geometry::MAX_GEOMETRY_NAME_LEN; use crate::{Error, Model, Result, check_name, context, insert_new, not_in_use, remove_existing, replace_existing}; /// Most partitions a model holds. pub const MAX_PARTITIONS: usize = 256; /// Most bodies one partition names. pub const MAX_PARTITION_BODIES: usize = 1000; -/// Longest body name a partition holds. -const MAX_BODY_NAME: usize = 256; /// A datum plane, axis or point. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] @@ -163,8 +162,8 @@ impl Model { return Err(invalid(format!("at most {MAX_PARTITION_BODIES} bodies"))); } for (i, b) in partition.bodies.iter().enumerate() { - if b.trim().is_empty() || b.len() > MAX_BODY_NAME { - return Err(invalid(format!("body names are 1 to {MAX_BODY_NAME} characters"))); + if b.trim().is_empty() || b.len() > MAX_GEOMETRY_NAME_LEN { + return Err(invalid(format!("body names are 1 to {MAX_GEOMETRY_NAME_LEN} bytes"))); } if partition.bodies.get(..i).is_some_and(|before| before.contains(b)) { return Err(invalid(format!("body `{b}` is listed twice"))); diff --git a/crates/model/src/geometry.rs b/crates/model/src/geometry.rs index 439140c..8f5ea29 100644 --- a/crates/model/src/geometry.rs +++ b/crates/model/src/geometry.rs @@ -16,9 +16,13 @@ use serde::{Deserialize, Serialize}; use crate::{Error, Model, Result}; -/// Longest body or entity name accepted (persistent names can be long; they are not written to -/// decks as such). -pub const MAX_GEOMETRY_NAME_LEN: usize = 256; +/// Longest body or entity name accepted, in bytes. Persistent names grow with the design's split +/// history (a face cut by a groove, then split by joined bosses, names every step), so a real +/// design can produce names of hundreds or thousands of characters; this cap only bounds hostile +/// input (control channel, model files). The same cap holds wherever a geometry name is taken: +/// opening, mesh tags, sets, ties, loads, partitions and saved views. Names are not written to +/// decks as such. +pub const MAX_GEOMETRY_NAME_LEN: usize = 64 * 1024; /// Most distinct geometry entities a mesh can be tagged with. pub const MAX_ENTITIES: usize = 1_000_000; @@ -114,7 +118,7 @@ impl From for GeometryRefRepr { fn check_geometry_name(what: &str, name: &str) -> Result<()> { if name.is_empty() || name.len() > MAX_GEOMETRY_NAME_LEN || name.chars().any(char::is_control) { let shown: String = name.chars().take(64).filter(|c| !c.is_control()).collect(); - return Err(Error::Invalid(format!("{what} name `{shown}` must be 1 to {MAX_GEOMETRY_NAME_LEN} characters with no control characters"))); + return Err(Error::Invalid(format!("{what} name `{shown}` must be 1 to {MAX_GEOMETRY_NAME_LEN} bytes with no control characters"))); } Ok(()) }