// SPDX-License-Identifier: Apache-2.0 // Real native adapter for offset.offset_executive_p2 (draft PR #201). // Upstream pinned c9c5979: source/SMLib/{inc,src}/SmOffsetExecutive.{h,cpp}, // source/SM_API/src/SmApiBrep.cpp. Calls all six functions owned by units.json. // Protocol/DTOs: crates/solidcore-ffi/src/units/offset_offset_executive_p2.rs. // Deviations: bounded analytic fixtures, traversal IDs and sampled observations; // no native implementation/helper is included or reimplemented. SC_OK is zero, // native SM_SUCCESS is 1000. [-1,status] is ONLY a fixture-construction rejection. // // Ownership: observe actual per-case B-rep construction/destruction notifications. // Each distinct live allocation is reclaimed once, including private native // working B-reps abandoned on early errors. The executive is destroyed FIRST; // its m_pOffsetBrep remains exclusively executive-owned until then. The static // shell entrypoint uses the explicit transfer-reporting overload. Source lifetime // is independently observed, so retained inputs, aliases and consumed inputs // cannot produce duplicate deletes or stale source snapshots. No address escapes. // The upstream global API context is kept alive with its normal configuration; // our scoped callback and doing-Boolean guard are restored before returning. // The shared native-call guard excludes every other shim until that restoration // and all native destruction complete (including exceptions and buffer retries). #include "sc_shim.h" #include "StdAfx.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int64_t max_selections = 1024; constexpr int64_t max_input = 4096; constexpr int64_t max_file_path = 1024; // Selection bound for a file fixture, whose face count is known only once read. constexpr int64_t max_file_faces = 1024; constexpr size_t max_output = 262144; constexpr size_t no_allocation = static_cast(-1); struct Argument {}; struct Rejected { SmStatus status; }; void finite(double v) { if (!std::isfinite(v)) throw Argument{}; } struct Reader { const double* data; int64_t size; int64_t pos = 0; double number() { if (pos >= size) throw Argument{}; double v = data[pos++]; finite(v); return v; } int64_t integer(int64_t lo, int64_t hi) { double v = number(); if (v < (double)lo || v > (double)hi || std::floor(v) != v) throw Argument{}; return (int64_t)v; } SmBoolean flag() { return integer(0, 1) ? TRUE : FALSE; } }; void interval(double a, double b, bool strict) { if (a > b || (strict && a == b)) throw Argument{}; finite(b-a); } struct Spec { int kind = 0; std::array v{}; std::string path; // kind 7: an ASCII .smb file int64_t faces() const { return kind == 3 ? 1 : kind == 4 ? 6 : kind == 5 ? 3 : kind == 6 ? 1 : kind == 7 ? max_file_faces : kind == 8 ? 1 : 0; } }; Spec read_spec(Reader& rd) { Spec s; s.kind = (int)rd.integer(0, 8); if (s.kind == 7) { // the byte count, then one value per byte (no NUL) int64_t n = rd.integer(1, max_file_path); for (int64_t i=0; i v[7]) throw Argument{}; double squared = 0; for (int i=0; i<3; ++i) squared += (v[i+3]-v[i])*(v[i+3]-v[i]); finite(squared); } else if (s.kind == 3) { interval(v[11], v[13], true); interval(v[12], v[14], true); for (int begin : {3, 6}) { double squared = 0; for (int i=begin; i 0) || !(v[4] > 0)) throw Argument{}; } return s; } struct Selection { int origin; int64_t index; }; struct Program { int operation; double tolerance, distance, member_distance, member_tol; SmOffsetOperationType member_operation; SmBoolean inset, extend, merge, solid, skip; Spec source, foreign; std::vector selections; int attributed = -1; // -1: no attributes, else the tags' SmAttributeBehaviorType bool member() const { return operation == 2 || operation == 3; } bool can_extend() const { // Called only after the whole DTO is parsed and every index validated. // Member NER and both API null checks precede extension arithmetic; // SmApiShellBrepFull also rejects foreign/null selectors before copying. // Use DTO origins, never traversal-index equality, to identify that path. if (source.kind == 0) return false; if (operation == 4) for (const auto& selection : selections) if (selection.origin != 0) return false; // Keep the conservative guard for every other non-null fixture, // including empty/wire B-reps; no additional unreachability is assumed. return operation == 3 || extend != FALSE; } }; Program read_program(Reader& rd) { Program p; p.operation = (int)rd.integer(0, 5); p.tolerance = rd.number(); p.distance = rd.number(); p.member_distance = rd.number(); p.member_tol = rd.number(); p.member_operation = (SmOffsetOperationType)rd.integer(0, 4); p.inset = rd.flag(); p.extend = rd.flag(); p.merge = rd.flag(); p.solid = rd.flag(); p.skip = rd.flag(); if (p.member() && p.member_distance < 0.0) throw Argument{}; p.source = read_spec(rd); p.foreign = read_spec(rd); if (p.source.kind == 0 && (p.operation == 0 || p.operation == 1)) throw Argument{}; int64_t count = rd.integer(0, max_selections); // Parse everything before allocating native geometry. No input-sized reserve. for (int64_t i=0; i= (origin == 0 ? p.source.faces() : p.foreign.faces())) throw Argument{}; p.selections.push_back({origin, index}); } p.attributed = (int)rd.integer(-1, 4); if (rd.pos != rd.size) throw Argument{}; // All scalars remain finite and all selectors remain bounds-checked, even // for proven early returns. Only the unused fivefold product is exempted. if (p.can_extend()) finite((p.member() ? p.member_distance : std::fabs(p.distance))*5.0); return p; } // SmBrep emits construction only after its topology lists are initialized, and // destruction before deleting those lists (SmBrep.cpp:576,682,707). This observer // never changes topology. It catches otherwise inaccessible working allocations // on native early returns as well as the public result. It is scoped to this // single synchronous call; system callbacks and the existing user callback stay // intact. Tokens, not pointer equality, distinguish address reuse after deletion. class Allocations : public SmNotifyCallback { struct Entry { SmBrep* ptr; bool live; }; SmContext& context; SmNotifyCallback* previous; std::vector entries; public: explicit Allocations(SmContext& ctx) : context(ctx), previous(ctx.GetUserNotifyCallback()) { entries.reserve(64); // fixed adapter bookkeeping, not an input-declared size context.SetUserNotifyCallback(this); } void finish() { // All executive instances must already have been destroyed. Continue // reclaiming distinct allocations even if a native destructor throws; // normal-path cleanup reports that as SC_ERR_EXCEPTION at the boundary. bool failed = false; for (size_t i=entries.size(); i>0; --i) { if (entries[i-1].live) { SmBrep* ptr = entries[i-1].ptr; entries[i-1].live = false; try { delete ptr; } catch (...) { failed = true; } } } context.SetUserNotifyCallback(previous); if (failed) throw std::runtime_error("native B-rep destruction threw"); } ~Allocations() override { // Never throw a second exception while unwinding a native operation. try { finish(); } catch (...) { context.SetUserNotifyCallback(previous); } } SmStatus Execute(SmNotifyOperation action, SmObject* object, SmObject* a, SmObject* b, SmObject* c) override { if (action == SM_NO_CONSTRUCTION && object && object->IsKindOf(SmBrep_TYPE)) entries.push_back({static_cast(object), true}); else if (action == SM_NO_DESTRUCTION) { for (auto& entry : entries) if (entry.live && entry.ptr == object) entry.live = false; } return previous ? previous->Execute(action, object, a, b, c) : SM_SUCCESS; } size_t token(const SmBrep* ptr) const { if (!ptr) return no_allocation; for (size_t i=entries.size(); i>0; --i) if (entries[i-1].live && entries[i-1].ptr == ptr) return i-1; throw std::runtime_error("unobserved native B-rep allocation"); } bool live(size_t token) const { return token != no_allocation && entries.at(token).live; } }; struct BooleanRestore { SmContext& context; SmBoolean previous; explicit BooleanRestore(SmContext& ctx) : context(ctx), previous(ctx.GetDoingBoolean()) {} ~BooleanRestore() { context.GetDoingBooleanRef() = previous; } }; void constructed(SmStatus status) { if (status != SM_SUCCESS) throw Rejected{status}; } template struct PendingGeometry { T* ptr; ~PendingGeometry() { if (ptr && !ptr->GetOwner()) delete ptr; } }; SmPoint3d point(const std::array& v, size_t i) { return SmPoint3d(v[i], v[i+1], v[i+2]); } SmBrep* build(const Spec& s, SmContext& ctx, double tolerance) { if (!s.kind) return nullptr; const auto& v = s.v; SmBrep* result = nullptr; if (s.kind == 4) { constructed(SmApiCreateBox(point(v, 0), v[3], v[4], v[5], result)); if (!result) throw std::runtime_error("successful box constructor returned null"); // CreateBox ignores CreateFaceFromSurface statuses (SmPrimitiveCreation.cpp: // 294-344), so SM_SUCCESS alone does not establish a constructed box. SmTArray faces; SmTArray edges; SmTArray vertices; result->GetFaces(faces); result->GetEdges(edges); result->GetVertices(vertices); // Tiny / far-translated boxes come back as an EMPTY B-rep (0 faces): a // defined native result, not a crash, so it is built and compared as is. result->SetTolerance(tolerance, FALSE); return result; } if (s.kind == 5 || s.kind == 6) { // The real primitive constructors (compared in primitive.api_primitives), // then the fixture tolerance, as for the box. SmVector3d origin(v[0], v[1], v[2]); if (s.kind == 5) constructed(SmApiCreateCylinder(origin, v[3], v[4], result)); else constructed(SmApiCreateSphere(origin, v[3], result)); if (!result) throw std::runtime_error("successful primitive constructor returned null"); result->SetTolerance(tolerance, FALSE); return result; } result = new (ctx) SmBrep(); if (s.kind == 7) { // smOffsetTest.cpp ShellBrep: ReadFromFile, keeping the file's tolerance. constructed(result->ReadFromFile(ctx, s.path.c_str(), SM_ASCII)); return result; } result->SetTolerance(tolerance, FALSE); if (s.kind == 2) { SmExtent1d domain(v[6], v[7]), trim(v[8], v[9]); SmLine* line = nullptr; SmStatus status = SmLine::CreateLineSegment(ctx, 3, point(v, 0), point(v, 3), line, &domain); PendingGeometry pending{line}; constructed(status); if (!line) throw std::runtime_error("successful line constructor returned null"); SmPoint3d a(0,0,0), b(0,0,0); constructed(line->EvaluatePoint(trim.GetMin(), a)); constructed(line->EvaluatePoint(trim.GetMax(), b)); SmShell* shell = nullptr; SmVertex *first = nullptr, *second = nullptr; constructed(result->MakeShellVertex(nullptr, a, shell, first)); constructed(result->MakeShellVertex(nullptr, b, shell, second)); SmEdge* edge = nullptr; // MakeWireEdge may attach the curve before returning an error. The // pending guard only deletes it if it still has no native topology owner. constructed(result->MakeWireEdge(nullptr, first, second, line, trim, SM_OT_SAME, edge)); if (!edge) throw std::runtime_error("successful wire constructor returned null"); } else if (s.kind == 8) { // A cylinder sheet: CreateFaceFromSurface on a bounded SmCylinder over its // natural domain (closed round along a seam edge). PendingGeometry pending{ new (ctx) SmCylinder(point(v,0), SmVector3d(1,0,0), SmVector3d(0,1,0), v[3], 0.0, 360.0, v[4], FALSE, FALSE, FALSE, &ctx) }; SmExtent2d domain = pending.ptr->GetNaturalUVDomain(); SmFace* face = nullptr; constructed(result->CreateFaceFromSurface(pending.ptr, domain, face)); if (!face) throw std::runtime_error("successful sheet constructor returned null"); } else if (s.kind == 3) { SmExtent2d domain(SmPoint2d(v[11],v[12]), SmPoint2d(v[13],v[14])); PendingGeometry pending{ new (ctx) SmPlane(point(v,0), point(v,3), point(v,6), SmVector2d(v[9],v[10]), domain, &ctx) }; SmFace* face = nullptr; constructed(result->CreateFaceFromSurface(pending.ptr, domain, face)); // CreateFaceFromSurface may succeed after tolerance-driven boundary // merges/removal, leaving a degenerate face; that is a defined native // result and is built and compared as is. if (!face) throw std::runtime_error("successful sheet constructor returned null"); } return result; } // Same safety preflight as p1: public native extraction/normalization checks, // not a replacement for extension. NL_CrvAdv's zero-tangent fallback reads a // third control point even when its isocurve has only two. SmPlane derives from // SmBSplineSurface, so real analytic sheets and box faces require this check too. void guard_two_point_isocurves(SmSurface* base) { SmBSplineSurface* spline = SM_CAST_PTR(SmBSplineSurface, base); if (!spline) return; gw_SURFACE* surface = spline->GetOrCreateGwNurbPointer(); if (!surface) throw Argument{}; for (int direction=0; direction<2; ++direction) { if ((direction == 0 ? surface->net->n : surface->net->m) != 1) continue; gw_KNOTVECTOR* knots = direction == 0 ? surface->knv : surface->knu; for (double fraction : {0.07, 0.19, 0.45, 0.73, 0.92}) { double parameter = (1.0-fraction)*knots->U[0] + fraction*knots->U[knots->m]; std::unique_ptr curve( sm_AllocateNurbCurve(1, 1, 3), &sm_FreeNurbCurve); if (!curve) throw std::bad_alloc(); if (N_SrfExtractIsoCrv(surface, parameter, direction == 0 ? NL_UDIR : NL_VDIR, curve.get(), nullptr) != 0) continue; NL_POINT a, b; NL_VECTOR tangent, unit; NL_REAL magnitude = 0; N_CPtToPtEuclid(curve->pol->Pw[0], &a); N_CPtToPtEuclid(curve->pol->Pw[1], &b); N_VectorDir(a, b, &tangent); if (N_VectorNormalize(tangent, &unit, &magnitude) != 0) throw Argument{}; } } } // Only access to protected flags for observation. No behavior override. class Executive : public SmOffsetExecutive { public: Executive(SmContext& ctx, SmOffsetOperationType op, SmOffsetGeometryCreation& geometry, SmBrep* source) : SmOffsetExecutive(ctx, op, geometry, source) {} bool merge() const { return m_bMergeResults != FALSE; } bool solid() const { return m_bCreateOffsetSolid != FALSE; } }; template double id(const SmTArray& traversal, const T* ptr) { if (!ptr) return -1; for (ULONG i=0; i& out, const SmPoint3d& p) { out.insert(out.end(), {p.x,p.y,p.z}); } void put_domain(std::vector& out, const SmExtent2d& d) { out.insert(out.end(), {d.GetMin().x,d.GetMin().y,d.GetMax().x,d.GetMax().y}); } int surface_kind(SmSurface* surface) { return !surface ? 0 : surface->IsKindOf(SmPlane_TYPE) ? 1 : surface->IsKindOf(SmBSplineSurface_TYPE) ? 2 : 3; } int curve_kind(SmCurve* curve) { return !curve ? 0 : curve->IsKindOf(SmLine_TYPE) ? 1 : curve->IsKindOf(SmBSplineCurve_TYPE) ? 2 : 3; } struct Block { bool present = false; std::vector integers, values, caches; }; // The lazily filled topology caches the Rust purity check also sees (its whole B-rep // state): per vertex use (GetVertexuses order) the vertex/edge gap's init flag and edge // parameter and the vertex/face gap's init flag and face UV, per edge use (GetEdgeuses // order) the max edge/face gap's init flag. Read through the const getters only when // already initialised, so observing never fills a cache. void put_caches(std::vector& out, SmBrep* brep, const SmTArray& edgeuses) { SmTArray uses; brep->GetVertexuses(uses); if ((size_t)uses.GetSize() + edgeuses.GetSize() > max_output/64) throw std::length_error("native snapshot cache budget"); for (ULONG i=0; iIsVertexEdgeGapInit() ? vu->GetVertexEdgeGap() : nullptr; SmVertexFaceGap* face = vu->IsVertexFaceGapInit() ? vu->GetVertexFaceGap() : nullptr; out.insert(out.end(), {edge ? 1.0 : 0.0, edge ? edge->GetEdgeT() : 0.0, face ? 1.0 : 0.0, face ? face->GetFaceUV().x : 0.0, face ? face->GetFaceUV().y : 0.0}); } for (ULONG i=0; iIsMaxEdgeFaceGapInit() ? 1.0 : 0.0); } template void put_ids(std::vector& out, const SmTArray& list, const SmTArray& traversal) { out.push_back((double)list.GetSize()); for (ULONG i=0; i regions; SmTArray shells; SmTArray faces; SmTArray loops; SmTArray edges; SmTArray edgeuses; SmTArray vertices; brep->GetRegions(regions); brep->GetShells(shells); brep->GetFaces(faces); brep->GetLoops(loops); brep->GetEdges(edges); brep->GetEdgeuses(edgeuses); brep->GetVertices(vertices); size_t total = (size_t)regions.GetSize() + shells.GetSize() + faces.GetSize() + loops.GetSize() + edges.GetSize() + edgeuses.GetSize() + vertices.GetSize(); if (total > max_output/64) throw std::length_error("native snapshot topology budget"); ints.insert(ints.end(), { (double)regions.GetSize(), (double)shells.GetSize(), (double)faces.GetSize(), (double)loops.GetSize(), (double)edges.GetSize(), (double)edgeuses.GetSize(), (double)vertices.GetSize(), id(regions,brep->GetInfiniteRegion()), brep->m_bEditingEnabled ? 1.0 : 0.0, brep->m_bValidateOnly ? 1.0 : 0.0 }); vals.push_back((double)brep->GetTolerance()); for (ULONG i=0; iIsVoid() ? 1.0 : 0.0}); for (ULONG i=0; iGetRegion())}); for (ULONG i=0; iGetSurface(); SmFaceuse* up = face->GetUpwardFaceuse(); SmFaceuse* mate = up ? up->GetMate() : nullptr; ints.insert(ints.end(), {(double)i, (double)surface_kind(surface), id(shells,up ? up->GetShell() : nullptr), id(shells,mate ? mate->GetShell() : nullptr), up ? (double)up->GetOrientation() : -1.0, mate ? (double)mate->GetOrientation() : -1.0}); SmTArray face_loops; face->GetLoops(face_loops); put_ids(ints,face_loops,loops); vals.push_back((double)face->GetTolerance()); SmExtent2d domain = face->GetUVDomain(); put_domain(vals,domain); if (surface) put_domain(vals,surface->GetNaturalUVDomain()); else vals.insert(vals.end(),4,0.0); SmPoint2d low = domain.GetMin(), high = domain.GetMax(); for (double fraction : {0.0,0.5,1.0}) { SmPoint2d uv((1.0-fraction)*low.x + fraction*high.x, (1.0-fraction)*low.y + fraction*high.y); SmPoint3d p(0,0,0); SmVector3d normal(0,0,0); // Nonfinite returned domains are observable, but must not become // unchecked NLib span indices. -1 means sampling was not attempted. SmStatus ps = (SmStatus)-1, ns = (SmStatus)-1; if (surface && std::isfinite(uv.x) && std::isfinite(uv.y)) { ps = surface->EvaluatePoint(uv,p); ns = surface->EvaluateNormal(uv,TRUE,TRUE,normal); } ints.insert(ints.end(), {(double)ps,(double)ns}); put_point(vals,p); put_point(vals,normal); } } for (ULONG i=0; iGetFace())}); SmTArray loop_edgeuses; loops[i]->GetEdgeuses(loop_edgeuses); put_ids(ints,loop_edgeuses,edgeuses); } for (ULONG i=0; iGetCurve(); ints.insert(ints.end(), {(double)i,id(vertices,edge->GetStartVertex()), id(vertices,edge->GetEndVertex()),(double)curve_kind(curve)}); SmTArray uses; edge->GetEdgeuses(uses); put_ids(ints,uses,edgeuses); SmExtent1d trim = edge->GetInterval(); vals.insert(vals.end(), {(double)edge->GetTolerance(),trim.GetMin(),trim.GetMax()}); if (curve) { SmExtent1d domain = curve->GetNaturalInterval(); vals.insert(vals.end(), {domain.GetMin(),domain.GetMax()}); } else vals.insert(vals.end(),2,0.0); for (double t : {trim.GetMin(),trim.GetMax()}) { SmPoint3d p(0,0,0); SmStatus status = (SmStatus)-1; if (curve && std::isfinite(t)) status = curve->EvaluatePoint(t,p); ints.push_back((double)status); put_point(vals,p); } } for (ULONG i=0; iGetEdge()),id(faces,eu->GetFace()), id(shells,eu->GetShell()),id(edgeuses,eu->GetMate()),id(edgeuses,eu->GetRadial()), (double)eu->GetOrientation()}); } for (ULONG i=0; iGetPoint()); vals.push_back((double)vertices[i]->GetTolerance()); } put_caches(block.caches,brep,edgeuses); return block; } void put_block(std::vector& out, const Block& block) { out.push_back(block.present ? 1.0 : 0.0); if (!block.present) return; if (block.integers.size() + block.values.size() + out.size() + 2 > max_output) throw std::length_error("native snapshot output budget"); out.push_back((double)block.integers.size()); out.insert(out.end(),block.integers.begin(),block.integers.end()); out.push_back((double)block.values.size()); out.insert(out.end(),block.values.begin(),block.values.end()); if (block.caches.size() + out.size() + 1 > max_output) throw std::length_error("native snapshot output budget"); out.push_back((double)block.caches.size()); out.insert(out.end(),block.caches.begin(),block.caches.end()); } // Attributed cases (#1462): every source face (GetFaces order) and edge (GetEdges order) // gets its own SmLongAttribute, id = value = attribute_base + index (+ 500 for an edge). constexpr ULONG attribute_base = 10007; void tag_source(SmBrep* b, SmContext& ctx, SmAttributeBehaviorType behavior) { SmTArray faces; b->GetFaces(faces); for (ULONG i=0; iAddAttribute(new (ctx) SmLongAttribute(id, (long)id, behavior)); } SmTArray edges; b->GetEdges(edges); for (ULONG i=0; iAddAttribute(new (ctx) SmLongAttribute(id, (long)id, behavior)); } } // One object's row: `n`, then per attribute `id behavior value` (value: an // SmLongAttribute's, else 0), the attributes in ascending order. std::vector attribute_row(const SmAObject* obj) { SmTArray attrs; obj->GetAttributes(attrs); std::vector> entries; for (ULONG i=0; i(a); entries.push_back({(double)a->GetAttributeID(), (double)a->GetBehavior(), l ? (double)l->GetValue() : 0.0}); } std::sort(entries.begin(), entries.end()); std::vector row{(double)entries.size()}; for (const auto& e : entries) row.insert(row.end(), e.begin(), e.end()); return row; } template void put_rows(std::vector& out, const SmTArray& objects) { std::vector> rows; for (ULONG i=0; i& out, SmBrep* brep) { out.push_back(brep ? 1.0 : 0.0); if (!brep) return; SmTArray faces; SmTArray edges; brep->GetFaces(faces); brep->GetEdges(edges); std::vector rows; put_rows(rows, faces); put_rows(rows, edges); if (rows.size() + out.size() + 1 > max_output) throw std::length_error("native attribute output budget"); out.push_back((double)rows.size()); out.insert(out.end(), rows.begin(), rows.end()); } std::vector observe_selectors(const SmTArray& selectors, SmBrep* source, SmBrep* foreign, const std::vector* before = nullptr) { SmTArray sources, foreigners; if (source) source->GetFaces(sources); if (foreign) foreign->GetFaces(foreigners); std::vector result; for (ULONG i=0; iat((size_t)i*2) == 0.0) { result.insert(result.end(), {3.0,before->at((size_t)i*2+1)}); continue; // Do not even compare the consumed source face pointer. } SmFace* face = selectors[i]; double src = id(sources,face), other = id(foreigners,face); if (!face) result.insert(result.end(), {2.0,-1.0}); else if (src >= 0) result.insert(result.end(), {0.0,src}); else if (other >= 0) result.insert(result.end(), {1.0,other}); else result.insert(result.end(), {4.0,-1.0}); } return result; } // The volume queries of the measured entry point (sc_offset_offset_executive_p2_eval_measured), // at the accuracy the Rust side's tests use. constexpr double volume_accuracy = 1e-6; // `SmApiBrepComputeVolume(brep)` as `status volume` (the native status; volume 0 on // failure); `-1 0` for no B-rep. void put_volume(std::vector& out, SmBrep* brep) { if (!brep) { out.insert(out.end(), {-1.0, 0.0}); return; } double volume = 0.0; SmApiStatus status = SmApiBrepComputeVolume(brep, volume_accuracy, volume); out.push_back((double)status); out.push_back(status == SM_SUCCESS ? volume : 0.0); } // The operand state a volume query can write (#1528), read without writing anything: per // edge use (`GetEdgeuses` order) `0` without UV trim curve (`GetUVTrimCurvePointer`), `2` // for one without NURB, else `1` then its first and last homogeneous control points // `x y w`; then per vertex use (`GetVertexuses` order) `0` when its vertex/face gap is not // initialised, else `1 gap u v`. Starts with the number of edge uses holding a trim curve. void put_operand_state(std::vector& out, SmBrep* brep) { if (!brep) return; SmTArray edgeuses; SmTArray vertexuses; brep->GetEdgeuses(edgeuses); brep->GetVertexuses(vertexuses); if ((size_t)edgeuses.GetSize() + vertexuses.GetSize() > max_output/16) throw std::length_error("native operand state budget"); double trimmed = 0.0; for (ULONG i=0; iGetUVTrimCurvePointer()) trimmed += 1.0; out.push_back(trimmed); for (ULONG i=0; iGetUVTrimCurvePointer(); const gw_CURVE* g = trim ? trim->GetGwNurbPointer() : nullptr; if (!trim) { out.push_back(0.0); continue; } if (!g) { out.push_back(2.0); continue; } out.push_back(1.0); for (gw_INDEX k : {(gw_INDEX)0, g->pol->n}) out.insert(out.end(), {g->pol->Pw[k].x, g->pol->Pw[k].y, g->pol->Pw[k].w}); } for (ULONG i=0; iIsVertexFaceGapInit()) { out.push_back(0.0); continue; } const SmVertexFaceGap* gap = vu->GetVertexFaceGap(); SmPoint2d uv = gap->GetFaceUV(); out.insert(out.end(), {1.0, gap->GetLength(), uv.x, uv.y}); } } // `volume_query`: -1 none; 0 the result's volume after the call; 1 also the source's volume // queried on the operand before the call (#1528: a query that changes a later operation). // A measured snapshot ends with `operand_state... operand_len pre_status pre_volume // post_status post_volume`, the operand state read right after the (optional) query and // before the call (`put_operand_state`). void run(const Program& p, std::vector& out, int volume_query) { SmContext& ctx = *SmApiGetOrCreateContext(); BooleanRestore restore(ctx); // outlives all per-case B-rep destruction Allocations allocations(ctx); SmBrep* source = build(p.source,ctx,p.tolerance); SmBrep* foreign = build(p.foreign,ctx,p.tolerance); size_t source_token = allocations.token(source), foreign_token = allocations.token(foreign); SmTArray source_faces, foreign_faces, selectors; SmTArray api_selectors; if (source && p.attributed >= 0) tag_source(source, ctx, (SmAttributeBehaviorType)p.attributed); std::vector pre_volume = {-1.0, 0.0}; if (volume_query == 1) { pre_volume.clear(); put_volume(pre_volume, source); } std::vector operand_state; if (volume_query >= 0) put_operand_state(operand_state, source); if (source) source->GetFaces(source_faces); if (foreign) foreign->GetFaces(foreign_faces); for (const auto& selection : p.selections) { SmFace* face = nullptr; if (selection.origin != 2) { const auto& list = selection.origin == 0 ? source_faces : foreign_faces; // Expected construction counts are not permission to index an // unexpectedly different native topology. This is a layout error, // never a fabricated constructor rejection or native operation status. // A constructor can legitimately leave fewer faces than the DTO // expects (an empty tiny box): there is then no face to select and // nothing to run on either side, which is a fixture rejection. if (selection.index < 0 || (ULONG)selection.index >= list.GetSize()) throw Rejected{SM_ERR}; face = list[(ULONG)selection.index]; } selectors.Add(face); api_selectors.Add(face); } // Do not cross API selection validation merely to perform a geometry safety // preflight: invalid API selectors must reach the actual early API return. if (source && p.can_extend()) { for (ULONG i=0; iGetSurface()); } Block source_before = snapshot(source), foreign_before = snapshot(foreign); auto selected_before = observe_selectors(selectors,source,foreign); SmOffsetGeometryCreation geometry(p.member_distance,p.member_tol); std::unique_ptr executive; if (p.member()) { executive.reset(new Executive(ctx,p.member_operation,geometry,source)); executive->SetInset(p.inset); executive->SetExtendConvexEdges(p.extend); executive->SetMergeResults(p.merge); executive->SetCreateOffsetSolid(p.solid); executive->SetShellFaces(selectors); } SmBrep* result = nullptr; SmBoolean transferred = FALSE; SmBoolean boolean_before = ctx.GetDoingBoolean(); SmStatus status = SM_ERR; switch (p.operation) { case 0: status = SmOffsetExecutive::OffsetBrep(ctx,source,p.distance,p.extend,p.merge,result); break; case 1: status = SmOffsetExecutive::ShellBrep(ctx,source,p.distance,p.extend,p.merge,p.solid, selectors,result,transferred); break; case 2: status = executive->DoSolidOffset(result,p.skip); break; case 3: status = executive->DoExtendedOffset(result); break; case 4: status = SmApiShellBrepFull(source,p.distance,p.extend,p.merge,p.solid,api_selectors,result); break; case 5: status = SmApiOffsetBrepFull(source,p.distance,p.extend,p.merge,result); break; } SmBoolean boolean_after = ctx.GetDoingBoolean(); bool source_live = allocations.live(source_token), foreign_live = allocations.live(foreign_token); // Verify a non-null result belongs to a live observed allocation BEFORE any // dereference. Native errors remain built-case statuses even with a result. allocations.token(result); SmBrep* offset = executive ? executive->GetOffsetBrep() : nullptr; allocations.token(offset); int lifetime = !source ? 0 : !source_live ? 3 : result == source ? 2 : 1; int origin = !result ? 0 : source_live && result == source ? 2 : result == offset ? 3 : 1; // The explicit transfer report controls ownership interpretation, while the // observer covers actual deletion and also cleans allocations on exceptions. if (p.operation == 1 && transferred && source_live && result != source) throw std::runtime_error("native shell ownership report contradicts source lifetime"); out.insert(out.end(), {0,1,(double)p.operation,(double)status,transferred ? 1.0 : 0.0, (double)lifetime,(double)origin,boolean_before ? 1.0 : 0.0,boolean_after ? 1.0 : 0.0, executive ? 1.0 : 0.0}); if (executive) { out.insert(out.end(), {(double)executive->GetOffsetOperation(), executive->GetInset() ? 1.0 : 0.0,executive->GetExtendConvexEdges() ? 1.0 : 0.0, executive->merge() ? 1.0 : 0.0,executive->solid() ? 1.0 : 0.0, geometry.GetOffsetDistance(),geometry.GetThisApproxTol3d(),executive->GetTangencyTolDegrees()}); } out.push_back((double)selectors.GetSize()); out.insert(out.end(),selected_before.begin(),selected_before.end()); auto selected_after = observe_selectors(selectors,source_live ? source : nullptr, foreign_live ? foreign : nullptr,&selected_before); out.insert(out.end(),selected_after.begin(),selected_after.end()); put_block(out,source_before); put_block(out,snapshot(source_live ? source : nullptr)); put_block(out,foreign_before); put_block(out,snapshot(foreign_live ? foreign : nullptr)); put_block(out,snapshot(result)); put_block(out,snapshot(offset)); if (p.attributed >= 0) put_attributes(out, result); if (volume_query >= 0) { out.insert(out.end(), operand_state.begin(), operand_state.end()); out.push_back((double)operand_state.size()); out.insert(out.end(), pre_volume.begin(), pre_volume.end()); put_volume(out, result); } // Executive destruction owns offset; then allocation scope reclaims every // remaining distinct B-rep (source/foreign/result/unreturned native working data). executive.reset(); allocations.finish(); } } // namespace namespace { int32_t eval_impl(const double* input, int64_t input_len, int volume_query, double* out, int64_t cap, int64_t* out_len) { if (!input || input_len < 0 || input_len > max_input || !out_len || cap < 0 || (cap && !out) || volume_query < -1 || volume_query > 1) return SC_ERR_ARG; try { SC_NATIVE_LOCK; Reader rd{input,input_len}; Program p = read_program(rd); std::vector result; try { run(p,result,volume_query); } catch (const Rejected& rejected) { result = {-1,(double)rejected.status}; } if (result.size() > max_output) throw std::length_error("native output budget"); return sc_copy_out(result.data(),(int64_t)result.size(),out,cap,out_len); } catch (const Argument&) { return SC_ERR_ARG; } catch (...) { return SC_ERR_EXCEPTION; } } } // namespace SC_EXPORT int32_t sc_offset_offset_executive_p2_eval( const double* input, int64_t input_len, double* out, int64_t cap, int64_t* out_len) { return eval_impl(input, input_len, -1, out, cap, out_len); } // The same call with the volume queries of `run`'s `volume_query` (-1, 0 or 1). SC_EXPORT int32_t sc_offset_offset_executive_p2_eval_measured( const double* input, int64_t input_len, int32_t volume_query, double* out, int64_t cap, int64_t* out_len) { return eval_impl(input, input_len, volume_query, out, cap, out_len); }