Files
solvecraft/harness/cpp/shim/blend_feature_executive.cpp
T
Brett Lawson 4db1ba7b2f blend.feature_executive: carry attributes through PartitionFeature and RestoreFeature copies
Upstream's CopyFaces notifies SM_NO_COPY on each copied face, edge and vertex
(SmMerge::MakeFaceuse), so attributes reach the copies as their behaviours say.
The port's copies ran with an empty attribute store.

- topology::brep::p2::CopyNotifyState: notification state for a copy between
  two B-reps, holding both B-reps' own attributes (source objects under tagged
  copy_source_ref identities) and giving each B-rep its own back.
- topology::brep::p7::copy_faces_from: copy_faces with distinct source
  identities (copy_faces delegates to it unchanged).
- SmFeatureExecutive: PartitionFeature and RestoreFeature copy with the
  attributes. The patch path does not yet: its merges still differ from SMLib
  (finding addendum).
- Shim, FFI and runner: optional SmLongAttribute inputs, plus per-id face and
  edge counts of every B-rep in the record; 4 attribute cases (3 pass; the
  extrusion case fails until the patch path carries).
- Unit test: copy and reference attributes reach the feature B-rep, temporary
  ones do not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXj2sJf943y2dDF36Vy
2026-10-31 04:34:25 +01:00

466 lines
20 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// blend.feature_executive: SmFeatureExecutive (source/SMLib/src/SmFeatureExecutive.cpp) on real
// SmBrep fixtures. The executive is the real SMLib class driven by a small script (features
// added by faces, by a face and loop edges, or by a face and caller-made SmFeatureLoops, then
// DoDefeature / setters / RestoreFeatures); the B-rep construction and the raw graph export
// are the shared ones of `sc_topology_fixture.h` (read only from SMLib). Layout in
// crates/solidcore-ffi/src/units/blend_feature_executive.rs.
#include "sc_topology_fixture.h"
#include <SmFeatureExecutive.h>
#include <SmAttribute.h>
#include <memory>
namespace {
// Read access to SmFeatureLoop's private members for the export only (the standard explicit
// instantiation access rule; nothing is written).
template <typename Tag, typename Tag::type M>
struct Rob {
friend typename Tag::type get(Tag) { return M; }
};
struct FlBase { typedef SmTArray<SmEdge*> SmFeatureLoop::*type; friend type get(FlBase); };
struct FlFeat { typedef SmTArray<SmEdge*> SmFeatureLoop::*type; friend type get(FlFeat); };
struct FlBPatch { typedef SmBrep* SmFeatureLoop::*type; friend type get(FlBPatch); };
struct FlFPatch { typedef SmBrep* SmFeatureLoop::*type; friend type get(FlFPatch); };
struct FlUseB { typedef SmBoolean SmFeatureLoop::*type; friend type get(FlUseB); };
struct FlUseF { typedef SmBoolean SmFeatureLoop::*type; friend type get(FlUseF); };
struct FlTried { typedef SmBoolean SmFeatureLoop::*type; friend type get(FlTried); };
template struct Rob<FlBase, &SmFeatureLoop::m_sBaseEdges>;
template struct Rob<FlFeat, &SmFeatureLoop::m_sFeatEdges>;
template struct Rob<FlBPatch, &SmFeatureLoop::m_pBasePatch>;
template struct Rob<FlFPatch, &SmFeatureLoop::m_pFeatPatch>;
template struct Rob<FlUseB, &SmFeatureLoop::m_bUseBPatch>;
template struct Rob<FlUseF, &SmFeatureLoop::m_bUseFPatch>;
template struct Rob<FlTried, &SmFeatureLoop::m_bTriedExtSurf>;
struct FtJoined { typedef SmBoolean SmFeature::*type; friend type get(FtJoined); };
template struct Rob<FtJoined, &SmFeature::m_bJoinedPatches>;
SmRebuildBrepType rb(int64_t v) { return (SmRebuildBrepType)v; }
int64_t index_of(const SmTArray<SmEdge*>& all, SmEdge* e) {
for (ULONG i = 0; i < all.GetSize(); ++i)
if (all[i] == e) return (int64_t)i;
return -1;
}
int64_t face_count(SmBrep* b) {
if (!b) return -1;
SmTArray<SmFace*> f;
b->GetFaces(f);
return (int64_t)f.GetSize();
}
// One feature's state: the public getters, then per loop the rebuild types, extension
// distances, the numbers of its base and feature edges, whether it has patches (the same patch for both: 2), their face counts, and the
// use / tried flags.
void put_feature(std::vector<double>& o, SmFeature* f) {
o.push_back(flg(f->GetIsPartitioned()));
o.push_back(flg(f->GetDeatureReady()));
o.push_back(flg(f->GetBaseRebuilt()));
o.push_back(flg(f->GetFeatureRebuilt()));
o.push_back(flg(f->GetBaseWasApplied()));
o.push_back(flg(f->GetFeatureWasApplied()));
o.push_back(flg(f->GetCanRestore()));
SmTArray<SmFace*> faces;
f->GetFaces(faces);
o.push_back((double)faces.GetSize());
o.push_back(f->GetFeatureBrep() ? 1.0 : 0.0);
SmTArray<SmFeatureLoop*> loops;
f->GetFeatureLoops(loops);
o.push_back((double)loops.GetSize());
for (ULONG i = 0; i < loops.GetSize(); ++i) {
SmFeatureLoop* l = loops[i];
o.push_back((double)l->GetBaseRBType());
o.push_back((double)l->GetFeatureRBType());
o.push_back(l->GetBaseExtDistance());
o.push_back(l->GetFeatureExtDistance());
// counts only: after DoDefeature the stitch may have deleted these edges
o.push_back((double)(l->*get(FlBase())).GetSize());
o.push_back((double)(l->*get(FlFeat())).GetSize());
SmBrep* bp = l->*get(FlBPatch());
SmBrep* fp = l->*get(FlFPatch());
o.push_back(bp ? 1.0 : 0.0);
o.push_back(fp ? (fp == bp ? 2.0 : 1.0) : 0.0);
o.push_back((double)face_count(bp));
o.push_back((double)face_count(fp));
o.push_back(flg(l->*get(FlUseB())));
o.push_back(flg(l->*get(FlUseF())));
o.push_back(flg(l->*get(FlTried())));
}
}
// Evaluates `c` at five parameters of its natural interval.
void put_curve_samples(std::vector<double>& o, SmCurve* c) {
o.push_back(c ? (double)c->GetType() : -1.0);
if (!c) return;
SmExtent1d d = c->GetNaturalInterval();
o.push_back(d.GetMin());
o.push_back(d.GetMax());
for (int i = 0; i < 5; ++i) {
SmPoint3d p;
SmStatus s = c->EvaluatePoint(d.Evaluate((double)i / 4), p);
o.push_back(s == SM_SUCCESS ? 0.0 : 1.0);
if (s == SM_SUCCESS) put_vec(o, p);
}
}
// Evaluates `s` on a 3x3 grid of its natural UV domain.
void put_surface_samples(std::vector<double>& o, SmSurface* s) {
o.push_back(s ? (double)s->GetType() : -1.0);
if (!s) return;
SmExtent2d d = s->GetNaturalUVDomain();
o.push_back(d.GetMin().x);
o.push_back(d.GetMin().y);
o.push_back(d.GetMax().x);
o.push_back(d.GetMax().y);
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) {
SmPoint2d uv(d.GetMin().x + (d.GetMax().x - d.GetMin().x) * i / 2.0,
d.GetMin().y + (d.GetMax().y - d.GetMin().y) * j / 2.0);
SmPoint3d p;
SmStatus st_ = s->EvaluatePoint(uv, p);
o.push_back(st_ == SM_SUCCESS ? 0.0 : 1.0);
if (st_ == SM_SUCCESS) put_vec(o, p);
}
}
// Every edge's curve and its number of edge uses, then every face's surface, in list order
// (`topology.brep_mirror`'s record without UV trim curves).
void put_geometry(std::vector<double>& o, SmBrep* b) {
SmTArray<SmEdge*> edges;
b->GetEdges(edges);
o.push_back((double)edges.GetSize());
for (ULONG i = 0; i < edges.GetSize(); ++i) {
put_curve_samples(o, edges[i]->GetCurve());
SmTArray<SmEdgeuse*> uses;
edges[i]->GetEdgeuses(uses);
o.push_back((double)uses.GetSize());
}
SmTArray<SmFace*> faces;
b->GetFaces(faces);
o.push_back((double)faces.GetSize());
for (ULONG i = 0; i < faces.GetSize(); ++i) put_surface_samples(o, faces[i]->GetSurface());
}
// `len geometry... graph_len graph...` of `b`.
void put_brep(std::vector<double>& o, SmBrep* b) {
std::vector<double> g;
put_geometry(g, b);
o.push_back((double)g.size());
o.insert(o.end(), g.begin(), g.end());
std::vector<double> graph = {0.0}; // the import's leading status slot
Graph walked;
walk(b, walked, true);
export_graph(b, walked, graph);
o.push_back((double)graph.size());
o.insert(o.end(), graph.begin(), graph.end());
}
// Whether DoDefeature may extend a surface that has no extension: ExtSurfaceRebuild then
// dereferences the NULL `CreateExtendedSurface` result (an analytic surface without the
// override, e.g. a cylinder). For each loop side rebuilt by ExtendSurface or FromOtherBrep
// with no patch yet and no failed try, the source faces along the loop whose outer loop
// holds a loop edge are extended by the loop's distance (estimated as upstream when
// negative). Before the partition the source faces are taken from the base B-rep: the
// feature's own faces for the feature, the others for the base (the copies share their
// geometry). This may refuse a script where the patch of the other side would be reused.
bool null_extension(const SmContext& ctx, SmFeature* f) {
SmTArray<SmFace*> ffaces;
f->GetFaces(ffaces);
bool part = f->GetIsPartitioned() != FALSE;
SmTArray<SmFeatureLoop*> loops;
f->GetFeatureLoops(loops);
for (ULONG li = 0; li < loops.GetSize(); ++li) {
SmFeatureLoop* l = loops[li];
if (l->*get(FlTried())) continue;
for (int side = 0; side < 2; ++side) {
bool base = side == 0;
SmRebuildBrepType t = base ? l->GetBaseRBType() : l->GetFeatureRBType();
if (t != SM_RB_EXTEND_SURFACE && t != SM_RB_FROM_OTHER_BREP) continue;
if (base ? l->*get(FlBPatch()) : l->*get(FlFPatch())) continue;
bool base_src = base ^ (t == SM_RB_FROM_OTHER_BREP);
double d = base_src ? l->GetBaseExtDistance() : l->GetFeatureExtDistance();
const SmTArray<SmEdge*>& es =
(base_src || !part) ? l->*get(FlBase()) : l->*get(FlFeat());
if (d < 0.0) {
SmExtent3d box;
for (ULONG i = 0; i < es.GetSize(); ++i) {
SmExtent3d one;
es[i]->CalculateBoundingBox(&one);
box.Union(one, box);
}
d = 2. * box.GetMaxLength();
}
SmTArray<SmFace*> fs;
for (ULONG i = 0; i < es.GetSize(); ++i) {
SmTArray<SmFace*> lf;
es[i]->GetFaces(lf);
fs.AppendUnique(lf);
}
for (ULONG i = 0; i < fs.GetSize(); ++i) {
SmFace* face = fs[i];
if (!part && (ffaces.IsIn(face) != FALSE) == base_src) continue;
SmTArray<SmEdge*> outer, common;
face->GetEdges(outer, NULL, TRUE);
outer.FindCommonElements(es, common);
if (common.GetSize() == 0) continue;
SmSurface* ns = NULL;
face->GetSurface()->CreateExtendedSurface(ctx, d, SM_CT_G1, ns);
if (!ns) return true;
delete ns;
}
}
}
return false;
}
// Whether DoDefeature would read a loop edge that is no longer in its B-rep: the
// StitchAndOrient after an applied patch may delete loop edges. A later BuildPatches reads
// the loop edges of every loop it builds a patch for (BuildPatch's source edges, and
// JoinPatches' merged loops), and with joined patches reused for the feature the feature
// edges of every loop (ValidatePatch). Only the pointers are compared.
bool stale_edges(SmFeatureExecutive& ex, SmBrep* b) {
SmTArray<SmFeature*> fs;
ex.GetFeatures(fs);
SmTArray<SmEdge*> be;
b->GetEdges(be);
for (ULONG i = 0; i < fs.GetSize(); ++i) {
SmFeature* f = fs[i];
if (!f->GetIsPartitioned()) continue;
SmBrep* fb = f->GetFeatureBrep();
SmTArray<SmEdge*> fe;
if (fb) fb->GetEdges(fe);
SmTArray<SmFeatureLoop*> loops;
f->GetFeatureLoops(loops);
bool base_build = false, feat_build = false, b_for_f = true;
bool base_stale = false, feat_stale = false;
for (ULONG li = 0; li < loops.GetSize(); ++li) {
SmFeatureLoop* l = loops[li];
if (!(l->*get(FlBPatch()))) base_build = true;
if (!(l->*get(FlFPatch()))) feat_build = true;
if (l->GetFeatureRBType() != SM_RB_FROM_OTHER_BREP) b_for_f = false;
const SmTArray<SmEdge*>& lb = l->*get(FlBase());
const SmTArray<SmEdge*>& lf = l->*get(FlFeat());
for (ULONG j = 0; j < lb.GetSize(); ++j)
if (!be.IsIn(lb[j])) base_stale = true;
for (ULONG j = 0; j < lf.GetSize(); ++j)
if (!fe.IsIn(lf[j])) feat_stale = true;
}
bool validate_joined = (f->*get(FtJoined())) && b_for_f;
if (base_build && (base_stale || feat_stale)) return true;
if (validate_joined && feat_stale) return true;
if (!validate_joined && feat_build && (base_stale || feat_stale)) return true;
}
return false;
}
// The numbers of faces and edges of `b` that carry an attribute of id `id`.
void put_attribute_counts(std::vector<double>& o, SmBrep* b, ULONG id) {
SmTArray<SmFace*> faces;
SmTArray<SmEdge*> edges;
b->GetFaces(faces);
b->GetEdges(edges);
double nf = 0, ne = 0;
for (ULONG i = 0; i < faces.GetSize(); ++i) if (faces[i]->FindAttribute(id)) nf += 1;
for (ULONG i = 0; i < edges.GetSize(); ++i) if (edges[i]->FindAttribute(id)) ne += 1;
o.push_back(nf);
o.push_back(ne);
}
// Number of features whose RestoreFeature would succeed.
int64_t restorable(SmFeatureExecutive& ex) {
SmTArray<SmFeature*> fs;
ex.GetFeatures(fs);
int64_t n = 0;
for (ULONG i = 0; i < fs.GetSize(); ++i)
if (fs[i]->GetCanRestore()) ++n;
return n;
}
} // namespace
// Builds the fixture B-rep from `input` (the shared B-rep block), then runs `script` on an
// SmFeatureExecutive over it (layout in the FFI module). Output: `record_len record...` then
// the base B-rep (`put_brep`) and, per feature of GetFeatureBreps, `0` (none) or `1`
// `put_brep`. A failed fixture build returns `-999 status`; an index past the fixture's faces
// or edges `-998`; a RestoreFeatures with two or more restorable features `-997` (upstream
// removes the wrong entry after the first and keeps a deleted feature); a DoDefeature
// that would read a loop edge an applied patch's stitch deleted `-996`; a DoDefeature that
// may extend a surface without an extension `-995` (NULL dereference in ExtSurfaceRebuild).
SC_EXPORT int32_t sc_blend_feature_executive_eval(const char* path, const double* input, int64_t n,
const double* script, int64_t ns, double* out, int64_t cap, int64_t* out_len) {
if (!input || n < 1 || !path || !out_len || !script || ns < 2) return SC_ERR_ARG;
SC_TRY
Rd rd{input, 0};
SmStatus status = SM_SUCCESS;
Built built;
built.brep = build(rd, path, status);
if (!built.brep || status != SM_SUCCESS) {
std::vector<double> o = {-999.0, st(status)};
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
}
SmBrep* b = built.brep;
const SmContext& ctx = the_context();
SmTArray<SmFace*> faces;
SmTArray<SmEdge*> edges;
b->GetFaces(faces);
b->GetEdges(edges);
std::vector<double> bad = {-998.0};
Rd sc{script, 0};
auto face_at = [&](int64_t i) -> SmFace* {
return (i >= 0 && i < (int64_t)faces.GetSize()) ? faces[(ULONG)i] : nullptr;
};
auto edge_at = [&](int64_t i) -> SmEdge* {
return (i >= 0 && i < (int64_t)edges.GetSize()) ? edges[(ULONG)i] : nullptr;
};
std::vector<double> record;
std::vector<SmFeatureLoop*> made_loops;
struct LoopsClean {
std::vector<SmFeatureLoop*>& v;
~LoopsClean() { for (auto* l : v) delete l; }
} loops_clean{made_loops};
// attributes: kind (0 face, 1 edge) index id behavior -> SmLongAttribute(id, 7, behavior)
std::vector<ULONG> attr_ids;
int64_t n_attrs = sc.idx();
for (int64_t ai = 0; ai < n_attrs; ++ai) {
int64_t kind = sc.idx();
int64_t index = sc.idx();
ULONG id = (ULONG)sc.idx();
SmAttributeBehaviorType behavior = (SmAttributeBehaviorType)sc.idx();
SmAObject* obj = kind == 0 ? (SmAObject*)face_at(index) : (SmAObject*)edge_at(index);
if (!obj) return sc_copy_out(bad.data(), 1, out, cap, out_len);
obj->AddAttribute(new (ctx) SmLongAttribute(id, 7, behavior));
attr_ids.push_back(id);
}
SmFeatureExecutive ex(ctx, *b);
int64_t n_features = sc.idx();
for (int64_t fi = 0; fi < n_features; ++fi) {
int64_t kind = sc.idx();
SmRebuildBrepType base_rb = rb(sc.idx());
SmRebuildBrepType feat_rb = rb(sc.idx());
SmFeature* made = nullptr;
if (kind == 0) {
int64_t nf = sc.idx();
SmTArray<SmFace*> fs;
for (int64_t i = 0; i < nf; ++i) {
SmFace* f = face_at(sc.idx());
if (!f) return sc_copy_out(bad.data(), 1, out, cap, out_len);
fs.Add(f);
}
made = ex.AddFeature(fs, base_rb, feat_rb);
} else if (kind == 1) {
SmFace* f = face_at(sc.idx());
int64_t ne = sc.idx();
SmTArray<SmEdge*> es;
for (int64_t i = 0; i < ne; ++i) {
SmEdge* e = edge_at(sc.idx());
if (!e) return sc_copy_out(bad.data(), 1, out, cap, out_len);
es.Add(e);
}
if (!f) return sc_copy_out(bad.data(), 1, out, cap, out_len);
made = ex.AddFeature(f, es, base_rb, feat_rb);
} else {
SmFace* f = face_at(sc.idx());
int64_t nl = sc.idx();
SmTArray<SmFeatureLoop*> ls;
for (int64_t li = 0; li < nl; ++li) {
SmRebuildBrepType lb = rb(sc.idx());
SmRebuildBrepType lf = rb(sc.idx());
int64_t ne = sc.idx();
SmTArray<SmEdge*> es;
for (int64_t i = 0; i < ne; ++i) {
SmEdge* e = edge_at(sc.idx());
if (!e) return sc_copy_out(bad.data(), 1, out, cap, out_len);
es.Add(e);
}
SmFeatureLoop* l = new SmFeatureLoop(es, lb, lf);
made_loops.push_back(l);
ls.Add(l);
}
if (!f) return sc_copy_out(bad.data(), 1, out, cap, out_len);
made = ex.AddFeature(f, ls);
}
record.push_back(made ? 1.0 : 0.0);
// the loops' base edges in their sorted order, as indices of GetEdges
if (made) {
SmTArray<SmFeatureLoop*> loops;
made->GetFeatureLoops(loops);
record.push_back((double)loops.GetSize());
for (ULONG i = 0; i < loops.GetSize(); ++i) {
const SmTArray<SmEdge*>& be = loops[i]->*get(FlBase());
record.push_back((double)be.GetSize());
for (ULONG j = 0; j < be.GetSize(); ++j)
record.push_back((double)index_of(edges, be[j]));
}
}
}
// ops: 0 DoDefeature, 1 SetBaseRBType(t), 2 SetFeatureRBType(t), 3 SetBaseExtDist(d),
// 4 SetFeatureExtDist(d), 5 RestoreFeatures
int64_t n_ops = sc.idx();
for (int64_t oi = 0; oi < n_ops; ++oi) {
int64_t op = sc.idx();
if (op == 0) {
if (stale_edges(ex, b)) {
std::vector<double> ub = {-996.0};
return sc_copy_out(ub.data(), 1, out, cap, out_len);
}
SmTArray<SmFeature*> pre;
ex.GetFeatures(pre);
for (ULONG i = 0; i < pre.GetSize(); ++i)
if (null_extension(ctx, pre[i])) {
std::vector<double> ub = {-995.0};
return sc_copy_out(ub.data(), 1, out, cap, out_len);
}
SmBrep* pb = nullptr;
SmTArray<SmBrep*> fbs;
SmStatus s = ex.DoDefeature(pb, &fbs);
record.push_back(st(s));
record.push_back(pb == b ? 1.0 : 0.0);
record.push_back((double)fbs.GetSize());
} else if (op == 1) {
ex.SetBaseRBType(rb(sc.idx()));
} else if (op == 2) {
ex.SetFeatureRBType(rb(sc.idx()));
} else if (op == 3) {
ex.SetBaseExtDist(sc.num());
} else if (op == 4) {
ex.SetFeatureExtDist(sc.num());
} else {
if (restorable(ex) >= 2) {
std::vector<double> ub = {-997.0};
return sc_copy_out(ub.data(), 1, out, cap, out_len);
}
record.push_back(st(ex.RestoreFeatures()));
}
}
SmTArray<SmFeature*> fs;
ex.GetFeatures(fs);
record.push_back((double)fs.GetSize());
for (ULONG i = 0; i < fs.GetSize(); ++i) put_feature(record, fs[i]);
{
SmTArray<SmBrep*> fbs;
ex.GetFeatureBreps(fbs);
for (ULONG id : attr_ids) {
put_attribute_counts(record, b, id);
for (ULONG i = 0; i < fbs.GetSize(); ++i) {
if (fbs[i]) put_attribute_counts(record, fbs[i], id);
else { record.push_back(-1.0); record.push_back(-1.0); }
}
}
}
std::vector<double> result = {(double)record.size()};
result.insert(result.end(), record.begin(), record.end());
put_brep(result, b);
SmTArray<SmBrep*> fbs;
ex.GetFeatureBreps(fbs);
for (ULONG i = 0; i < fbs.GetSize(); ++i) {
if (!fbs[i]) {
result.push_back(0.0);
continue;
}
result.push_back(1.0);
put_brep(result, fbs[i]);
}
// the executive does not delete the feature B-reps
for (ULONG i = 0; i < fbs.GetSize(); ++i) delete fbs[i];
return sc_copy_out(result.data(), (int64_t)result.size(), out, cap, out_len);
SC_CATCH
}