From 750f5f5a616c995b7beb8639f44230ea9f9702d9 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:08:33 -0400 Subject: [PATCH] model: geometry names up to 64 KiB everywhere, so long persistent face names mesh Persistent names grow with a design's split history; opening accepted them but mesh tags, sets and loads capped geometry names at 256 characters. One cap (64 KiB, against hostile input) now holds for geometry references, partitions and saved views. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/engine/src/cmd/mesh.rs | 29 +++++++++++++++++++++++++++++ crates/io/src/lib.rs | 4 ++-- crates/model/src/datums.rs | 7 +++---- crates/model/src/geometry.rs | 12 ++++++++---- 4 files changed, 42 insertions(+), 10 deletions(-) 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(()) }