mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
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:
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user