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
49 changes: 49 additions & 0 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
return evalTransform(n, xform);
else if constexpr (std::is_same_v<T, IfNode>)
return evalIf(n, xform);
else if constexpr (std::is_same_v<T, ForNode>)
return evalFor(n, xform);
return nullptr;
}, node);
}
Expand Down Expand Up @@ -201,4 +203,51 @@ CsgNodePtr CsgEvaluator::evalIf(const IfNode& node, const glm::mat4& xform) {
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// for — iterate range or list, union all child geometry
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) {
// Build the sequence of iteration values
std::vector<double> values;
static constexpr int kMaxIter = 10000;

if (node.range.isRange) {
double start = m_interp->evalNumber(*node.range.start);
double end = m_interp->evalNumber(*node.range.end);
double step = node.range.step
? m_interp->evalNumber(*node.range.step)
: 1.0;
if (step == 0.0) return nullptr;
if (step > 0.0)
for (double v = start; v <= end + 1e-10 && (int)values.size() < kMaxIter; v += step)
values.push_back(v);
else
for (double v = start; v >= end - 1e-10 && (int)values.size() < kMaxIter; v += step)
values.push_back(v);
} else {
for (const auto& e : node.range.list)
values.push_back(m_interp->evalNumber(*e));
}

// Save the loop variable's current binding, iterate, then restore
const Value saved = m_interp->getVar(node.var);
std::vector<CsgNodePtr> all;
for (double v : values) {
m_interp->setVar(node.var, Value::fromNumber(v));
for (const auto& child : node.children) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
}
}
m_interp->setVar(node.var, saved);

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

CsgBoolean u;
u.op = CsgBoolean::Op::Union;
u.children = std::move(all);
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 @@ -29,6 +29,7 @@ class CsgEvaluator {
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);
CsgNodePtr evalFor(const chisel::lang::ForNode& n, const glm::mat4& xform);

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

// ---------------------------------------------------------------------------
// 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, IfNode>;
using AstNode = std::variant<PrimitiveNode, BooleanNode, TransformNode, IfNode, ForNode>;
using AstNodePtr = std::unique_ptr<AstNode>;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -90,6 +91,30 @@ inline AstNodePtr makeIf(IfNode n) {
return std::make_unique<AstNode>(std::move(n));
}

// ---------------------------------------------------------------------------
// ForNode — for (var = [start:step:end]) { ... }
// for (var = [start:end]) { ... } (step defaults to 1)
// for (var = [v0, v1, v2, ...]) { ... } (explicit list)
// ---------------------------------------------------------------------------
struct ForRange {
bool isRange = true; // true: start/step/end; false: list
ExprPtr start; // range form — required
ExprPtr step; // range form — nullptr means step of 1
ExprPtr end; // range form — required
std::vector<ExprPtr> list; // list form
};

struct ForNode {
std::string var;
ForRange range;
std::vector<AstNodePtr> children;
SourceLoc loc;
};

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

// ---------------------------------------------------------------------------
// AssignStmt — a variable assignment at file or block scope: x = expr;
// ---------------------------------------------------------------------------
Expand Down
12 changes: 12 additions & 0 deletions src/lang/Interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ std::array<double, 3> Interpreter::evalVec3(const ExprNode& expr) const {
return result;
}

// ---------------------------------------------------------------------------
// getVar / setVar — used by CsgEvaluator to bind for-loop variables
// ---------------------------------------------------------------------------
Value Interpreter::getVar(const std::string& name) const {
auto it = m_env.find(name);
return it != m_env.end() ? it->second : Value::undef();
}

void Interpreter::setVar(const std::string& name, Value val) {
m_env[name] = std::move(val);
}

// ---------------------------------------------------------------------------
// Built-in functions (V2a subset — math functions added in V2c)
// ---------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/lang/Interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class Interpreter {
// Missing elements default to 0.0.
std::array<double, 3> evalVec3(const ExprNode& expr) const;

// For-loop variable binding — used by CsgEvaluator::evalFor.
Value getVar(const std::string& name) const;
void setVar(const std::string& name, Value val);

private:
std::unordered_map<std::string, Value> m_env;

Expand Down
2 changes: 2 additions & 0 deletions src/lang/Lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ static const std::unordered_map<std::string_view, TokenKind> kKeywords = {
{"mirror", TokenKind::Mirror},
{"if", TokenKind::If},
{"else", TokenKind::Else},
{"for", TokenKind::For},
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -94,6 +95,7 @@ std::vector<Token> Lexer::tokenize() {
case ']': tokens.push_back(makeToken(TokenKind::RBracket, startOffset)); break;
case ',': tokens.push_back(makeToken(TokenKind::Comma, startOffset)); break;
case ';': tokens.push_back(makeToken(TokenKind::Semicolon, startOffset)); break;
case ':': tokens.push_back(makeToken(TokenKind::Colon, startOffset)); break;
case '+': tokens.push_back(makeToken(TokenKind::Plus, startOffset)); break;
case '-': tokens.push_back(makeToken(TokenKind::Minus, startOffset)); break;
case '*': tokens.push_back(makeToken(TokenKind::Star, startOffset)); break;
Expand Down
54 changes: 54 additions & 0 deletions src/lang/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ AstNodePtr Parser::parseNode() {
case TokenKind::If:
return parseIf();

case TokenKind::For:
return parseFor();

default:
return nullptr;
}
Expand Down Expand Up @@ -234,6 +237,57 @@ AstNodePtr Parser::parseIf() {
return makeIf(std::move(node));
}

// ---------------------------------------------------------------------------
// for — for (var = [start:step:end]) or for (var = [v0, v1, ...])
// ---------------------------------------------------------------------------
AstNodePtr Parser::parseFor() {
const Token& kw = advance(); // consume 'for'
ForNode node;
node.loc = kw.loc;

expect(TokenKind::LParen, "expected '(' after 'for'");
node.var = expect(TokenKind::Ident, "expected loop variable").text;
expect(TokenKind::Equals, "expected '=' after loop variable");
expect(TokenKind::LBracket, "expected '[' for range/list");

// Parse first expression — determines range vs list form
auto first = parseExpr();

if (check(TokenKind::Colon)) {
// Range form: [start : end] or [start : step : end]
advance(); // consume ':'
auto second = parseExpr();
if (check(TokenKind::Colon)) {
advance(); // consume ':'
auto third = parseExpr();
// [first:second:third] = [start:step:end]
node.range.isRange = true;
node.range.start = std::move(first);
node.range.step = std::move(second);
node.range.end = std::move(third);
} else {
// [first:second] = [start:end], implicit step of 1
node.range.isRange = true;
node.range.start = std::move(first);
node.range.end = std::move(second);
}
} else {
// List form: [first, ...]
node.range.isRange = false;
node.range.list.push_back(std::move(first));
while (match(TokenKind::Comma)) {
if (check(TokenKind::RBracket)) break;
node.range.list.push_back(parseExpr());
}
}

expect(TokenKind::RBracket, "expected ']' after range/list");
expect(TokenKind::RParen, "expected ')' after for header");

node.children = parseBody();
return makeFor(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 @@ -37,6 +37,7 @@ class Parser {
AstNodePtr parseBoolean(TokenKind k);
AstNodePtr parseTransform(TokenKind k);
AstNodePtr parseIf();
AstNodePtr parseFor();

// ---- 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 @@ -49,6 +49,10 @@ enum class TokenKind : uint8_t {
// Control flow
If, // if
Else, // else
For, // for

// Range separator
Colon, // :

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

// ─── Scene 1 (0, 0, 0): simple range — row of spheres ────────────────────────
// for (i = [0:4]) — five spheres spaced 12 apart
translate([0, 0, 0])
for (i = [0:4])
translate([i*12, 0, 0]) sphere(r=4);

// ─── Scene 2 (0, -25, 0): range with step — every other position ─────────────
// for (i = [0:2:8]) — five spheres at i=0,2,4,6,8
translate([0, -25, 0])
for (i = [0:2:8])
translate([i*6, 0, 0]) sphere(r=3);

// ─── Scene 3 (0, -50, 0): list of values ─────────────────────────────────────
// Cylinders of varying radii
translate([0, -50, 0])
for (r = [2, 4, 6, 4, 2])
translate([r*5, 0, 0]) cylinder(h=r*2, r=r, center=true);

// ─── Scene 4 (0, -80, 0): loop variable in expression ────────────────────────
// Staircase — cube height grows with i
translate([0, -80, 0])
for (i = [0:5])
translate([i*8, 0, (i+1)*2]) cube([6, 6, (i+1)*4], center=true);

// ─── Scene 5 (0, -115, 0): for inside difference (cutting pattern) ───────────
// Cube with a row of cylindrical holes
translate([0, -115, 0])
difference() {
cube([60, 12, 12], center=true);
for (i = [0:4])
translate([-24 + i*12, 0, 0]) cylinder(h=20, r=3, center=true);
}

// ─── Scene 6 (0, -145, 0): nested for — grid of spheres ─────────────────────
translate([0, -145, 0])
for (x = [0:3])
for (y = [0:3])
translate([x*10, y*10, 0]) sphere(r=3);
57 changes: 57 additions & 0 deletions tests/test_csg_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,60 @@ TEST_CASE("CsgEval:if inherits outer transform", "[csg]") {
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).transform[3][0] == Approx(7.0f));
}

// ---------------------------------------------------------------------------
// for loops
// ---------------------------------------------------------------------------
TEST_CASE("CsgEval:for range produces one child per step", "[csg]") {
// [0:2] → i=0,1,2 — three spheres wrapped in a union
auto s = evaluate("for (i = [0:2]) sphere(r=1);");
REQUIRE(s.roots.size() == 1);
const auto& b = asBool(s.roots[0]);
REQUIRE(b.op == CsgBoolean::Op::Union);
REQUIRE(b.children.size() == 3);
}

TEST_CASE("CsgEval:for range with step", "[csg]") {
// [0:2:6] → i=0,2,4,6 — four children
auto s = evaluate("for (i = [0:2:6]) sphere(r=1);");
const auto& b = asBool(s.roots[0]);
REQUIRE(b.children.size() == 4);
}

TEST_CASE("CsgEval:for list produces one child per value", "[csg]") {
auto s = evaluate("for (v = [1, 5, 9]) cube([v, v, v]);");
const auto& b = asBool(s.roots[0]);
REQUIRE(b.op == CsgBoolean::Op::Union);
REQUIRE(b.children.size() == 3);
}

TEST_CASE("CsgEval:for single iteration returns leaf directly", "[csg]") {
// [3:3] → i=3 only — no wrapping union needed
auto s = evaluate("for (i = [3:3]) sphere(r=2);");
REQUIRE(s.roots.size() == 1);
REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Sphere);
}

TEST_CASE("CsgEval:for uses loop variable in child", "[csg]") {
// translate([i,0,0]) — each sphere should sit at x = i
auto s = evaluate("for (i = [0:2]) translate([i*5, 0, 0]) sphere(r=1);");
const auto& b = asBool(s.roots[0]);
REQUIRE(b.children.size() == 3);
REQUIRE(asLeaf(b.children[0]).transform[3][0] == Approx(0.0f));
REQUIRE(asLeaf(b.children[1]).transform[3][0] == Approx(5.0f));
REQUIRE(asLeaf(b.children[2]).transform[3][0] == Approx(10.0f));
}

TEST_CASE("CsgEval:for restores outer variable after loop", "[csg]") {
// 'i' is set before the loop; the for loop should restore it afterward
auto s = evaluate("i = 99; for (i = [0:1]) sphere(r=1); cube([i,i,i]);");
// cube gets i=99 (restored), for produces 2 spheres
REQUIRE(s.roots.size() == 2);
REQUIRE(asLeaf(s.roots[1]).params.at("x") == Approx(99.0));
}

TEST_CASE("CsgEval:for empty range yields no geometry", "[csg]") {
// [5:3] with positive step — no iterations
auto s = evaluate("for (i = [5:3]) sphere(r=1);");
REQUIRE(s.roots.empty());
}
15 changes: 15 additions & 0 deletions tests/test_lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ TEST_CASE("Lexer:if and else keywords", "[lexer]") {
REQUIRE(kinds("else") == std::vector{TokenKind::Else});
}

TEST_CASE("Lexer:for keyword and colon", "[lexer]") {
REQUIRE(kinds("for") == std::vector{TokenKind::For});
REQUIRE(kinds(":") == std::vector{TokenKind::Colon});
}

TEST_CASE("Lexer:for range tokens", "[lexer]") {
// for (i = [0:5]) → for ( i = [ 0 : 5 ] )
auto t = kinds("for (i = [0:5])");
REQUIRE(t[0] == TokenKind::For);
REQUIRE(t[4] == TokenKind::LBracket);
REQUIRE(t[5] == TokenKind::Number);
REQUIRE(t[6] == TokenKind::Colon);
REQUIRE(t[7] == TokenKind::Number);
}

// ---------------------------------------------------------------------------
// Transform keywords
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading