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
25 changes: 25 additions & 0 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
return evalBoolean(n, xform);
else if constexpr (std::is_same_v<T, TransformNode>)
return evalTransform(n, xform);
else if constexpr (std::is_same_v<T, IfNode>)
return evalIf(n, xform);
return nullptr;
}, node);
}
Expand Down Expand Up @@ -176,4 +178,27 @@ glm::mat4 CsgEvaluator::makeMatrix(const TransformNode& t) const {
return m;
}

// ---------------------------------------------------------------------------
// if/else — evaluate condition, walk the live branch
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalIf(const IfNode& node, const glm::mat4& xform) {
const bool cond = bool(m_interp->evaluate(*node.condition));
const auto& branch = cond ? node.thenChildren : node.elseChildren;

std::vector<CsgNodePtr> children;
children.reserve(branch.size());
for (const auto& child : branch) {
if (auto c = evalNode(*child, xform))
children.push_back(std::move(c));
}

if (children.empty()) return nullptr;
if (children.size() == 1) return children[0];

CsgBoolean u;
u.op = CsgBoolean::Op::Union;
u.children = std::move(children);
return makeBoolean(std::move(u));
}

} // 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 @@ -28,6 +28,7 @@ class CsgEvaluator {
CsgNodePtr evalPrimitive(const chisel::lang::PrimitiveNode& p, const glm::mat4& xform);
CsgNodePtr evalBoolean(const chisel::lang::BooleanNode& b, const glm::mat4& xform);
CsgNodePtr evalTransform(const chisel::lang::TransformNode& t, const glm::mat4& xform);
CsgNodePtr evalIf(const chisel::lang::IfNode& n, const glm::mat4& xform);

glm::mat4 makeMatrix(const chisel::lang::TransformNode& t) const;
};
Expand Down
17 changes: 16 additions & 1 deletion src/lang/AST.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ namespace chisel::lang {
struct PrimitiveNode;
struct BooleanNode;
struct TransformNode;
struct IfNode;

// ---------------------------------------------------------------------------
// AstNode — the top-level variant
// All nodes are heap-allocated via unique_ptr so the tree is
// easy to move/own and the variant stays small.
// ---------------------------------------------------------------------------
using AstNode = std::variant<PrimitiveNode, BooleanNode, TransformNode>;
using AstNode = std::variant<PrimitiveNode, BooleanNode, TransformNode, IfNode>;
using AstNodePtr = std::unique_ptr<AstNode>;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -75,6 +76,20 @@ inline AstNodePtr makeTransform(TransformNode n) {
return std::make_unique<AstNode>(std::move(n));
}

// ---------------------------------------------------------------------------
// IfNode — if (condition) { ... } else { ... }
// ---------------------------------------------------------------------------
struct IfNode {
ExprPtr condition;
std::vector<AstNodePtr> thenChildren;
std::vector<AstNodePtr> elseChildren; // empty when no else branch
SourceLoc loc;
};

inline AstNodePtr makeIf(IfNode n) {
return std::make_unique<AstNode>(std::move(n));
}

// ---------------------------------------------------------------------------
// AssignStmt — a variable assignment at file or block scope: x = expr;
// ---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions src/lang/Lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ static const std::unordered_map<std::string_view, TokenKind> kKeywords = {
{"rotate", TokenKind::Rotate},
{"scale", TokenKind::Scale},
{"mirror", TokenKind::Mirror},
{"if", TokenKind::If},
{"else", TokenKind::Else},
};

// ---------------------------------------------------------------------------
Expand Down
25 changes: 25 additions & 0 deletions src/lang/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ AstNodePtr Parser::parseNode() {
case TokenKind::Mirror:
return parseTransform(k);

case TokenKind::If:
return parseIf();

default:
return nullptr;
}
Expand Down Expand Up @@ -209,6 +212,28 @@ AstNodePtr Parser::parseTransform(TokenKind k) {
return makeTransform(std::move(node));
}

// ---------------------------------------------------------------------------
// if / else
// ---------------------------------------------------------------------------
AstNodePtr Parser::parseIf() {
const Token& kw = advance(); // consume 'if'
IfNode node;
node.loc = kw.loc;

expect(TokenKind::LParen, "expected '(' after 'if'");
node.condition = parseExpr();
expect(TokenKind::RParen, "expected ')' after condition");

node.thenChildren = parseBody();

if (check(TokenKind::Else)) {
advance(); // consume 'else'
node.elseChildren = parseBody();
}

return makeIf(std::move(node));
}

// ---------------------------------------------------------------------------
// parseVecExpr — parse a [x, y, z] literal into a VectorLit ExprPtr
// ---------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/lang/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Parser {
AstNodePtr parsePrimitive(TokenKind k);
AstNodePtr parseBoolean(TokenKind k);
AstNodePtr parseTransform(TokenKind k);
AstNodePtr parseIf();

// ---- expressions (Pratt parser) --------------------------------------
ExprPtr parseExpr(int minPrec = 0);
Expand Down
4 changes: 4 additions & 0 deletions src/lang/Token.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ enum class TokenKind : uint8_t {
Scale,
Mirror,

// Control flow
If, // if
Else, // else

// Arithmetic operators
Plus, // +
Minus, // -
Expand Down
42 changes: 42 additions & 0 deletions tests/if_else_test.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// if/else control flow test — V2 Tier 2b
// Each scene is a translate() block; comment out all but one to isolate.
$fn = 32;

// ─── Scene 1 (0, 0, 0): if true → sphere visible ─────────────────────────────
show_sphere = 1;
translate([0, 0, 0])
if (show_sphere) sphere(r = 6);

// ─── Scene 2 (20, 0, 0): if false → nothing rendered ─────────────────────────
translate([20, 0, 0])
if (0) sphere(r = 6);

// ─── Scene 3 (40, 0, 0): if/else — variable selects shape ────────────────────
use_cube = 0;
translate([40, 0, 0])
if (use_cube) cube([10, 10, 10], center = true);
else sphere(r = 6);

// ─── Scene 4 (0, -20, 0): expression condition ───────────────────────────────
r = 8;
translate([0, -20, 0])
if (r > 5) sphere(r = r);
else cube([r, r, r], center = true);

// ─── Scene 5 (20, -20, 0): chained if/else if/else ───────────────────────────
mode = 2;
translate([20, -20, 0])
if (mode == 1) cube([8, 8, 8], center = true);
else if (mode == 2) sphere(r = 5);
else cylinder(h = 10, r = 3, center = true);

// ─── Scene 6 (40, -20, 0): if wrapping a boolean op ─────────────────────────
hollow = 1;
translate([40, -20, 0])
if (hollow)
difference() {
cube([12, 12, 12], center = true);
sphere(r = 7);
}
else
cube([12, 12, 12], center = true);
52 changes: 52 additions & 0 deletions tests/test_csg_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,55 @@ TEST_CASE("CsgEval:minkowski stores outer transform", "[csg]") {
const auto& child = asLeaf(b.children[0]);
REQUIRE(child.transform[3][1] == Approx(0.0f)); // local space — no y offset
}

// ---------------------------------------------------------------------------
// if / else
// ---------------------------------------------------------------------------
TEST_CASE("CsgEval:if true yields then geometry", "[csg]") {
auto s = evaluate("if (1) sphere(r=5);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Sphere);
}

TEST_CASE("CsgEval:if false yields no geometry", "[csg]") {
auto s = evaluate("if (0) sphere(r=5);");
REQUIRE(s.roots.empty());
}

TEST_CASE("CsgEval:if false else yields else geometry", "[csg]") {
auto s = evaluate("if (0) sphere(r=5); else cube([3,3,3]);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Cube);
}

TEST_CASE("CsgEval:if true else skips else geometry", "[csg]") {
auto s = evaluate("if (1) sphere(r=5); else cube([3,3,3]);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Sphere);
}

TEST_CASE("CsgEval:if condition from expression", "[csg]") {
auto s = evaluate("if (3 > 2) sphere(r=4);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Sphere);
}

TEST_CASE("CsgEval:if condition from variable", "[csg]") {
auto s = evaluate("show = 1; if (show) cube([5,5,5]);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Cube);
}

TEST_CASE("CsgEval:if multiple then children wrapped in union", "[csg]") {
auto s = evaluate("if (1) { sphere(r=1); cube([2,2,2]); }");
REQUIRE(s.roots.size() == 1);
const auto& b = asBool(s.roots[0]);
REQUIRE(b.op == CsgBoolean::Op::Union);
REQUIRE(b.children.size() == 2);
}

TEST_CASE("CsgEval:if inherits outer transform", "[csg]") {
auto s = evaluate("translate([7,0,0]) if (1) sphere(r=1);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).transform[3][0] == Approx(7.0f));
}
5 changes: 5 additions & 0 deletions tests/test_lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ TEST_CASE("Lexer:hull and minkowski keywords", "[lexer]") {
REQUIRE(kinds("minkowski") == std::vector{TokenKind::Minkowski});
}

TEST_CASE("Lexer:if and else keywords", "[lexer]") {
REQUIRE(kinds("if") == std::vector{TokenKind::If});
REQUIRE(kinds("else") == std::vector{TokenKind::Else});
}

// ---------------------------------------------------------------------------
// Transform keywords
// ---------------------------------------------------------------------------
Expand Down
47 changes: 47 additions & 0 deletions tests/test_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,53 @@ TEST_CASE("Parser:multiple root statements", "[parser]") {
REQUIRE(r.roots.size() == 2);
}

// ---------------------------------------------------------------------------
// if / else
// ---------------------------------------------------------------------------
static const IfNode& asIf(const AstNodePtr& n) {
return std::get<IfNode>(*n);
}

TEST_CASE("Parser:if with single then child", "[parser]") {
auto r = parse("if (1) sphere(r=3);");
REQUIRE(r.roots.size() == 1);
auto& n = asIf(r.roots[0]);
REQUIRE(n.thenChildren.size() == 1);
REQUIRE(n.elseChildren.empty());
REQUIRE(asPrim(n.thenChildren[0]).kind == PrimitiveNode::Kind::Sphere);
}

TEST_CASE("Parser:if with brace body", "[parser]") {
auto r = parse("if (1) { cube([5,5,5]); sphere(r=2); }");
auto& n = asIf(r.roots[0]);
REQUIRE(n.thenChildren.size() == 2);
REQUIRE(n.elseChildren.empty());
}

TEST_CASE("Parser:if-else both branches", "[parser]") {
auto r = parse("if (0) sphere(r=3); else cube([4,4,4]);");
auto& n = asIf(r.roots[0]);
REQUIRE(n.thenChildren.size() == 1);
REQUIRE(n.elseChildren.size() == 1);
REQUIRE(asPrim(n.thenChildren[0]).kind == PrimitiveNode::Kind::Sphere);
REQUIRE(asPrim(n.elseChildren[0]).kind == PrimitiveNode::Kind::Cube);
}

TEST_CASE("Parser:if-else chained", "[parser]") {
// else branch is itself an if — chaining works naturally
auto r = parse("if (0) sphere(r=1); else if (1) cube([2,2,2]);");
auto& outer = asIf(r.roots[0]);
REQUIRE(outer.elseChildren.size() == 1);
auto& inner = asIf(outer.elseChildren[0]);
REQUIRE(inner.thenChildren.size() == 1);
}

TEST_CASE("Parser:if condition is expression", "[parser]") {
auto r = parse("if (3 > 2) sphere(r=1);");
REQUIRE(r.roots.size() == 1);
REQUIRE(std::holds_alternative<IfNode>(*r.roots[0]));
}

// ---------------------------------------------------------------------------
// Error recovery
// ---------------------------------------------------------------------------
Expand Down
Loading