WIP: port shell child transfers with literal face-use sibling links

Compare raw native mutation state, partial wire lists and defined self-transfer behavior. All 234 transfer/query cases pass before the subsequent safe-cast getter audit correction; dedicated metadata-override cases and remaining owned operations are still pending. Preserve the draft/todo status.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
This commit is contained in:
lillith
2026-10-15 04:53:53 +02:00
parent 4fe76c6293
commit c19f168fb7
12 changed files with 3126 additions and 21 deletions
@@ -748,6 +748,7 @@ pub(crate) fn import(ex: &[f64]) -> Result<Brep, HarnessError> {
mate,
orientation,
loopuses,
sibling_links: None,
});
}
let mut surfaces = Vec::new();
@@ -874,7 +875,7 @@ pub(crate) fn import(ex: &[f64]) -> Result<Brep, HarnessError> {
let tolerance = rd.num()?;
vertices.push(Vertex { point, tolerance, vertexuses: Vec::new() });
}
if rd.idx()? != 2 {
if rd.idx()? != 3 {
return Err(harness("export: unsupported shell extension version"));
}
let infinite_region = opt(rd.idx()?, RegionId::from_index)?;
@@ -899,6 +900,10 @@ pub(crate) fn import(ex: &[f64]) -> Result<Brep, HarnessError> {
.map(|v| opt(v, VertexuseId::from_index)?.ok_or_else(|| harness("null vertex use")))
.collect::<Result<Vec<_>, _>>()?;
}
for faceuse in &mut faceuses {
faceuse.sibling_links = Some((opt(rd.idx()?, FaceuseId::from_index)?,
opt(rd.idx()?, FaceuseId::from_index)?));
}
if rd.at != ex.len() {
return Err(harness("export: trailing data"));
}
+267 -2
View File
@@ -14,12 +14,15 @@ use solidcore_ffi::units::{topology_edgeuse_p1 as graph, topology_shell as ffi};
pub struct Runner;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Inputs {
brep: Value,
#[serde(default)]
void_state: Option<i32>,
#[serde(default)]
wires: bool,
#[serde(default)]
transfer: Option<TransferInputs>,
}
const TYPES: [i64; 5] = [16013, 16012, 16000, 16999, -1];
@@ -36,6 +39,237 @@ fn list(out: &mut Vec<i64>, values: impl IntoIterator<Item = u32>) {
out.extend(values.into_iter().map(i64::from));
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct TransferInputs {
#[serde(default)]
split_faceuse: bool,
#[serde(default)]
require_faceuse_pair: bool,
#[serde(default)]
extra_empty: usize,
source: usize,
recipient: usize,
candidates: Option<Vec<i64>>,
}
struct ResolvedTransfer {
split_faceuse: bool,
require_faceuse_pair: bool,
extra_empty: usize,
source: usize,
recipient: usize,
candidates: Option<Vec<Option<usize>>>,
}
fn resolve_transfer(
input: &TransferInputs,
counts: [usize; 10],
) -> Result<ResolvedTransfer, HarnessError> {
if input.extra_empty > 64 {
return Err(failure("too many empty shells"));
}
let shells = counts
.get(1)
.copied()
.and_then(|n| n.checked_add(input.extra_empty))
.ok_or_else(|| failure("shell count overflow"))?;
if shells == 0 {
return Err(failure("transfer fixture has no shells"));
}
let edges = counts
.get(7)
.copied()
.ok_or_else(|| failure("missing edge count"))?;
let candidates = if let Some(list) = &input.candidates {
let mut resolved = Vec::new();
for &entry in list {
resolved.push(if entry == -1 {
None
} else if entry < 0 || edges == 0 {
return Err(failure("invalid wire candidate selector"));
} else {
Some(usize::try_from(entry).map_err(failure)? % edges)
});
}
Some(resolved)
} else {
None
};
Ok(ResolvedTransfer {
split_faceuse: input.split_faceuse,
require_faceuse_pair: input.require_faceuse_pair,
extra_empty: input.extra_empty,
source: input.source % shells,
recipient: input.recipient % shells,
candidates,
})
}
fn rust_transfer_records(
brep: &mut solidcore::topology::Brep,
options: &ResolvedTransfer,
) -> Result<Output, HarnessError> {
use solidcore::topology::shell::{faceuse_sibling_links, transfer_children, ShellMembers};
use solidcore::topology::{EdgeId, EdgeuseOwner, VertexuseOwner};
use solidcore::SolidError;
let region = brep
.infinite_region()
.ok_or_else(|| failure("fixture has no infinite region"))?;
for _ in 0..options.extra_empty {
let shell = brep
.add_shell_record(Shell {
region: Some(region),
..Shell::new()
})
.map_err(failure)?;
brep.region_mut(region).map_err(failure)?.shells.push(shell);
}
if options.split_faceuse {
if options.extra_empty == 0 {
return Err(failure("split fixture needs an extra shell"));
}
let original = ShellId::from_index(0);
let extra_index = brep
.shells()
.len()
.checked_sub(options.extra_empty)
.ok_or_else(|| failure("extra shell index underflow"))?;
let extra = ShellId::from_index(u32::try_from(extra_index).map_err(failure)?);
let members = match &mut brep.shell_mut(original).map_err(failure)?.members {
ShellMembers::Faceuses(members) if members.len() >= 2 => members,
_ => return Err(failure("split fixture needs a face-use cycle")),
};
let moved = members.remove(0);
let target = brep.shell_mut(extra).map_err(failure)?;
target.shell_type = solidcore::topology::shell::ShellType::Faceuse;
target.members = ShellMembers::Faceuses(vec![moved]);
brep.faceuse_mut(moved).map_err(failure)?.shell = Some(extra);
// Construct the two baseline cycles independently of the transfer port.
for id in [original, extra] {
let members = match &brep.shell(id).map_err(failure)?.members {
ShellMembers::Faceuses(members) => members.clone(),
_ => return Err(failure("split fixture lost its cycle")),
};
let mut previous = members.last().copied();
for (&member, &next) in members.iter().zip(members.iter().cycle().skip(1)) {
brep.faceuse_mut(member).map_err(failure)?.sibling_links =
Some((Some(next), previous));
previous = Some(member);
}
}
}
let candidates = if let Some(list) = &options.candidates {
let mut ids = Vec::new();
for &entry in list {
ids.push(match entry {
None => None,
Some(index) => Some(EdgeId::from_index(u32::try_from(index).map_err(failure)?)),
});
}
Some(ids)
} else {
None
};
let source = ShellId::from_index(u32::try_from(options.source).map_err(failure)?);
let recipient = ShellId::from_index(u32::try_from(options.recipient).map_err(failure)?);
if options.require_faceuse_pair {
let source_record = brep.shell(source).map_err(failure)?;
let target_record = brep.shell(recipient).map_err(failure)?;
if source == recipient
|| source_record.shell_type.code() != 16022
|| target_record.shell_type.code() != 16022
|| !matches!(&source_record.members,ShellMembers::Faceuses(ids) if !ids.is_empty())
|| !matches!(&target_record.members,ShellMembers::Faceuses(ids) if !ids.is_empty())
{
return Err(failure(
"fixture must contain two distinct nonempty face-use shells",
));
}
}
let status = match transfer_children(brep, source, recipient, candidates.as_deref()) {
Ok(()) => 0,
Err(SolidError::Upstream { code, .. }) => i64::from(code),
Err(error) => return Output::unported(&error).ok_or_else(|| failure(error)),
};
let mut out = vec![
status,
brep.shells().len() as i64,
brep.faceuses().len() as i64,
brep.edgeuses().len() as i64,
brep.vertexuses().len() as i64,
brep.regions().len() as i64,
];
for (_, shell) in brep.shells().iter() {
let (size, kind, head) = match &shell.members {
ShellMembers::Empty => (0, 0, -1),
ShellMembers::Faceuses(ids) => (
ids.len(),
if ids.is_empty() { 0 } else { 16022 },
ids.first().map_or(-1, |id| i64::from(id.index())),
),
ShellMembers::Edgeuse(id) => {
let edge = brep
.edgeuse(*id)
.map_err(failure)?
.edge
.ok_or_else(|| failure("wire head has no edge"))?;
(
brep.edge(edge).map_err(failure)?.edgeuses.len(),
16005,
i64::from(id.index()),
)
}
ShellMembers::Vertexuse(id) => (1, 16006, i64::from(id.index())),
};
out.extend([
shell.shell_type.code(),
size as i64,
shell.region.map_or(-1, |id| i64::from(id.index())),
kind,
head,
]);
}
for (id, faceuse) in brep.faceuses().iter() {
let (next, last) = faceuse_sibling_links(brep, id).map_err(failure)?;
out.extend([
faceuse.shell.map_or(-1, |id| i64::from(id.index())),
next.map_or(-1, |id| i64::from(id.index())),
last.map_or(-1, |id| i64::from(id.index())),
]);
}
for (_, edgeuse) in brep.edgeuses().iter() {
let (kind, id) = match edgeuse.owner {
EdgeuseOwner::Shell(id) => (16013, id.map_or(-1, |id| i64::from(id.index()))),
EdgeuseOwner::Loopuse(id) => (16004, id.map_or(-1, |id| i64::from(id.index()))),
EdgeuseOwner::Unknown => (16999, -1),
};
out.extend([kind, id]);
}
for (_, vertexuse) in brep.vertexuses().iter() {
let (kind, id) = match vertexuse.owner {
VertexuseOwner::Shell(id) => (16013, id.map_or(-1, |id| i64::from(id.index()))),
VertexuseOwner::Loopuse(id) => (16004, id.map_or(-1, |id| i64::from(id.index()))),
VertexuseOwner::Edgeuse(id) => (16005, id.map_or(-1, |id| i64::from(id.index()))),
VertexuseOwner::Unknown => (16999, -1),
};
out.extend([kind, id]);
}
for (_, region) in brep.regions().iter() {
out.extend([
region.shells.len() as i64,
region.shells.first().map_or(-1, |id| i64::from(id.index())),
]);
list(&mut out, region.shells.iter().copied().map(ShellId::index));
}
for shell in brep.shells().ids() {
let wires = wire_edges(brep, shell).map_err(failure)?;
out.push(0);
list(&mut out, wires.into_iter().map(EdgeId::index));
}
Ok(Output::new().ints("records", out))
}
impl UnitDiff for Runner {
fn id(&self) -> &'static str {
"topology.shell"
@@ -44,7 +278,24 @@ impl UnitDiff for Runner {
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
let inputs: Inputs = case.parse()?;
let (path, block) = fixture_input(inputs.brep)?;
let native = if inputs.wires {
if inputs.transfer.is_some() && (inputs.wires || inputs.void_state.is_some()) {
return Err(failure("transfer cannot be combined with query mutations"));
}
let native = if let Some(transfer) = inputs.transfer {
let export = graph::eval(graph::Op::Export, &path, &block).map_err(failure)?;
let counts = graph::counts(&export)
.ok_or_else(|| failure("native transfer fixture build failed"))?;
let options = resolve_transfer(&transfer, counts)?;
ffi::transfer(
&path,
&block,
options.extra_empty,
options.source,
options.recipient,
options.candidates.as_deref(),
options.split_faceuse,
)
} else if inputs.wires {
ffi::wire_edges(&path, &block)
} else {
ffi::query(&path, &block, inputs.void_state)
@@ -67,6 +318,14 @@ impl UnitDiff for Runner {
let (path, block) = fixture_input(inputs.brep)?;
let export = graph::eval(graph::Op::Export, &path, &block).map_err(failure)?;
let mut brep = import(&export)?;
if inputs.transfer.is_some() && (inputs.wires || inputs.void_state.is_some()) {
return Err(failure("transfer cannot be combined with query mutations"));
}
if let Some(transfer) = inputs.transfer {
let counts = graph::counts(&export)
.ok_or_else(|| failure("native transfer fixture build failed"))?;
return rust_transfer_records(&mut brep, &resolve_transfer(&transfer, counts)?);
}
if inputs.wires {
let mut out = vec![brep.shells().len() as i64];
for shell in brep.shells().ids() {
@@ -156,9 +415,15 @@ impl UnitDiff for Runner {
let scale = [1e-3, 1.0, 1e3].get(rng.below(3) as usize).copied()?;
let origin = rng.point3(-2.0, 2.0).map(|v| v * scale);
let normal = [rng.uniform(-1.0, 1.0), rng.uniform(-1.0, 1.0), 1.0];
let wires = rng.chance(0.5);
let void_state = if wires {
None
} else {
Some(rng.below(3) as i32)
};
Some(
json!({"brep": {"kind":"face", "surface":{"plane":{"o":origin,"n":normal}},
"dom":[-scale,-scale,scale,scale]}, "void_state":rng.below(3) as i32, "wires":rng.chance(0.5)}),
"dom":[-scale,-scale,scale,scale]}, "void_state":void_state, "wires":wires}),
)
}
}
@@ -33,10 +33,11 @@
//! | `Gaps` | `eu which flag want_out` | per `which` (`0` `GetMaxEdgeFaceGap`, `1` `GetEdgeCCWEdgeGap`, `2` `CalcMaxEdgeFaceGap`, `3` `CalcEdgeCCWEdgeGap`) |
//! | `SetOrient` | `eu orientation` | `orient mate radial` |
//!
//! The graph export ends with a versioned extension: `2 infinite_region`, then
//! The graph export ends with a versioned extension: `3 infinite_region`, then
//! one raw void flag per region and one raw wire-edge-use/vertex-use head index
//! per shell (`-1` for other member classes), then a vertex-use index list per
//! vertex in native list order. The original graph prefix and the
//! vertex in native list order, then literal next/last indices per face use.
//! The original graph prefix and the
//! ten entity counts remain unchanged. This supplies shell state independently
//! of the shell operations being compared.
//!
@@ -46,7 +47,8 @@
//!
//! [`eval`] accepts only what upstream handles without undefined behaviour: existing
//! `.smb` files, well-formed clamped B-splines, planes with a non-zero normal,
//! parameters within 100 domain lengths of a surface's domain, indices inside the
//! parameters within 100 domain lengths of a surface's domain, plane-face domains
//! bounded by 1e6 for graph export (1e4 for other operations), indices inside the
//! B-rep's export (it builds the B-rep once more to count), finite inputs.
use std::path::Path;
@@ -325,7 +327,7 @@ fn uv(rd: &mut Reader<'_>, dom: Domain) -> Result<(), FfiError> {
}
/// A B-rep block.
fn brep_block(rd: &mut Reader<'_>, path: &str) -> Result<(), FfiError> {
fn brep_block(rd: &mut Reader<'_>, path: &str, plane_domain_limit: f64) -> Result<(), FfiError> {
match rd.index(4)? {
0 => {
let p = Path::new(path);
@@ -359,7 +361,7 @@ fn brep_block(rd: &mut Reader<'_>, path: &str) -> Result<(), FfiError> {
}
}
None => {
if d.iter().any(|x| x.abs() > 1e4) {
if d.iter().any(|x| x.abs() > plane_domain_limit) {
return Err(bad("plane face domain too large"));
}
}
@@ -550,7 +552,11 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
rd.done()?;
return raw(op, path, input);
}
brep_block(&mut rd, path)?;
// Export constructs the graph but does not run an edge-use geometry
// operation. Actual native shell probes cover plane domains through 1e6;
// retain the existing 1e4 bound for the other edge-use operations.
let plane_domain_limit = if op == Op::Export { 1e6 } else { 1e4 };
brep_block(&mut rd, path, plane_domain_limit)?;
let block = input.get(..rd.at).ok_or_else(|| bad("input too short"))?;
let export = raw(Op::Export, path, block)?;
if op == Op::Export {
@@ -70,8 +70,103 @@ pub fn wire_edges(path: &str, block: &[f64]) -> Result<Vec<f64>, FfiError> {
})
}
/// Native child transfer plus all affected raw topology state and wire queries.
/// Empty shells use the real native constructor in the B-rep's context. At most
/// 64 extra shells and 64 candidate entries are accepted by this fixture adapter.
///
/// # Errors
/// Malformed fixture data, out-of-range identities, failed native setup or a
/// nullable candidate on a branch that would dereference it in native code.
pub fn transfer(
path: &str,
block: &[f64],
extra_empty: usize,
source: usize,
recipient: usize,
candidates: Option<&[Option<usize>]>,
split_faceuse: bool,
) -> Result<Vec<f64>, FfiError> {
let export = fixture::eval(Op::Export, path, block)?;
let counts = fixture::counts(&export)
.ok_or_else(|| invalid("native topology fixture construction failed"))?;
if split_faceuse && extra_empty == 0 {
return Err(invalid("split fixture needs an extra shell"));
}
if extra_empty > 64 {
return Err(invalid("too many extra shells"));
}
let shell_count = counts
.get(1)
.copied()
.and_then(|n| n.checked_add(extra_empty))
.ok_or_else(|| invalid("shell count overflow"))?;
let edge_count = counts
.get(7)
.copied()
.ok_or_else(|| invalid("missing edge count"))?;
if source >= shell_count || recipient >= shell_count {
return Err(invalid("shell index out of range"));
}
let entries = candidates.unwrap_or(&[]);
if entries.len() > 64 {
return Err(invalid("too many wire candidates"));
}
let mut flat = Vec::new();
for &entry in entries {
flat.push(match entry {
None => -1,
Some(index) if index < edge_count => {
i64::try_from(index).map_err(|_| invalid("edge index overflow"))?
}
Some(_) => return Err(invalid("edge index out of range")),
});
}
let path = std::ffi::CString::new(path).map_err(|_| invalid("path contains NUL"))?;
let count = i64::try_from(block.len()).map_err(|_| invalid("fixture too long"))?;
let source = i64::try_from(source).map_err(|_| invalid("shell index overflow"))?;
let recipient = i64::try_from(recipient).map_err(|_| invalid("shell index overflow"))?;
call_with_buffer(4096, |buffer, len| {
// SAFETY: the existing fixture wrapper validated the entire block.
// All identities, lengths and flags are bounded above; the native shim
// guards nullable candidates before unsafe branches, keeps native objects
// inside its callback and honours the output capacity.
unsafe {
sys::sc_topology_shell_transfer(
path.as_ptr(),
block.as_ptr(),
count,
extra_empty as i32,
source,
recipient,
i32::from(candidates.is_some()),
flat.as_ptr(),
flat.len() as i64,
i32::from(split_faceuse),
buffer.as_mut_ptr(),
buffer.len() as i64,
&raw mut *len,
)
}
})
}
mod sys {
unsafe extern "C" {
pub fn sc_topology_shell_transfer(
path: *const std::ffi::c_char,
input: *const f64,
count: i64,
extra_empty: i32,
source: i64,
recipient: i64,
has_candidates: i32,
candidates: *const i64,
candidate_count: i64,
split_faceuse: i32,
output: *mut f64,
capacity: i64,
output_len: *mut i64,
) -> i32;
pub fn sc_topology_shell_wire_edges(
path: *const std::ffi::c_char,
input: *const f64,
+3
View File
@@ -332,6 +332,9 @@ pub struct Faceuse {
pub orientation: OrientType,
/// `m_pList`: the loop uses, outer loop use first.
pub loopuses: Vec<LoopuseId>,
/// Literal sibling links when present, even if the owner has no list head.
/// `None` uses the owner's canonical face-use vector during construction.
pub sibling_links: Option<shell::FaceuseSiblingLinks>,
}
/// What a loop use starts from (`m_tLoopuseType` and `m_pEUorVU`).
@@ -1589,6 +1589,7 @@ mod tests {
mate: Some(FaceuseId::from_index(1 - k)),
orientation: o,
loopuses: vec![LoopuseId::from_index(k)],
sibling_links: None,
})
.unwrap();
b.add_loopuse_record(Loopuse {
+196 -7
View File
@@ -3,7 +3,9 @@
//! Upstream: `source/SMLib/inc/SmShell.h`, `source/SMLib/src/SmShell.cpp`,
//! `source/SMLib/inc/SmRegion.h`, `source/SMLib/src/SmRegion.cpp`,
//! and the minimum default-argument branches of `source/SMLib/src/SmEdge.cpp`
//! and `source/SMLib/src/SmVertex.cpp` used by wire-edge traversal.
//! and `source/SMLib/src/SmVertex.cpp` used by wire-edge traversal;
//! `source/SMLib/inc/SmTopology.h` supplies literal sibling links and
//! `source/SMLib/inc/SmObject.h` defines the checked `SM_CAST_PTR` semantics.
//!
//! | Rust | Upstream |
//! |------|----------|
@@ -14,6 +16,8 @@
//! | [`Shell::is_closed`], [`Shell::mated_shells`] | `IsClosed`, `GetMatedShells` |
//! | [`shell_region`], [`shell_brep`] | `GetRegion`, `GetBrep` |
//! | [`wire_edges`] | `SmShell::GetWireEdges` |
//! | [`transfer_children`] | `SmShell::TransferChildrenTo` |
//! | [`faceuse_sibling_links`] | typed `SmTopology::GetNext` / `GetLast` for face uses |
//! | `edge_vertices_for_shell`, `edge_is_wire_for_shell` | `SmEdge::GetVertices` (no attribute filter), `IsWire` |
//! | `vertex_edges_for_shell` | `SmVertex::GetVertexuses`, `GetEdges` (no face filter) |
//! | [`Region::get_shells`], [`Region::is_void`], [`Region::set_is_void`] | `GetShells`, `IsVoid`, `SetIsVoid` |
@@ -24,6 +28,7 @@
//! shared Brep arena model. Temporary marks use a local set without changing
//! first-seen order. Invalid native pointer casts return `InvalidInput`. Native
//! notification/logging side effects are not represented by these arena records.
//! Face-use sibling links survive a cleared owner head, as in native self-transfer.
//! There is no recursion. This is WIP; remaining operations and native gates are pending.
use crate::error::{Result, SolidError};
@@ -161,15 +166,25 @@ impl Shell {
}
/// First face use; native logging-only errors retain the NULL result.
///
/// A head-class mismatch retains native NULL rather than becoming an error.
///
/// # Errors
/// A native list head of an incompatible class.
/// This compatibility result succeeds; native logging-only errors yield `None`.
pub fn first_faceuse(&self) -> Result<Option<FaceuseId>> {
Ok(self.get_faceuses()?.first().copied())
if !self.is_faceuse_shell() {
return Ok(None);
}
// SM_CAST_PTR checks the actual head class and returns NULL on mismatch.
// Unlike the reinterpret-cast GetFaceuses array, this is a defined path.
Ok(match &self.members {
ShellMembers::Faceuses(ids) => ids.first().copied(),
_ => None,
})
}
/// First wire edge, preserving NULL for empty or other-kind shells.
///
/// # Errors
/// Incompatible member class or dangling arena references.
/// Dangling arena references; a head-class mismatch retains native NULL.
pub fn wire_edge(&self, brep: &Brep) -> Result<Option<EdgeId>> {
if !self.is_wire_shell() {
return Ok(None);
@@ -177,13 +192,13 @@ impl Shell {
match self.members {
ShellMembers::Empty => Ok(None),
ShellMembers::Edgeuse(id) => Ok(brep.edgeuse(id)?.edge),
_ => Err(invalid_members()),
_ => Ok(None),
}
}
/// Vertex of a vertex shell, preserving NULL for empty or other-kind shells.
///
/// # Errors
/// Incompatible member class or dangling arena references.
/// Dangling arena references; a head-class mismatch retains native NULL.
pub fn vertex(&self, brep: &Brep) -> Result<Option<VertexId>> {
if !self.is_vertex_shell() {
return Ok(None);
@@ -191,7 +206,7 @@ impl Shell {
match self.members {
ShellMembers::Empty => Ok(None),
ShellMembers::Vertexuse(id) => Ok(brep.vertexuse(id)?.vertex),
_ => Err(invalid_members()),
_ => Ok(None),
}
}
/// Whether a face in this shell has a mate belonging to a different shell.
@@ -485,6 +500,180 @@ fn vertex_edges_for_shell(brep: &Brep, vertex: VertexId) -> Result<Vec<EdgeId>>
Ok(output)
}
/// Move a shell's vertex, wire edges and face uses to another shell.
///
/// `wire_candidates` is the native optional list of B-rep wire edges. When it
/// is present, ownership filters it in caller order; otherwise `GetWireEdges`
/// gathers the source's wires. It is never replaced by an empty default list.
/// Nullable list entries are only rejected if the native branch reads them.
///
/// # Errors
/// Native `SM_ERR` for an illegal vertex-shell move, dangling graph links, or
/// incompatible native list classes. The caller must supply connected topology.
pub fn transfer_children(
brep: &mut Brep,
source: ShellId,
recipient: ShellId,
wire_candidates: Option<&[Option<EdgeId>]>,
) -> Result<()> {
use crate::topology::edgeuse::p1;
use crate::topology::{EdgeuseOwner, VertexuseOwner};
const OP: &str = "topology::shell::transfer_children";
brep.shell(source)?;
if shell_list_size(brep, recipient)? == 0 {
brep.shell_mut(recipient)?.shell_type = ShellType::Unknown;
if brep.shell(source)?.is_vertex_shell() {
let ShellMembers::Vertexuse(vertexuse) = brep.shell(source)?.members else {
return Err(invalid_members());
};
brep.vertexuse(vertexuse)?;
let target = brep.shell_mut(recipient)?;
target.shell_type = ShellType::Vertex;
target.members = ShellMembers::Vertexuse(vertexuse);
brep.vertexuse_mut(vertexuse)?.owner = VertexuseOwner::Shell(Some(recipient));
clear_shell_members(brep, source)?;
return Ok(());
}
} else {
if brep.shell(recipient)?.is_vertex_shell() {
return if shell_list_size(brep, source)? == 0 {
Ok(())
} else {
Err(SolidError::sm_err(OP))
};
}
if brep.shell(source)?.is_vertex_shell() {
return Err(SolidError::sm_err(OP));
}
}
let wires = if let Some(candidates) = wire_candidates {
let mut wires = Vec::new();
for &edge in candidates {
let edge = edge.ok_or(invalid_members())?;
let primary = p1::primary_edgeuse(brep, edge)?.ok_or(invalid_members())?;
if brep.edgeuse(primary)?.owner == EdgeuseOwner::Shell(Some(source)) {
wires.push(edge);
}
}
wires
} else {
wire_edges(brep, source)?
};
if let Some(&first) = wires.first() {
if brep.shell(recipient)?.is_unknown_shell() {
let primary = p1::primary_edgeuse(brep, first)?.ok_or(invalid_members())?;
let target = brep.shell_mut(recipient)?;
target.members = ShellMembers::Edgeuse(primary);
target.shell_type = ShellType::Wire;
// UpdateListSize counts this edge's radial list; the shared arena
// model retains that list on Edge::edgeuses rather than separately.
}
for edge in wires {
// SmEdge::GetEdgeuses with neither optional filter. The shared edge
// record already retains its native circular radial list order.
let edgeuses = brep.edge(edge)?.edgeuses.clone();
for edgeuse in edgeuses {
brep.edgeuse_mut(edgeuse)?.owner = EdgeuseOwner::Shell(Some(recipient));
}
}
}
let faceuses = brep.shell(source)?.get_faceuses()?;
if !faceuses.is_empty() {
if !brep.shell(recipient)?.is_faceuse_shell() {
let target = brep.shell_mut(recipient)?;
target.shell_type = ShellType::Faceuse;
target.members = ShellMembers::Empty;
}
for faceuse in faceuses {
if brep.faceuse(faceuse)?.shell != Some(source) {
return Err(invalid_members());
}
// Remove, then PostInsert: the member moves to the recipient's tail
// without changing either list's remaining relative order.
match &mut brep.shell_mut(source)?.members {
ShellMembers::Faceuses(members) => {
let index = members
.iter()
.position(|id| *id == faceuse)
.ok_or(invalid_members())?;
members.remove(index);
}
_ => return Err(invalid_members()),
}
let target = &mut brep.shell_mut(recipient)?.members;
match target {
ShellMembers::Empty => *target = ShellMembers::Faceuses(vec![faceuse]),
ShellMembers::Faceuses(members) => members.push(faceuse),
_ => return Err(invalid_members()),
}
brep.faceuse_mut(faceuse)?.shell = Some(recipient);
}
// Self-transfer clears the shell head but leaves this face-use cycle
// connected. Preserve those native links independently of the head.
refresh_faceuse_sibling_links(brep, recipient)?;
}
clear_shell_members(brep, source)
}
fn clear_shell_members(brep: &mut Brep, id: ShellId) -> Result<()> {
let shell = brep.shell_mut(id)?;
shell.shell_type = ShellType::Unknown;
shell.members = ShellMembers::Empty;
Ok(())
}
fn shell_list_size(brep: &Brep, id: ShellId) -> Result<usize> {
match &brep.shell(id)?.members {
ShellMembers::Empty => Ok(0),
ShellMembers::Faceuses(members) => Ok(members.len()),
ShellMembers::Vertexuse(_) => Ok(1),
ShellMembers::Edgeuse(id) => {
let edge = brep.edgeuse(*id)?.edge.ok_or(invalid_members())?;
Ok(brep.edge(edge)?.edgeuses.len())
}
}
}
/// Native next/previous face-use links, independently of a shell's list head.
pub type FaceuseSiblingLinks = (Option<FaceuseId>, Option<FaceuseId>);
/// Typed adapter for a face use's native `GetNext` and `GetLast` links.
///
/// # Errors
/// A dangling id or a constructed owner vector that does not contain its member.
pub fn faceuse_sibling_links(brep: &Brep, id: FaceuseId) -> Result<FaceuseSiblingLinks> {
let faceuse = brep.faceuse(id)?;
if let Some(links) = faceuse.sibling_links {
return Ok(links);
}
let Some(owner) = faceuse.shell else {
return Ok((None, None));
};
let ShellMembers::Faceuses(members) = &brep.shell(owner)?.members else {
return Err(invalid_members());
};
let mut previous = members.last().copied();
for (&member, &next) in members.iter().zip(members.iter().cycle().skip(1)) {
if member == id {
return Ok((Some(next), previous));
}
previous = Some(member);
}
Err(invalid_members())
}
fn refresh_faceuse_sibling_links(brep: &mut Brep, owner: ShellId) -> Result<()> {
let members = brep.shell(owner)?.get_faceuses()?;
let mut previous = members.last().copied();
for (&member, &next) in members.iter().zip(members.iter().cycle().skip(1)) {
brep.faceuse_mut(member)?.sibling_links = Some((Some(next), previous));
previous = Some(member);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -795,7 +795,7 @@ void export_graph(SmBrep* brep, Graph& g, std::vector<double>& o)
}
// Versioned shell/region extension. Read raw state, never shell operations
// under test. Keep the existing graph prefix and its entity counts stable.
o.push_back(2.0);
o.push_back(3.0);
o.push_back((double)g.find(0, brep->GetInfiniteRegion()));
for (SmRegion* p : g.regions)
o.push_back((double)(p->*RegionPeek::is_void()));
@@ -811,6 +811,11 @@ void export_graph(SmBrep* brep, Graph& g, std::vector<double>& o)
for (SmTopology* use : list_of(p->GetList())) uses.push_back(g.find(8, use));
put_list(o, uses);
}
for (SmFaceuse* p : g.faceuses)
{
o.push_back((double)g.find(2, p->GetNext()));
o.push_back((double)g.find(2, p->GetLast()));
}
}
// Builds the B-rep of a B-rep block; nullptr (status in `st`) when construction fails.
+180
View File
@@ -98,3 +98,183 @@ SC_EXPORT int32_t sc_topology_shell_wire_edges(const char* path, const double* i
});
SC_CATCH
}
// Raw mutation snapshots: independent of shell operations being compared.
#include <SmContext.h>
#include <SmRegion.h>
#include <SmEdgeuse.h>
#include <SmVertexuse.h>
#include <memory>
#include <functional>
namespace transfer_detail {
struct NodePeek : SmTopology {
static SmOwningTopology* SmTopology::*owner() { return &NodePeek::m_pListOwner; }
static SmTopology* SmTopology::*next() { return &NodePeek::m_pNext; }
static SmTopology* SmTopology::*last() { return &NodePeek::m_pLast; }
};
struct OwnerPeek : SmOwningTopology {
static SmTopology* SmOwningTopology::*head() { return &OwnerPeek::m_pList; }
static ULONG SmOwningTopology::*size() { return &OwnerPeek::m_lListSize; }
};
struct ShellPeek : SmShell {
static SM_TYPE SmShell::*type() { return &ShellPeek::m_tShellType; }
};
struct EuPeek : SmEdgeuse {
static SmTopology* SmEdgeuse::*owner() { return &EuPeek::m_pSorLU; }
};
struct VuPeek : SmVertexuse {
static SmTopology* SmVertexuse::*owner() { return &VuPeek::m_pSorLUorEU; }
};
// Expose the real SmShell constructor, with its real context and region owner.
struct EmptyShell : SmShell { EmptyShell() : SmShell() {} };
struct Restore {
std::vector<std::function<void()>> actions;
Restore(const sc_topology_fixture::View& view, const std::vector<SmShell*>& shells) {
for (SmShell* p : shells) {
const auto type=p->*ShellPeek::type();
const auto head=p->*OwnerPeek::head();
const auto size=p->*OwnerPeek::size();
actions.push_back([p,type,head,size] {
p->*ShellPeek::type()=type;
p->*OwnerPeek::head()=head;
p->*OwnerPeek::size()=size;
});
}
for (SmFaceuse* p : view.faceuses) {
const auto owner=p->*NodePeek::owner();
const auto next=p->*NodePeek::next();
const auto last=p->*NodePeek::last();
actions.push_back([p,owner,next,last] {
p->*NodePeek::owner()=owner; p->*NodePeek::next()=next; p->*NodePeek::last()=last;
});
}
for (SmEdgeuse* p : view.edgeuses) {
const auto owner=p->*EuPeek::owner();
actions.push_back([p,owner] { p->*EuPeek::owner()=owner; });
}
for (SmVertexuse* p : view.vertexuses) {
const auto owner=p->*VuPeek::owner();
actions.push_back([p,owner] { p->*VuPeek::owner()=owner; });
}
}
~Restore() { for (const auto& action : actions) action(); }
};
void snapshot(std::vector<double>& out, const sc_topology_fixture::View& view,
const std::vector<SmShell*>& shells) {
out.insert(out.end(), {(double)shells.size(), (double)view.faceuses.size(),
(double)view.edgeuses.size(), (double)view.vertexuses.size(), (double)view.regions.size()});
for (SmShell* p : shells) {
SmTopology* head=p->*OwnerPeek::head();
const SM_TYPE kind=head ? head->GetType() : 0;
double id=-1;
if (kind==SmFaceuse_TYPE) id=identity(view.faceuses,(SmFaceuse*)head);
else if (kind==SmEdgeuse_TYPE) id=identity(view.edgeuses,(SmEdgeuse*)head);
else if (kind==SmVertexuse_TYPE) id=identity(view.vertexuses,(SmVertexuse*)head);
else if (head) throw 1;
out.insert(out.end(), {(double)(p->*ShellPeek::type()), (double)(p->*OwnerPeek::size()),
identity(view.regions,(SmRegion*)(p->*NodePeek::owner())), (double)kind, id});
}
for (SmFaceuse* p : view.faceuses) {
out.insert(out.end(), {identity(shells,(SmShell*)(p->*NodePeek::owner())),
identity(view.faceuses,(SmFaceuse*)(p->*NodePeek::next())),
identity(view.faceuses,(SmFaceuse*)(p->*NodePeek::last()))});
}
for (SmEdgeuse* p : view.edgeuses) {
const SM_TYPE kind=p->GetEdgeuseType();
SmTopology* owner=p->*EuPeek::owner();
const double id=kind==SmShell_TYPE ? identity(shells,(SmShell*)owner)
: kind==SmLoopuse_TYPE ? identity(view.loopuses,(SmLoopuse*)owner) : -1;
out.insert(out.end(), {(double)kind,id});
}
for (SmVertexuse* p : view.vertexuses) {
const SM_TYPE kind=p->GetVertexuseType();
SmTopology* owner=p->*VuPeek::owner();
const double id=kind==SmShell_TYPE ? identity(shells,(SmShell*)owner)
: kind==SmLoopuse_TYPE ? identity(view.loopuses,(SmLoopuse*)owner)
: kind==SmEdgeuse_TYPE ? identity(view.edgeuses,(SmEdgeuse*)owner) : -1;
out.insert(out.end(), {(double)kind,id});
}
for (SmRegion* p : view.regions) {
SmTopology* head=p->*OwnerPeek::head();
out.insert(out.end(), {(double)(p->*OwnerPeek::size()), identity(shells,(SmShell*)head)});
std::vector<double> members;
SmTopology* node=head;
while (node) {
if (members.size()>=100000 || node->GetType()!=SmShell_TYPE) throw 1;
members.push_back(identity(shells,(SmShell*)node));
node=node->*NodePeek::next();
if (node==head) break;
}
out.push_back((double)members.size());
out.insert(out.end(),members.begin(),members.end());
}
}
}
SC_EXPORT int32_t sc_topology_shell_transfer(const char* path, const double* input,
int64_t count, int32_t extra_empty, int64_t source, int64_t recipient,
int32_t has_candidates, const int64_t* candidates, int64_t candidate_count,
int32_t split_faceuse, double* output, int64_t cap, int64_t* output_len) {
if (!output_len || cap<0 || (cap && !output) || extra_empty<0 || extra_empty>64
|| source<0 || recipient<0 || (has_candidates!=0 && has_candidates!=1)
|| candidate_count<0 || candidate_count>64 || (candidate_count && !candidates)
|| (!has_candidates && candidate_count) || (split_faceuse!=0 && split_faceuse!=1)) return SC_ERR_ARG;
SC_TRY
return sc_topology_fixture::apply(path,input,count,
[&](const sc_topology_fixture::View& view)->int32_t {
std::vector<SmShell*> shells=view.shells;
SmRegion* region=view.brep.GetInfiniteRegion();
if (!region || !view.brep.GetContext()) return SC_ERR_ARG;
for (int32_t i=0;i<extra_empty;++i) {
std::unique_ptr<transfer_detail::EmptyShell> shell(
new (&view.brep) transfer_detail::EmptyShell());
shells.push_back(shell.get());
if (region->PostInsert(shell.get())!=SM_SUCCESS) return SC_ERR_ARG;
shell.release();
}
if (split_faceuse) {
if (view.shells.empty() || extra_empty<1) return SC_ERR_ARG;
SmShell* original=view.shells.front();
SmShell* extra=shells[view.shells.size()];
SmTopology* head=original->GetList();
if (!original->IsFaceuseShell() || original->GetSize()<2 || !head
|| head->GetType()!=SmFaceuse_TYPE) return SC_ERR_ARG;
// Fixture construction uses the already-existing base list
// operations, never TransferChildrenTo under test.
if (original->Remove(head)!=SM_SUCCESS) return SC_ERR_ARG;
extra->*transfer_detail::ShellPeek::type()=SmFaceuse_TYPE;
if (extra->PostInsert(head)!=SM_SUCCESS) return SC_ERR_ARG;
}
if (source>=(int64_t)shells.size() || recipient>=(int64_t)shells.size()) return SC_ERR_ARG;
SmShell* from=shells[(size_t)source];
SmShell* to=shells[(size_t)recipient];
const bool reads_candidates=!(to->GetSize()==0 && from->IsVertexShell())
&& !(to->GetSize()!=0 && (to->IsVertexShell() || from->IsVertexShell()));
SmTArray<SmEdge*> wires;
for (int64_t i=0;i<candidate_count;++i) {
const auto index=candidates[i];
if (index < -1 || index >= (int64_t)view.edges.size()) return SC_ERR_ARG;
SmEdge* edge=index==-1 ? nullptr : view.edges[(size_t)index];
if (reads_candidates && (!edge || !edge->GetPrimaryEdgeuse())) return SC_ERR_ARG;
wires.Add(edge);
}
// Snapshot the result before restoring canonical links for deletion.
// Self-transfer is compared, including its detached face-use cycle.
transfer_detail::Restore restore(view,shells);
const auto status=from->TransferChildrenTo(to,has_candidates ? &wires : nullptr);
std::vector<double> out{status==SM_SUCCESS ? 0.0 : (double)status};
transfer_detail::snapshot(out,view,shells);
// Exercise both connected-wire traversal and global ownership
// filtering on the actual post-transfer topology.
for (SmShell* shell : shells) {
SmTArray<SmEdge*> after;
const auto query_status=shell->GetWireEdges(after);
out.push_back(query_status==SM_SUCCESS ? 0.0 : (double)query_status);
list(out,after,view.edges);
}
return sc_copy_out(out.data(),(int64_t)out.size(),output,cap,output_len);
});
SC_CATCH
}
+43 -2
View File
@@ -3866,7 +3866,48 @@ unregistered pointer in its snapshot. Native GetWireEdges returns traversal IDs
`0, 2, 4, 1, 3`; the order is intentionally neither sorted nor forced by the
initial edge. No cases, tolerances or pointer checks were removed or weakened.
The complete Rust/C++ rerun is pending. The unit remains todo/draft while the
remaining owned operations and backedges are implemented. Issue #86 coordinates
The complete rerun passes all 144 shell comparisons and restores edge-use p1 to
2,449 passes with 273 genuine deferrals out of 2,722 cases, zero failures. Strict
C++ Clippy, formatting and the three existing kernel regressions pass.
Perturbation detects every compared case in both units. The unit remains
todo/draft while the remaining owned operations and backedges are implemented. Issue #86 coordinates
the actual region lookup context needed by attribute operations; it is separate
from this fixture correction and from the excluded SSI work.
## topology.shell WIP: child transfers and literal sibling links (PR #80)
A native probe of self-transfer on a real plane-face B-rep returns SM_SUCCESS,
clears the shell head/count/type to NULL/0/unknown, and leaves each face use's
owner and next/previous cycle intact. Faceuse now retains literal sibling links
independently of the owner's head; a canonical construction vector remains the
fallback before literal links are supplied. TransferChildrenTo preserves this
observed behavior rather than treating self-transfer as a no-op or excluding it.
The transfer adapter snapshots every affected shell head/count/type, face-use
owner/sibling link, edge-use/vertex-use owner and region list, then exercises
GetWireEdges on the post-transfer topology. A scope guard restores the native
links before B-rep destruction; this does not alter the observed result. Native
probes confirm partial/duplicate wire lists, nullable entries ignored by early
vertex branches, rejection before unsafe NULL dereferences, and real plane-face
domains at 1e-7/1/1e6. Graph export alone accepts plane domains through the native-
verified 1e6 bound; the existing edge-use-operation bound stays at 1e4. Comparison
tolerances are unchanged.
The first 234-case run exposed four fixture precondition errors: Endcap.smb has
one face-use shell with two members, whereas Mouse and box_with_large_fillet have
two nonempty face-use shells (4/4 and 7/7 members). The Endcap setup now uses native
base Remove/PostInsert operations to form two nonempty shells, independently of
TransferChildrenTo. All four cases and the nonempty-recipient coverage assertion
are retained. The corrected run passes all 234 shell cases, with zero failures,
zero deferrals and exact integer/topology agreement; perturbation detects all 234.
Edge-use p1 retains 2449 passes/273 deferrals out of 2722, zero failures, and all
2449 comparisons are detected under perturbation. Strict C++ Clippy, formatting
and the three kernel regressions pass.
Subsequent source audit found that SM_CAST_PTR explicitly returns NULL for a
head-class mismatch. GetVertex, GetWireEdge and GetFirstFaceuse now preserve that
defined NULL result rather than returning an incompatible-class error. Dedicated
metadata-override comparisons and a rerun after that correction remain pending.
The unit is still todo/draft: remaining owned operations, backedges, final gates
and the region-context coordination in #86 are not claimed complete.
+2 -2
View File
@@ -742,7 +742,7 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
status = "green"
claimed_by = "swarm-topology.topology-a1"
claimed_at = "2026-10-12T21:22:31Z"
notes = "GREEN: difftest 657 cases (57 upstream-derived/edge-case fixtures + 600 generated scripts: marks under SmNewMarkAndLock, flags/user values, Notify propagation for every operation incl. the callback pass-through, tolerance, IsConnectedTo, destruction, SmTrackTopologyChanges start/stop/nesting/foreign callback), 0 deferred, bit-identical; wired SmSAGObject::Notify (geom.a_object 169 -> 168 deferred) and retargeted the SmTol::GetZoneTol3d(SmObject*) markers to the derived-kind units; SmTopologyChangeCallback::Execute is unused upstream and not ported (findings.md); 30 fns; largest: SmTopology::Notify, SmTrackTopologyChanges::StartTracking, SmTrackTopologyChanges::StopTracking, SmTrackTopologyChanges::SmTrackTopologyChanges; cycle back-edges (call into later units; stub or co-port): topology.proto_topology_p1(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
notes = "GREEN: difftest 657 cases (57 upstream-derived/edge-case fixtures + 600 generated scripts: marks under SmNewMarkAndLock, flags/user values, Notify propagation for every operation incl. the callback pass-through, tolerance, IsConnectedTo, destruction, SmTrackTopologyChanges start/stop/nesting/foreign callback), 0 deferred, bit-identical; wired SmSAGObject::Notify (geom.a_object 169 -> 168 deferred) and retargeted the SmTol::GetZoneTol3d(SmObject*) markers to the derived-kind units; SmTopologyChangeCallback::Execute is unused upstream and not ported (findings.md); 30 fns; largest: SmTopology::Notify, SmTrackTopologyChanges::StartTracking, SmTrackTopologyChanges::StopTracking, SmTrackTopologyChanges::SmTrackTopologyChanges; cycle back-edges (call into later units; stub or co-port): topology.proto_topology_p1(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.shell (PR #80): SmTopology face-use sibling links -> topology::Faceuse::sibling_links and topology::shell::faceuse_sibling_links, retaining literal next/last links after self-transfer clears the shell head"
[[unit]]
id = "nlib.crv_adv_p1"
@@ -2534,7 +2534,7 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
status = "todo"
claimed_by = ""
claimed_at = ""
notes = "60 fns; largest: SmEdge::IsTopological, SmEdge::FindRadialSector, SmEdge::GetBlendEdgeuse, SmEdge::OrientEdgeSequence; cycle back-edges (call into later units; stub or co-port): heal.gap(12), topology.edgeuse_p3(3), query.edge(2), topology.brep_p2(2), blend.fillet_geom_p1(1), topology.brep_p1(1), topology.face_p1(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmEdge members m_pList (edge use list, primary first), m_vInterval, m_sZoneTol3d, m_pCurve -> topology::Edge::{edgeuses, interval, tolerance, curve}; SmEdge::GetPrimaryEdgeuse, IsLamina, GetBrep -> topology::edgeuse::p1::{primary_edgeuse, edge_is_lamina, edge_brep}; CO-PORTED in topology.shell (PR #80): SmEdge::GetVertices default no-attribute branch and IsWire -> topology::shell::{edge_vertices_for_shell,edge_is_wire_for_shell}, using primary edge-use orientation and actual mate/vertex-use links"
notes = "60 fns; largest: SmEdge::IsTopological, SmEdge::FindRadialSector, SmEdge::GetBlendEdgeuse, SmEdge::OrientEdgeSequence; cycle back-edges (call into later units; stub or co-port): heal.gap(12), topology.edgeuse_p3(3), query.edge(2), topology.brep_p2(2), blend.fillet_geom_p1(1), topology.brep_p1(1), topology.face_p1(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmEdge members m_pList (edge use list, primary first), m_vInterval, m_sZoneTol3d, m_pCurve -> topology::Edge::{edgeuses, interval, tolerance, curve}; SmEdge::GetPrimaryEdgeuse, IsLamina, GetBrep -> topology::edgeuse::p1::{primary_edgeuse, edge_is_lamina, edge_brep}; CO-PORTED in topology.shell (PR #80): SmEdge::GetVertices default no-attribute branch and IsWire -> topology::shell::{edge_vertices_for_shell,edge_is_wire_for_shell}, using primary edge-use orientation and actual mate/vertex-use links; SmEdge::GetEdgeuses default no-filter branch -> topology::shell::transfer_children via the native radial Edge::edgeuses list"
[[unit]]
id = "blend.fillet_custom_solver"