Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 138 additions & 8 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
return evalFor(n, xform);
else if constexpr (std::is_same_v<T, ModuleCallNode>)
return evalModuleCall(n, xform);
else if constexpr (std::is_same_v<T, ExtrusionNode>)
return evalExtrusion(n, xform);
return nullptr;
}, node);
}
Expand All @@ -68,18 +70,107 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalPrimitive(const PrimitiveNode& p, const glm::mat4& xform) {
CsgLeaf leaf;
leaf.center = p.center;
leaf.transform = xform;

switch (p.kind) {
case PrimitiveNode::Kind::Cube: leaf.kind = CsgLeaf::Kind::Cube; break;
case PrimitiveNode::Kind::Sphere: leaf.kind = CsgLeaf::Kind::Sphere; break;
case PrimitiveNode::Kind::Cylinder: leaf.kind = CsgLeaf::Kind::Cylinder; break;
// ---- 3-D primitives: resolve all params as scalars --------------------
case PrimitiveNode::Kind::Cube:
leaf.kind = CsgLeaf::Kind::Cube;
for (const auto& [name, exprPtr] : p.params)
leaf.params[name] = m_interp->evalNumber(*exprPtr);
break;

case PrimitiveNode::Kind::Sphere:
leaf.kind = CsgLeaf::Kind::Sphere;
for (const auto& [name, exprPtr] : p.params)
leaf.params[name] = m_interp->evalNumber(*exprPtr);
break;

case PrimitiveNode::Kind::Cylinder:
leaf.kind = CsgLeaf::Kind::Cylinder;
for (const auto& [name, exprPtr] : p.params)
leaf.params[name] = m_interp->evalNumber(*exprPtr);
break;

// ---- square([w,h]) / square(s) / square(size=[w,h]) ------------------
case PrimitiveNode::Kind::Square2D: {
leaf.kind = CsgLeaf::Kind::Square2D;
// Resolve scalar params ($fn, etc.) — skip "size" which may be a vector
for (const auto& [name, exprPtr] : p.params) {
if (name == "size") continue;
leaf.params[name] = m_interp->evalNumber(*exprPtr);
}
// "size" overrides x/y if present
if (p.params.count("size")) {
Value sv = m_interp->evaluate(*p.params.at("size"));
if (sv.isNumber()) {
leaf.params["sx"] = sv.asNumber();
leaf.params["sy"] = sv.asNumber();
} else if (sv.isVector() && sv.asVec().size() >= 2) {
leaf.params["sx"] = sv.asVec()[0].asNumber();
leaf.params["sy"] = sv.asVec()[1].asNumber();
}
}
// If sx/sy not set yet, fall back to positional vector "x","y" or scalar "_pos0"
if (!leaf.params.count("sx")) {
double sx = leaf.params.count("x") ? leaf.params["x"] : 1.0;
double sy = leaf.params.count("y") ? leaf.params["y"] : 1.0;
if (leaf.params.count("_pos0")) {
double s = leaf.params["_pos0"];
sx = sy = s;
}
leaf.params["sx"] = sx;
leaf.params["sy"] = sy;
}
break;
}

// Resolve every expression param to a concrete double
for (const auto& [name, exprPtr] : p.params)
leaf.params[name] = m_interp->evalNumber(*exprPtr);
// ---- circle(r) / circle(d) --------------------------------------------
case PrimitiveNode::Kind::Circle2D:
leaf.kind = CsgLeaf::Kind::Circle2D;
for (const auto& [name, exprPtr] : p.params)
leaf.params[name] = m_interp->evalNumber(*exprPtr);
// diameter → radius
if (!leaf.params.count("r") && leaf.params.count("d"))
leaf.params["r"] = leaf.params["d"] * 0.5;
break;

// ---- polygon(points=[[x,y],...], paths=[[i,j,...],...]) ---------------
case PrimitiveNode::Kind::Polygon2D: {
leaf.kind = CsgLeaf::Kind::Polygon2D;
if (p.params.count("points")) {
Value pts = m_interp->evaluate(*p.params.at("points"));
if (pts.isVector()) {
leaf.polyPoints.reserve(pts.asVec().size());
for (const auto& pt : pts.asVec()) {
if (pt.isVector() && pt.asVec().size() >= 2) {
leaf.polyPoints.push_back({
static_cast<float>(pt.asVec()[0].asNumber()),
static_cast<float>(pt.asVec()[1].asNumber())
});
}
}
}
}
if (p.params.count("paths")) {
Value paths = m_interp->evaluate(*p.params.at("paths"));
if (paths.isVector()) {
for (const auto& path : paths.asVec()) {
if (path.isVector()) {
std::vector<int> indices;
indices.reserve(path.asVec().size());
for (const auto& idx : path.asVec())
indices.push_back(static_cast<int>(idx.asNumber()));
leaf.polyPaths.push_back(std::move(indices));
}
}
}
}
break;
}
}

leaf.center = p.center;
leaf.transform = xform;
return makeLeaf(std::move(leaf));
}

Expand Down Expand Up @@ -319,4 +410,43 @@ CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::m
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// Extrusion — build a CsgExtrusion from an ExtrusionNode
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalExtrusion(const ExtrusionNode& e, const glm::mat4& xform) {
CsgExtrusion ext;
ext.kind = (e.kind == ExtrusionNode::Kind::Linear) ? CsgExtrusion::Kind::Linear
: CsgExtrusion::Kind::Rotate;
ext.transform = xform;

// Resolve numeric params; treat "scale" and "center" specially
for (const auto& [name, exprPtr] : e.params) {
if (name == "scale") {
Value sv = m_interp->evaluate(*exprPtr);
if (sv.isNumber()) {
ext.params["scale_x"] = sv.asNumber();
ext.params["scale_y"] = sv.asNumber();
} else if (sv.isVector() && sv.asVec().size() >= 2) {
ext.params["scale_x"] = sv.asVec()[0].asNumber();
ext.params["scale_y"] = sv.asVec()[1].asNumber();
}
} else if (name == "center") {
Value cv = m_interp->evaluate(*exprPtr);
ext.params["center"] = bool(cv) ? 1.0 : 0.0;
} else {
ext.params[name] = m_interp->evalNumber(*exprPtr);
}
}

// Evaluate 2-D children in local space (identity xform)
// The outer xform is stored in ext.transform and applied to the final solid.
const glm::mat4 identity{1.0f};
for (const auto& child : e.children) {
if (auto c = evalNode(*child, identity))
ext.children.push_back(std::move(c));
}

return makeExtrusion(std::move(ext));
}

} // namespace chisel::csg
1 change: 1 addition & 0 deletions src/csg/CsgEvaluator.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class CsgEvaluator {
CsgNodePtr evalIf(const chisel::lang::IfNode& n, const glm::mat4& xform);
CsgNodePtr evalFor(const chisel::lang::ForNode& n, const glm::mat4& xform);
CsgNodePtr evalModuleCall(const chisel::lang::ModuleCallNode& n, const glm::mat4& xform);
CsgNodePtr evalExtrusion(const chisel::lang::ExtrusionNode& e, const glm::mat4& xform);

glm::mat4 makeMatrix(const chisel::lang::TransformNode& t) const;
};
Expand Down
33 changes: 29 additions & 4 deletions src/csg/CsgNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,36 @@
namespace chisel::csg {

// ---------------------------------------------------------------------------
// Forward declaration needed for the recursive variant
// Forward declarations needed for the recursive variant
// ---------------------------------------------------------------------------
struct CsgBoolean;
struct CsgExtrusion;

// ---------------------------------------------------------------------------
// CsgLeaf — a primitive with its fully-accumulated world transform.
// The transform encodes every translate/rotate/scale/mirror applied above
// this node in the AST; the MeshEvaluator applies it after tessellation.
// ---------------------------------------------------------------------------
struct CsgLeaf {
enum class Kind { Cube, Sphere, Cylinder };
enum class Kind { Cube, Sphere, Cylinder, Square2D, Circle2D, Polygon2D };

Kind kind = Kind::Cube;
// Named params carried forward from the parser ("r", "h", "r1", "r2",
// "$fn", "$fs", "$fa", "x"/"y"/"z" for cube size, "_pos0" for sphere(5)).
// "$fn", "$fs", "$fa", "x"/"y"/"z" for cube size, "_pos0" for sphere(5),
// "sx"/"sy" for square, etc.)
std::unordered_map<std::string, double> params;
bool center = false;
glm::mat4 transform{1.0f}; // accumulated model-to-world matrix

// Polygon2D only — resolved contour points and optional path indices
std::vector<glm::vec2> polyPoints;
std::vector<std::vector<int>> polyPaths;
};

// ---------------------------------------------------------------------------
// CsgNode — the CSG IR variant
// ---------------------------------------------------------------------------
using CsgNode = std::variant<CsgLeaf, CsgBoolean>;
using CsgNode = std::variant<CsgLeaf, CsgBoolean, CsgExtrusion>;
using CsgNodePtr = std::shared_ptr<CsgNode>;

struct CsgBoolean {
Expand All @@ -47,6 +53,22 @@ struct CsgBoolean {
glm::mat4 transform{1.0f};
};

// ---------------------------------------------------------------------------
// CsgExtrusion — linear_extrude / rotate_extrude in the CSG IR.
// Children are 2-D CsgLeafs (Square2D / Circle2D / Polygon2D) or CsgBooleans
// of 2-D leaves; MeshEvaluator converts them to CrossSections.
// ---------------------------------------------------------------------------
struct CsgExtrusion {
enum class Kind { Linear, Rotate };

Kind kind = Kind::Linear;
// Resolved numeric params: "height", "twist", "scale_x", "scale_y",
// "angle", "$fn", "center", "_pos0"
std::unordered_map<std::string, double> params;
std::vector<CsgNodePtr> children;
glm::mat4 transform{1.0f}; // outer 3-D transform applied to the final solid
};

// ---------------------------------------------------------------------------
// Factory helpers
// ---------------------------------------------------------------------------
Expand All @@ -56,6 +78,9 @@ inline CsgNodePtr makeLeaf(CsgLeaf leaf) {
inline CsgNodePtr makeBoolean(CsgBoolean b) {
return std::make_shared<CsgNode>(std::move(b));
}
inline CsgNodePtr makeExtrusion(CsgExtrusion e) {
return std::make_shared<CsgNode>(std::move(e));
}

// ---------------------------------------------------------------------------
// CsgScene — output of CsgEvaluator
Expand Down
115 changes: 111 additions & 4 deletions src/csg/MeshEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ static std::string leafKey(const CsgLeaf& leaf) {
k.reserve(256);

switch (leaf.kind) {
case CsgLeaf::Kind::Cube: k += "cube:"; break;
case CsgLeaf::Kind::Sphere: k += "sphere:"; break;
case CsgLeaf::Kind::Cylinder: k += "cylinder:"; break;
case CsgLeaf::Kind::Cube: k += "cube:"; break;
case CsgLeaf::Kind::Sphere: k += "sphere:"; break;
case CsgLeaf::Kind::Cylinder: k += "cylinder:"; break;
case CsgLeaf::Kind::Square2D: k += "square2d:"; break;
case CsgLeaf::Kind::Circle2D: k += "circle2d:"; break;
case CsgLeaf::Kind::Polygon2D: k += "polygon2d:"; break;
}

// Sort params for a stable key
Expand Down Expand Up @@ -87,8 +90,10 @@ manifold::Manifold MeshEvaluator::evalNode(const CsgNode& node, const PrimitiveG
using T = std::decay_t<decltype(n)>;
if constexpr (std::is_same_v<T, CsgLeaf>)
return evalLeaf(n, gen);
else
else if constexpr (std::is_same_v<T, CsgBoolean>)
return evalBoolean(n, gen);
else
return evalExtrusion(n, gen);
}, node);
}

Expand Down Expand Up @@ -137,4 +142,106 @@ manifold::Manifold MeshEvaluator::evalBoolean(const CsgBoolean& b, const Primiti
return result;
}

// ---------------------------------------------------------------------------
// getChildCrossSection — recursively build a CrossSection from a 2-D subtree.
// Handles CsgLeaf (2-D kinds) and CsgBoolean (union/difference/intersection
// of 2-D children). 3-D leaves produce an empty CrossSection.
// ---------------------------------------------------------------------------
manifold::CrossSection MeshEvaluator::getChildCrossSection(const CsgNode& node,
const PrimitiveGen& gen) {
return std::visit([&](const auto& n) -> manifold::CrossSection {
using T = std::decay_t<decltype(n)>;

if constexpr (std::is_same_v<T, CsgLeaf>) {
auto cs = gen.generateCrossSection(n);
// Apply the 2-D portion of the accumulated transform so that
// translate/rotate applied to 2-D children is respected.
if (n.transform != glm::mat4{1.0f}) {
manifold::mat2x3 m2;
m2[0][0] = n.transform[0][0]; m2[0][1] = n.transform[0][1];
m2[1][0] = n.transform[1][0]; m2[1][1] = n.transform[1][1];
m2[2][0] = n.transform[3][0]; m2[2][1] = n.transform[3][1];
cs = cs.Transform(m2);
}
return cs;
} else if constexpr (std::is_same_v<T, CsgBoolean>) {
if (n.children.empty()) return {};
auto result = getChildCrossSection(*n.children[0], gen);
for (std::size_t i = 1; i < n.children.size(); ++i) {
auto child = getChildCrossSection(*n.children[i], gen);
switch (n.op) {
case CsgBoolean::Op::Union: result = result + child; break;
case CsgBoolean::Op::Difference: result = result - child; break;
case CsgBoolean::Op::Intersection: result = result ^ child; break;
default: result = result + child; break; // hull/minkowski → union
}
}
return result;
} else {
return {}; // nested extrusion — not supported
}
}, node);
}

// ---------------------------------------------------------------------------
// evalExtrusion — convert a CsgExtrusion node to a 3-D Manifold
// ---------------------------------------------------------------------------
manifold::Manifold MeshEvaluator::evalExtrusion(const CsgExtrusion& e,
const PrimitiveGen& gen) {
// Build the unified 2-D cross-section from all children
manifold::CrossSection cs;
for (const auto& child : e.children)
cs = cs + getChildCrossSection(*child, gen);

if (cs.IsEmpty()) return {};

auto getP = [&](const std::string& k, double def) -> double {
auto it = e.params.find(k);
return (it != e.params.end()) ? it->second : def;
};

manifold::Manifold result;

if (e.kind == CsgExtrusion::Kind::Linear) {
double height = getP("height", getP("h", getP("_pos0", 1.0)));
double twist = -getP("twist", 0.0); // OpenSCAD uses left-hand rule; Manifold uses right-hand
float scaleX = static_cast<float>(getP("scale_x", 1.0));
float scaleY = static_cast<float>(getP("scale_y", 1.0));
double fnOvr = getP("$fn", 0.0);
// Divisions are only needed for twist (to smoothly interpolate the
// rotation). A plain scale taper works correctly with 0 divisions.
int nDivs = (twist != 0.0)
? std::max(1, static_cast<int>(fnOvr > 0 ? fnOvr : 10))
: 0;
bool center = (getP("center", 0.0) != 0.0);

result = manifold::Manifold::Extrude(
cs.ToPolygons(),
static_cast<float>(height),
nDivs,
static_cast<float>(twist),
{scaleX, scaleY});

if (center)
result = result.Translate({0.0f, 0.0f,
-static_cast<float>(height) * 0.5f});
} else {
// rotate_extrude
double angle = getP("angle", 360.0);
double fnOvr = getP("$fn", 0.0);
int segs = gen.resolveSegments(10.0, fnOvr); // 10 = proxy radius

result = manifold::Manifold::Revolve(
cs.ToPolygons(),
segs,
static_cast<float>(angle));
}

// Apply the outer 3-D world transform
if (e.transform != glm::mat4{1.0f})
result = result.Transform(toAffine(e.transform));

return result;
}

} // namespace chisel::csg
Loading
Loading