feat(sdf): S11 — SDF 隐式建模完整模块
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 35s

S11-A: SDF 图元库
- 14 种基础形状(inline header)
- sphere/box/round_box/torus/capsule/cylinder/cone/plane/ellipsoid
- triangular_prism/hex_prism/link/wedge
- 2D 挤出(extrusion/extrusion_bounded/revolution)

S11-B: SDF 操作 + 域变形
- 锐利布尔: union/intersection/difference
- 平滑布尔: smooth_union/smooth_intersection/smooth_difference
- 修饰器: round/onion
- 域变形: repeat/mirror/rotate/translate/scale/twist/bend/elongate/displace/cheap_bend
- 24 项测试

S11-C: CSG 表达式树 + 网格转换 + Python 绑定
- SdfNode 树结构 — 工厂构造函数,递归求值
- sdf_to_mesh — 基于 marching_cubes 的 SDF→网格
- bind_sdf — pybind11 Python 绑定

文件: 14 文件,2,545 行(测试 1,296 行)
This commit is contained in:
茂之钳
2026-07-24 07:04:55 +00:00
parent 6d3f8fc8b3
commit b7419a7881
17 changed files with 1997 additions and 115 deletions
+14
View File
@@ -179,6 +179,20 @@ target_link_libraries(vde_sketch
PUBLIC vde_core vde_compile_options
)
# ── sdf ────────────────────────────────────────────
add_library(vde_sdf STATIC
sdf/sdf_primitives.cpp
sdf/sdf_tree.cpp
sdf/sdf_to_mesh.cpp
)
target_include_directories(vde_sdf
PUBLIC ${CMAKE_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(vde_sdf
PUBLIC vde_core vde_mesh vde_compile_options
)
# ── C API ──────────────────────────────────────────
add_library(vde_capi STATIC
capi/vde_capi.cpp
+130
View File
@@ -0,0 +1,130 @@
#include "vde/sdf/sdf_primitives.h"
#include <cmath>
#include <algorithm>
namespace vde::sdf {
// ── Helper: 2D triangle SDF (Inigo Quilez) ──────
namespace {
struct Vec2 { double x, y; };
inline double dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }
inline double cross2(Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }
inline Vec2 sub(Vec2 a, Vec2 b) { return {a.x - b.x, a.y - b.y}; }
inline Vec2 madd(Vec2 a, Vec2 b, double t) { return {a.x - b.x * t, a.y - b.y * t}; }
inline double len2(Vec2 a) { return a.x * a.x + a.y * a.y; }
[[nodiscard]] double triangle_sdf_2d(Vec2 p, Vec2 a, Vec2 b, Vec2 c) {
Vec2 e0 = sub(b, a);
Vec2 e1 = sub(c, b);
Vec2 e2 = sub(a, c);
Vec2 v0 = sub(p, a);
Vec2 v1 = sub(p, b);
Vec2 v2 = sub(p, c);
double d0 = dot(e0, e0);
double d1 = dot(e1, e1);
double d2 = dot(e2, e2);
Vec2 pq0 = madd(v0, e0, std::clamp(dot(v0, e0) / d0, 0.0, 1.0));
Vec2 pq1 = madd(v1, e1, std::clamp(dot(v1, e1) / d1, 0.0, 1.0));
Vec2 pq2 = madd(v2, e2, std::clamp(dot(v2, e2) / d2, 0.0, 1.0));
double s = cross2(e0, e2) > 0.0 ? 1.0 : -1.0;
double dx0 = len2(pq0);
double dx1 = len2(pq1);
double dx2 = len2(pq2);
double sy0 = s * cross2(v0, e0);
double sy1 = s * cross2(v1, e1);
double sy2 = s * cross2(v2, e2);
// Find minimum (d², signed) pair
double best_d2 = dx0;
double best_sd = sy0;
if (dx1 < best_d2) { best_d2 = dx1; best_sd = sy1; }
if (dx2 < best_d2) { best_d2 = dx2; best_sd = sy2; }
if (sy1 < best_sd) { best_sd = sy1; best_d2 = dx1; }
if (sy2 < best_sd) { best_sd = sy2; best_d2 = dx2; }
return -std::sqrt(best_d2) * (best_sd > 0.0 ? 1.0 : -1.0);
}
} // anonymous namespace
// ═══ cone ════════════════════════════════════════
double cone(const Point3D& p, double angle_rad, double height) {
double r_base = height * std::tan(angle_rad);
double h2 = height * 0.5;
// Shift so base is at y = 0, apex at y = height
double qx = std::sqrt(p.x() * p.x() + p.z() * p.z());
double qy = p.y() + h2;
double b = r_base / height; // slope: (r1 - r2) / h
if (b > 1.0) b = 1.0; // clamp for wide cones (>45°)
double a = std::sqrt(1.0 - b * b);
// Projection parameter: k = dot(q, vec2(-b, a))
double k = a * qy - b * qx;
if (k < 0.0) {
// Closest to base circle
return std::sqrt(qx * qx + qy * qy) - r_base;
}
if (k > a * height) {
// Closest to apex
return std::sqrt((qx - r_base) * (qx - r_base) + (qy - height) * (qy - height));
}
// Closest to side surface
return qx * a + qy * b - r_base;
}
// ═══ triangular_prism ═══════════════════════════
double triangular_prism(const Point3D& p, const Point3D& a, const Point3D& b,
const Point3D& c, double height) {
// Project triangle to XY plane
Vec2 p2{p.x(), p.y()};
Vec2 a2{a.x(), a.y()};
Vec2 b2{b.x(), b.y()};
Vec2 c2{c.x(), c.y()};
double d_xy = triangle_sdf_2d(p2, a2, b2, c2);
return std::max(d_xy, std::abs(p.z()) - height * 0.5);
}
// ═══ link ═══════════════════════════════════════
double link(const Point3D& p, double length, double major_r, double minor_r) {
// Two parallel toruses offset along X
double half_len = length * 0.5;
// Torus at +half_len
double px1 = p.x() - half_len;
double qx1 = std::sqrt(px1 * px1 + p.z() * p.z()) - major_r;
double d1 = std::sqrt(qx1 * qx1 + p.y() * p.y()) - minor_r;
// Torus at -half_len
double px2 = p.x() + half_len;
double qx2 = std::sqrt(px2 * px2 + p.z() * p.z()) - major_r;
double d2 = std::sqrt(qx2 * qx2 + p.y() * p.y()) - minor_r;
return std::min(d1, d2);
}
// ═══ infinite_cone ══════════════════════════════
double infinite_cone(const Point3D& p, const Point3D& apex,
const Vector3D& axis, double angle_rad) {
Vector3D d = p - apex;
double proj = d.dot(axis); // projection onto axis
double perp = (d - axis * proj).norm(); // perpendicular distance
return perp * std::cos(angle_rad) - proj * std::sin(angle_rad);
}
} // namespace vde::sdf
+23
View File
@@ -0,0 +1,23 @@
#include "vde/sdf/sdf_to_mesh.h"
namespace vde::sdf {
mesh::MCMesh sdf_to_mesh(const SdfNodePtr& root, int resolution, double iso_level) {
SdfBBox bbox = estimate_bbox(root, 1.0);
auto sdf_fn = [&root](double x, double y, double z) -> double {
return evaluate(root, Point3D(x, y, z));
};
return mesh::marching_cubes(sdf_fn, iso_level, bbox.min, bbox.max, resolution);
}
mesh::MCMesh sdf_to_mesh_lambda(
const std::function<double(double, double, double)>& f,
const Point3D& bmin, const Point3D& bmax,
int resolution, double iso_level) {
return mesh::marching_cubes(f, iso_level, bmin, bmax, resolution);
}
} // namespace vde::sdf
+377
View File
@@ -0,0 +1,377 @@
#include "vde/sdf/sdf_tree.h"
#include <cmath>
#include <stdexcept>
namespace vde::sdf {
// ── Helper: make a node ──────────────────────────────────────────
static SdfNodePtr make(SdfOp op) { return std::make_shared<SdfNode>(op); }
static void set_one_child(SdfNodePtr& n, SdfNodePtr c) { n->children = {std::move(c)}; }
static void set_two_children(SdfNodePtr& n, SdfNodePtr a, SdfNodePtr b) {
n->children = {std::move(a), std::move(b)};
}
// ── Primitive factories ──────────────────────────────────────────
SdfNodePtr SdfNode::sphere(double r) {
auto n = make(SdfOp::Sphere);
n->params.radius = r;
n->name = "sphere";
return n;
}
SdfNodePtr SdfNode::box(const Point3D& half_extents) {
auto n = make(SdfOp::Box);
n->params.extents = half_extents;
n->name = "box";
return n;
}
SdfNodePtr SdfNode::round_box(const Point3D& half_extents, double r) {
auto n = make(SdfOp::RoundBox);
n->params.extents = half_extents;
n->params.radius = r;
n->name = "round_box";
return n;
}
SdfNodePtr SdfNode::cylinder(double r, double h) {
auto n = make(SdfOp::Cylinder);
n->params.radius = r;
n->params.height = h; // full height
n->name = "cylinder";
return n;
}
SdfNodePtr SdfNode::torus(double major_r, double minor_r) {
auto n = make(SdfOp::Torus);
n->params.major_radius = major_r;
n->params.minor_radius = minor_r;
n->name = "torus";
return n;
}
SdfNodePtr SdfNode::capsule(const Point3D& a, const Point3D& b, double r) {
auto n = make(SdfOp::Capsule);
n->params.pt_a = a;
n->params.pt_b = b;
n->params.radius = r;
n->name = "capsule";
return n;
}
SdfNodePtr SdfNode::cone(double angle_rad, double h) {
auto n = make(SdfOp::Cone);
n->params.angle_rad = angle_rad;
n->params.height = h; // full height
n->name = "cone";
return n;
}
SdfNodePtr SdfNode::plane(const Vector3D& normal, double offset) {
auto n = make(SdfOp::Plane);
n->params.normal = normal;
n->params.offset = offset;
n->name = "plane";
return n;
}
SdfNodePtr SdfNode::ellipsoid(const Point3D& radii) {
auto n = make(SdfOp::Ellipsoid);
n->params.extents = radii;
n->name = "ellipsoid";
return n;
}
SdfNodePtr SdfNode::triangular_prism(double h) {
auto n = make(SdfOp::TriangularPrism);
n->params.height = h; // full height
n->name = "triangular_prism";
return n;
}
SdfNodePtr SdfNode::hex_prism(double h) {
auto n = make(SdfOp::HexPrism);
n->params.radius = h; // circumradius
n->params.height = h; // full height (= radius for equilateral)
n->name = "hex_prism";
return n;
}
SdfNodePtr SdfNode::link(double r, double length, double thickness) {
auto n = make(SdfOp::Link);
n->params.radius = r; // major radius
n->params.height = length; // spacing
n->params.thickness = thickness; // minor radius
n->name = "link";
return n;
}
SdfNodePtr SdfNode::wedge(const Point3D& extents) {
auto n = make(SdfOp::Wedge);
n->params.extents = extents;
n->name = "wedge";
return n;
}
// ── Boolean CSG factories ────────────────────────────────────────
SdfNodePtr SdfNode::op_union(SdfNodePtr a, SdfNodePtr b) {
auto n = make(SdfOp::Union); n->name="union";
set_two_children(n, std::move(a), std::move(b)); return n;
}
SdfNodePtr SdfNode::op_intersection(SdfNodePtr a, SdfNodePtr b) {
auto n = make(SdfOp::Intersection); n->name="intersection";
set_two_children(n, std::move(a), std::move(b)); return n;
}
SdfNodePtr SdfNode::op_difference(SdfNodePtr a, SdfNodePtr b) {
auto n = make(SdfOp::Difference); n->name="difference";
set_two_children(n, std::move(a), std::move(b)); return n;
}
SdfNodePtr SdfNode::smooth_union(SdfNodePtr a, SdfNodePtr b, double k) {
auto n = make(SdfOp::SmoothUnion); n->params.blend_k = k; n->name="smooth_union";
set_two_children(n, std::move(a), std::move(b)); return n;
}
SdfNodePtr SdfNode::smooth_intersection(SdfNodePtr a, SdfNodePtr b, double k) {
auto n = make(SdfOp::SmoothIntersection); n->params.blend_k = k; n->name="smooth_intersection";
set_two_children(n, std::move(a), std::move(b)); return n;
}
SdfNodePtr SdfNode::smooth_difference(SdfNodePtr a, SdfNodePtr b, double k) {
auto n = make(SdfOp::SmoothDifference); n->params.blend_k = k; n->name="smooth_difference";
set_two_children(n, std::move(a), std::move(b)); return n;
}
// ── Modifier factories ───────────────────────────────────────────
SdfNodePtr SdfNode::round(SdfNodePtr child, double r) {
auto n = make(SdfOp::Round); n->params.radius = r; n->name="round";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::onion(SdfNodePtr child, double thickness) {
auto n = make(SdfOp::Onion); n->params.thickness = thickness; n->name="onion";
set_one_child(n, std::move(child)); return n;
}
// ── Domain transform factories ───────────────────────────────────
SdfNodePtr SdfNode::repeat(SdfNodePtr child, const Point3D& cell) {
auto n = make(SdfOp::Repeat); n->params.repeat_cell = cell; n->name="repeat";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::mirror_x(SdfNodePtr child) {
auto n = make(SdfOp::MirrorX); n->name="mirror_x";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::mirror_y(SdfNodePtr child) {
auto n = make(SdfOp::MirrorY); n->name="mirror_y";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::mirror_z(SdfNodePtr child) {
auto n = make(SdfOp::MirrorZ); n->name="mirror_z";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::translate(SdfNodePtr child, const Point3D& offset) {
auto n = make(SdfOp::Translate); n->params.translate_offset = offset; n->name="translate";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::rotate(SdfNodePtr child, double angle_rad) {
auto n = make(SdfOp::Rotate); n->params.angle_rad = angle_rad; n->name="rotate";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::scale(SdfNodePtr child, const Point3D& factors) {
auto n = make(SdfOp::Scale); n->params.scale_factors = factors; n->name="scale";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::twist(SdfNodePtr child, double amount) {
auto n = make(SdfOp::Twist); n->params.amount = amount; n->name="twist";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::bend(SdfNodePtr child, double amount) {
auto n = make(SdfOp::Bend); n->params.amount = amount; n->name="bend";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::elongate(SdfNodePtr child, const Point3D& h) {
auto n = make(SdfOp::Elongate); n->params.extents = h; n->name="elongate";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::cheap_bend(SdfNodePtr child, double amount) {
auto n = make(SdfOp::CheapBend); n->params.amount = amount; n->name="cheap_bend";
set_one_child(n, std::move(child)); return n;
}
SdfNodePtr SdfNode::displace(SdfNodePtr child, double amplitude, double frequency) {
auto n = make(SdfOp::Displace);
n->params.amplitude = amplitude; n->params.frequency = frequency; n->name="displace";
set_one_child(n, std::move(child)); return n;
}
// ── Recursive evaluator ──────────────────────────────────────────
double evaluate(const SdfNodePtr& node, const Point3D& p) {
if (!node) return 1e30;
const auto& pr = node->params;
switch (node->op) {
// ── Primitives (from sdf_primitives.h) ──
case SdfOp::Sphere:
return sphere(p, pr.radius);
case SdfOp::Box:
return box(p, pr.extents);
case SdfOp::RoundBox:
return round_box(p, pr.extents, pr.radius);
case SdfOp::Torus:
return torus(p, pr.major_radius, pr.minor_radius);
case SdfOp::Capsule:
return capsule(p, pr.pt_a, pr.pt_b, pr.radius);
case SdfOp::Cylinder:
return cylinder(p, pr.radius, pr.height);
case SdfOp::Cone:
return cone(p, pr.angle_rad, pr.height);
case SdfOp::Plane:
return plane(p, pr.normal, pr.offset);
case SdfOp::Ellipsoid:
return ellipsoid(p, pr.extents);
case SdfOp::TriangularPrism: {
// Equilateral triangle in XZ plane
double rh = pr.height * 0.57735;
Point3D a(-rh, 0, -rh * 0.5), b(rh, 0, -rh * 0.5), c(0, 0, rh);
return triangular_prism(p, a, b, c, pr.height);
}
case SdfOp::HexPrism:
return hex_prism(p, pr.radius, pr.height);
case SdfOp::Link:
return link(p, pr.height, pr.radius, pr.thickness);
case SdfOp::Wedge: {
const auto& e = pr.extents;
return wedge(p, e.x() * 2.0, e.y() * 2.0, e.z() * 2.0);
}
// ── Boolean CSG ──
case SdfOp::Union:
return op_union(evaluate(node->children[0], p),
evaluate(node->children[1], p));
case SdfOp::Intersection:
return op_intersection(evaluate(node->children[0], p),
evaluate(node->children[1], p));
case SdfOp::Difference:
return op_difference(evaluate(node->children[0], p),
evaluate(node->children[1], p));
case SdfOp::SmoothUnion:
return op_smooth_union(evaluate(node->children[0], p),
evaluate(node->children[1], p), pr.blend_k);
case SdfOp::SmoothIntersection:
return op_smooth_intersection(evaluate(node->children[0], p),
evaluate(node->children[1], p), pr.blend_k);
case SdfOp::SmoothDifference:
return op_smooth_difference(evaluate(node->children[0], p),
evaluate(node->children[1], p), pr.blend_k);
// ── Modifiers (distance post-process) ──
case SdfOp::Round:
return op_round(evaluate(node->children[0], p), pr.radius);
case SdfOp::Onion:
return op_onion(evaluate(node->children[0], p), pr.thickness);
// ── Domain transforms (point pre-process) ──
case SdfOp::Repeat:
return evaluate(node->children[0], op_repeat(p, pr.repeat_cell));
case SdfOp::MirrorX:
return evaluate(node->children[0], op_mirror_x(p));
case SdfOp::MirrorY:
return evaluate(node->children[0], op_mirror_y(p));
case SdfOp::MirrorZ:
return evaluate(node->children[0], op_mirror_z(p));
case SdfOp::Translate:
return evaluate(node->children[0], op_translate(p, pr.translate_offset));
case SdfOp::Rotate:
return evaluate(node->children[0], op_rotate(p, pr.angle_rad));
case SdfOp::Scale:
return evaluate(node->children[0], op_scale(p, pr.scale_factors));
case SdfOp::Twist:
return evaluate(node->children[0], op_twist(p, pr.amount));
case SdfOp::Bend:
return evaluate(node->children[0], op_bend(p, pr.amount));
case SdfOp::Elongate:
return evaluate(node->children[0], op_elongate(p, pr.extents));
case SdfOp::CheapBend:
return evaluate(node->children[0], op_cheap_bend(p, pr.amount));
// ── Displacement ──
case SdfOp::Displace:
return op_displace(evaluate(node->children[0], p), p,
pr.amplitude, pr.frequency);
}
return 1e30;
}
// ── Bounds estimation ────────────────────────────────────────────
static double estimate_radius(const SdfNodePtr& node) {
if (!node) return 1.0;
switch (node->op) {
case SdfOp::Sphere:
return node->params.radius;
case SdfOp::Box:
return node->params.extents.norm();
case SdfOp::RoundBox:
return node->params.extents.norm() + node->params.radius;
case SdfOp::Torus:
return node->params.major_radius + node->params.minor_radius;
case SdfOp::Capsule:
return (node->params.pt_b - node->params.pt_a).norm() * 0.5 + node->params.radius;
case SdfOp::Cylinder:
return std::sqrt(node->params.radius * node->params.radius +
node->params.height * node->params.height * 0.25);
case SdfOp::Cone:
return node->params.height * 0.5 + node->params.height * 0.5 * std::tan(node->params.angle_rad);
case SdfOp::Plane:
return 10.0;
case SdfOp::Ellipsoid:
return std::max({node->params.extents.x(),
node->params.extents.y(),
node->params.extents.z()});
case SdfOp::TriangularPrism:
case SdfOp::HexPrism:
return node->params.height;
case SdfOp::Link:
return node->params.radius + node->params.height * 0.5 + node->params.thickness;
case SdfOp::Wedge:
return node->params.extents.norm();
case SdfOp::Union:
case SdfOp::Intersection:
case SdfOp::SmoothUnion:
case SdfOp::SmoothIntersection:
return std::max(estimate_radius(node->children[0]),
estimate_radius(node->children[1]));
case SdfOp::Difference:
case SdfOp::SmoothDifference:
return estimate_radius(node->children[0]);
case SdfOp::Round:
return estimate_radius(node->children[0]) + node->params.radius;
case SdfOp::Onion:
return estimate_radius(node->children[0]) + node->params.thickness;
case SdfOp::Translate:
return estimate_radius(node->children[0]) + node->params.translate_offset.norm();
case SdfOp::Scale:
return estimate_radius(node->children[0]) *
std::max({node->params.scale_factors.x(),
node->params.scale_factors.y(),
node->params.scale_factors.z()});
default:
return node->children.empty() ? 1.0 : estimate_radius(node->children[0]);
}
}
Point3D estimate_bounds(const SdfNodePtr& root, double margin) {
double r = estimate_radius(root) + margin;
return Point3D(r, r, r);
}
SdfBBox estimate_bbox(const SdfNodePtr& root, double margin) {
Point3D half = estimate_bounds(root, margin);
return SdfBBox{-half, half};
}
} // namespace vde::sdf