- n3xd_ocp.sample.face_grid: a face's UV grid of points and outward normals in one GIL-free call, sampling exactly where np.linspace does so a caller's fitted surface does not move. - n3xd_ocp.helix: OCCT 8.0's TKHelix, which upstream does not bind at all. Takes Python lists rather than NCollection_Array1. Two things the header does not say, both found by probing: SetParameters wants N+1 diameters for N segments (one per boundary, so a taper interpolates), and the builder is right-hand only -- a negative pitch is error status 12, not a mirrored helix. - Bind BRepPrimAPI_MakeSphere and give inventory.py an EXTRA_SYMBOLS addendum for symbols no app source imports. assay's gen_flow_fixtures has been unrunnable since the 10C cutover for want of it; the gap was wider than --check, since sigdiff is inventory-driven too. Gates: 105 tests, 139/139 symbols, sigdiff clean, ASAN clean, wheel self-contained with no libGL/libX11 DT_NEEDED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbTQ2HYWQwdtGmGJwypt6Z
123 lines
4.9 KiB
C++
123 lines
4.9 KiB
C++
/*
|
||
n3xd_ocp.sample — a face's UV grid, without the per-sample round trip.
|
||
|
||
cad/operations/surface_pattern.py walks an (res+1)² UV grid in nested
|
||
Python loops: one BRepAdaptor_Surface::D1 plus roughly ten accessor calls
|
||
per sample, and the cross product in Python. The kernel can fill the whole
|
||
grid in one call with the GIL released.
|
||
|
||
The output reproduces what the Python loop produces:
|
||
|
||
* the grid is BRepTools::UVBounds, sampled the way numpy's linspace
|
||
samples it — start + i*step with the endpoint forced exactly, so the
|
||
sample locations are bit-identical and the fitted surface does not
|
||
move. The bounds come back with the arrays for the same reason: the
|
||
caller derives its normalised u/v from these numbers rather than
|
||
recomputing them.
|
||
* normals are du × dv normalised, negated for a reversed face.
|
||
* a sample whose cross product is degenerate (a pole or apex) gets the
|
||
zero vector, not a unit vector in an arbitrary direction. Callers
|
||
test it — `_inward_thickness` skips a sample on `nrm.any()`.
|
||
*/
|
||
|
||
#include "../common/occt_module.h"
|
||
|
||
#include <nanobind/ndarray.h>
|
||
|
||
#include <BRepAdaptor_Surface.hxx>
|
||
#include <BRepTools.hxx>
|
||
#include <TopAbs_Orientation.hxx>
|
||
#include <TopoDS_Face.hxx>
|
||
#include <gp_Pnt.hxx>
|
||
#include <gp_Vec.hxx>
|
||
|
||
#include <cmath>
|
||
#include <stdexcept>
|
||
|
||
namespace {
|
||
|
||
template <typename T> nb::capsule owner_of(T *data) {
|
||
return nb::capsule(data, [](void *p) noexcept { delete[] (T *) p; });
|
||
}
|
||
|
||
/// numpy's linspace endpoint handling: the last sample is the bound itself,
|
||
/// not start + (n-1)*step, which can miss it by an ulp.
|
||
inline double linspace_at(double lo, double hi, int i, int n) {
|
||
if (n <= 1)
|
||
return lo;
|
||
return i == n - 1 ? hi : lo + (double) i * (hi - lo) / (double) (n - 1);
|
||
}
|
||
|
||
constexpr double MIN_NORMAL = 1e-12;
|
||
|
||
} // namespace
|
||
|
||
void register_ext_sample() {
|
||
nb::module_ m = ocp_named_module("n3xd_ocp.sample");
|
||
|
||
m.def(
|
||
"face_grid",
|
||
[](const TopoDS_Face &face, int n) {
|
||
if (n < 1)
|
||
throw std::invalid_argument("face_grid: n must be >= 1");
|
||
|
||
double umin = 0.0, umax = 0.0, vmin = 0.0, vmax = 0.0;
|
||
BRepTools::UVBounds(face, umin, umax, vmin, vmax);
|
||
const double sign =
|
||
face.Orientation() == TopAbs_REVERSED ? -1.0 : 1.0;
|
||
|
||
const size_t count = (size_t) n * (size_t) n;
|
||
double *points = new double[3 * count];
|
||
double *normals = new double[3 * count];
|
||
|
||
{
|
||
nb::gil_scoped_release nogil;
|
||
BRepAdaptor_Surface adaptor(face);
|
||
gp_Pnt point;
|
||
gp_Vec du, dv;
|
||
for (int i = 0; i < n; ++i) {
|
||
const double u = linspace_at(umin, umax, i, n);
|
||
for (int j = 0; j < n; ++j) {
|
||
const double v = linspace_at(vmin, vmax, j, n);
|
||
adaptor.D1(u, v, point, du, dv);
|
||
|
||
const size_t at = 3 * ((size_t) i * (size_t) n + j);
|
||
points[at + 0] = point.X();
|
||
points[at + 1] = point.Y();
|
||
points[at + 2] = point.Z();
|
||
|
||
const double nx = du.Y() * dv.Z() - du.Z() * dv.Y();
|
||
const double ny = du.Z() * dv.X() - du.X() * dv.Z();
|
||
const double nz = du.X() * dv.Y() - du.Y() * dv.X();
|
||
const double mag = std::sqrt(nx * nx + ny * ny + nz * nz);
|
||
if (mag > MIN_NORMAL) {
|
||
normals[at + 0] = sign * nx / mag;
|
||
normals[at + 1] = sign * ny / mag;
|
||
normals[at + 2] = sign * nz / mag;
|
||
} else {
|
||
normals[at + 0] = 0.0;
|
||
normals[at + 1] = 0.0;
|
||
normals[at + 2] = 0.0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
size_t shape[3] = {(size_t) n, (size_t) n, 3};
|
||
return nb::make_tuple(
|
||
nb::ndarray<nb::numpy, double, nb::ndim<3>>(points, 3, shape,
|
||
owner_of(points)),
|
||
nb::ndarray<nb::numpy, double, nb::ndim<3>>(normals, 3, shape,
|
||
owner_of(normals)),
|
||
nb::make_tuple(umin, umax, vmin, vmax));
|
||
},
|
||
"face"_a, "n"_a,
|
||
R"doc(Sample *face* on an n x n grid over its UV bounds.
|
||
|
||
Returns ``(points[n, n, 3], normals[n, n, 3], (umin, umax, vmin, vmax))`` as
|
||
float64 arrays, u along axis 0. Normals are outward (negated for a reversed
|
||
face) and unit length, or the zero vector where the surface derivatives are
|
||
degenerate. Sample locations match ``numpy.linspace`` over the returned
|
||
bounds exactly.)doc");
|
||
}
|