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
77 changes: 77 additions & 0 deletions src/app/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ void Application::run() {
initImGui();

m_camera.init(m_config.cameraDistance);
m_camera.setState(m_config.cameraYaw, m_config.cameraPitch,
m_config.cameraDistance,
{m_config.cameraTargetX, m_config.cameraTargetY, m_config.cameraTargetZ});
m_meshBuilder.setWarnOverlappingRoots(m_config.warnOverlappingRoots);

// Restore last-opened file when none was provided on the command line
if (m_state.scadPath.empty() && !m_config.lastFilePath.empty()) {
std::filesystem::path lastPath(m_config.lastFilePath);
std::error_code ec;
if (std::filesystem::exists(lastPath, ec)) {
m_state.scadPath = lastPath;
m_firstMesh = false; // use restored camera, don't auto-fit
}
}

if (!m_state.scadPath.empty()) {
auto ext = m_state.scadPath.extension().string();
Expand Down Expand Up @@ -188,6 +202,26 @@ void Application::run() {
}

vkDeviceWaitIdle(m_ctx.device());

// Persist window state
int ww = 0, wh = 0;
glfwGetWindowSize(m_window, &ww, &wh);
if (ww > 0 && wh > 0) {
m_config.windowWidth = ww;
m_config.windowHeight = wh;
}

// Persist camera state
m_config.cameraDistance = m_camera.distance();
m_config.cameraYaw = m_camera.yaw();
m_config.cameraPitch = m_camera.pitch();
m_config.cameraTargetX = m_camera.target().x;
m_config.cameraTargetY = m_camera.target().y;
m_config.cameraTargetZ = m_camera.target().z;

// Persist last opened file
m_config.lastFilePath = m_state.scadPath.string();

m_config.save(Config::defaultPath());
}

Expand Down Expand Up @@ -387,6 +421,9 @@ void Application::drawMenuBar() {
if (ImGui::MenuItem("Presentation Mode", "P", m_presentationMode))
m_presentationMode = !m_presentationMode;
ImGui::Separator();
if (ImGui::MenuItem("Preferences..."))
m_showPrefs = true;
ImGui::Separator();

if (ImGui::BeginMenu("Rendering")) {
bool isSolid = (m_renderMode == render::RenderMode::Solid);
Expand Down Expand Up @@ -431,6 +468,44 @@ void Application::drawMenuBar() {
ImGui::EndMenuBar();
}

// ---------------------------------------------------------------------------
// Preferences popup
// ---------------------------------------------------------------------------
void Application::drawPrefsPopup() {
if (m_showPrefs) {
ImGui::OpenPopup("Preferences");
m_showPrefs = false;
}

ImGui::SetNextWindowSize({360, 0}, ImGuiCond_Always);
if (ImGui::BeginPopupModal("Preferences", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) {

// ── Analysis ─────────────────────────────────────────────────────
ImGui::SeparatorText("Analysis");

bool prev = m_config.warnOverlappingRoots;
ImGui::Checkbox("Warn on overlapping root objects", &m_config.warnOverlappingRoots);
if (ImGui::IsItemHovered())
ImGui::SetTooltip(
"After each build, test whether any top-level objects\n"
"overlap and warn if so. Has a small per-pair cost;\n"
"disable for large scenes with many root objects.");
if (m_config.warnOverlappingRoots != prev) {
m_meshBuilder.setWarnOverlappingRoots(m_config.warnOverlappingRoots);
if (!m_state.scadPath.empty())
m_meshBuilder.requestBuild(m_state.scadPath);
}

ImGui::Spacing();
ImGui::Separator();
if (ImGui::Button("Close", {80, 0}))
ImGui::CloseCurrentPopup();

ImGui::EndPopup();
}
}

// ---------------------------------------------------------------------------
// ImGui
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -565,6 +640,8 @@ void Application::drawImGui() {
ImGui::EndPopup();
}

drawPrefsPopup();

if (m_showAbout)
ImGui::OpenPopup("About ChiselCAD");

Expand Down
2 changes: 2 additions & 0 deletions src/app/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Application {
// ImGui drawing
void drawMenuBar();
void drawImGui();
void drawPrefsPopup();

// Camera / file helpers
void fitToView();
Expand Down Expand Up @@ -99,6 +100,7 @@ class Application {
// UI state
bool m_showChiselPanel = true;
bool m_showAbout = false;
bool m_showPrefs = false;
float m_fontScale = 1.0f;

// Export error shown in a modal
Expand Down
18 changes: 16 additions & 2 deletions src/app/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ Config Config::load(const std::filesystem::path& path) {
if (j.contains("windowWidth")) cfg.windowWidth = j["windowWidth"];
if (j.contains("windowHeight")) cfg.windowHeight = j["windowHeight"];
if (j.contains("cameraDistance")) cfg.cameraDistance = j["cameraDistance"];
if (j.contains("cameraYaw")) cfg.cameraYaw = j["cameraYaw"].get<float>();
if (j.contains("cameraPitch")) cfg.cameraPitch = j["cameraPitch"].get<float>();
if (j.contains("cameraTargetX")) cfg.cameraTargetX = j["cameraTargetX"].get<float>();
if (j.contains("cameraTargetY")) cfg.cameraTargetY = j["cameraTargetY"].get<float>();
if (j.contains("cameraTargetZ")) cfg.cameraTargetZ = j["cameraTargetZ"].get<float>();
if (j.contains("lastFilePath")) cfg.lastFilePath = j["lastFilePath"];
if (j.contains("globalFn")) cfg.globalFn = j["globalFn"];
if (j.contains("globalFs")) cfg.globalFs = j["globalFs"];
if (j.contains("globalFa")) cfg.globalFa = j["globalFa"];
if (j.contains("fontSize")) cfg.fontSize = j["fontSize"];
if (j.contains("fontSize")) cfg.fontSize = j["fontSize"];
if (j.contains("warnOverlappingRoots")) cfg.warnOverlappingRoots = j["warnOverlappingRoots"];
} catch (const std::exception& e) {
spdlog::warn("Config load failed: {}", e.what());
}
Expand All @@ -50,10 +57,17 @@ void Config::save(const std::filesystem::path& path) const {
j["windowWidth"] = windowWidth;
j["windowHeight"] = windowHeight;
j["cameraDistance"] = cameraDistance;
j["cameraYaw"] = cameraYaw;
j["cameraPitch"] = cameraPitch;
j["cameraTargetX"] = cameraTargetX;
j["cameraTargetY"] = cameraTargetY;
j["cameraTargetZ"] = cameraTargetZ;
j["lastFilePath"] = lastFilePath;
j["globalFn"] = globalFn;
j["globalFs"] = globalFs;
j["globalFa"] = globalFa;
j["fontSize"] = fontSize;
j["fontSize"] = fontSize;
j["warnOverlappingRoots"] = warnOverlappingRoots;
std::ofstream f(path);
f << j.dump(2);
} catch (const std::exception& e) {
Expand Down
10 changes: 10 additions & 0 deletions src/app/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ struct Config {
int windowWidth = 1280;
int windowHeight = 800;
float cameraDistance = 50.0f;
float cameraYaw = 0.0f;
float cameraPitch = 0.4f;
float cameraTargetX = 0.0f;
float cameraTargetY = 0.0f;
float cameraTargetZ = 0.0f;
std::string lastFilePath;

double globalFn = 0.0;
double globalFs = 2.0;
double globalFa = 12.0;
int fontSize = 1; // 0=small(0.85×), 1=normal(1.0×), 2=large(1.3×)

// Analysis preferences
bool warnOverlappingRoots = false;

static Config load(const std::filesystem::path& path);
void save(const std::filesystem::path& path) const;

Expand Down
80 changes: 70 additions & 10 deletions src/app/MeshBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ void MeshBuilder::buildOne(std::filesystem::path path, int gen) {
csg::MeshCache cache;
csg::MeshEvaluator meshEval(cache);
meshEval.useManifoldSphere = m_useManifoldSphere.load();
manifold::Manifold manifoldMesh;
std::vector<manifold::Manifold> rootMeshes;
try {
manifoldMesh = meshEval.evaluate(scene);
rootMeshes = meshEval.evaluate(scene);
} catch (const std::exception& e) {
result->errorMsg = std::string("Mesh error: ") + e.what();
storeError(std::move(result));
Expand All @@ -184,18 +184,78 @@ void MeshBuilder::buildOne(std::filesystem::path path, int gen) {
// ---- Phase: Converting to vertex buffers ----
m_phase = BuildPhase::Converting;

// Capture mesh properties before flat-shading (volume is exact, counts comparable)
{
result->volume = manifoldMesh.Volume();
result->surfaceArea = manifoldMesh.SurfaceArea();
// Each root is converted independently and appended — this keeps objects
// that are spatially inside other objects visible (no boolean union across roots).
std::vector<uint32_t> rootVertStart; // per-root start index into result->verts
rootVertStart.reserve(rootMeshes.size());

for (const auto& m : rootMeshes) {
rootVertStart.push_back(static_cast<uint32_t>(result->verts.size()));

result->volume += m.Volume();
result->surfaceArea += m.SurfaceArea();

auto rawMesh = manifoldMesh.GetMeshGL();
result->triCount = static_cast<uint32_t>(rawMesh.triVerts.size() / 3);
result->vertCount = static_cast<uint32_t>(
auto rawMesh = m.GetMeshGL();
result->triCount += static_cast<uint32_t>(rawMesh.triVerts.size() / 3);
result->vertCount += static_cast<uint32_t>(
rawMesh.numProp > 0 ? rawMesh.vertProperties.size() / rawMesh.numProp : 0);

std::vector<render::Vertex> verts;
std::vector<uint32_t> indices;
manifoldToMesh(m, verts, indices);

// Offset indices by the current vertex count before appending
const auto base = static_cast<uint32_t>(result->verts.size());
for (auto& idx : indices) idx += base;

result->verts.insert(result->verts.end(), verts.begin(), verts.end());
result->indices.insert(result->indices.end(), indices.begin(), indices.end());
}

manifoldToMesh(manifoldMesh, result->verts, result->indices);
// ---- Optional: pairwise overlap detection ----
if (m_warnOverlappingRoots.load() && rootMeshes.size() > 1) {
// Compute per-root AABB from the already-converted vertex data
const auto totalVerts = static_cast<uint32_t>(result->verts.size());
auto rootAABB = [&](std::size_t ri) -> std::pair<glm::vec3, glm::vec3> {
uint32_t start = rootVertStart[ri];
uint32_t end = (ri + 1 < rootVertStart.size())
? rootVertStart[ri + 1] : totalVerts;
glm::vec3 bmin{ 1e30f, 1e30f, 1e30f};
glm::vec3 bmax{-1e30f, -1e30f, -1e30f};
for (uint32_t vi = start; vi < end; ++vi) {
bmin = glm::min(bmin, result->verts[vi].pos);
bmax = glm::max(bmax, result->verts[vi].pos);
}
return {bmin, bmax};
};

auto aabbOverlap = [](glm::vec3 mn1, glm::vec3 mx1,
glm::vec3 mn2, glm::vec3 mx2) {
return (mn1.x <= mx2.x && mx1.x >= mn2.x) &&
(mn1.y <= mx2.y && mx1.y >= mn2.y) &&
(mn1.z <= mx2.z && mx1.z >= mn2.z);
};

for (std::size_t i = 0; i < rootMeshes.size(); ++i) {
if (gen != m_currentGen.load()) return; // newer build queued — abort
auto [mn1, mx1] = rootAABB(i);
for (std::size_t j = i + 1; j < rootMeshes.size(); ++j) {
auto [mn2, mx2] = rootAABB(j);
if (!aabbOverlap(mn1, mx1, mn2, mx2)) continue;

// AABBs overlap — run exact Manifold intersection
manifold::Manifold sect = rootMeshes[i] ^ rootMeshes[j];
if (std::abs(sect.Volume()) > 1e-6) {
lang::Diagnostic warn;
warn.level = lang::DiagLevel::Warning;
warn.message = "Objects " + std::to_string(i + 1) +
" and " + std::to_string(j + 1) +
" overlap — wrap in union() or difference() if intentional";
result->diags.push_back(std::move(warn));
}
}
}
}
result->elapsedMs = elapsedMs();

m_elapsedMs = result->elapsedMs;
Expand Down
4 changes: 3 additions & 1 deletion src/app/MeshBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class MeshBuilder {
// discarded when poll() is next called.
void requestBuild(std::filesystem::path path);

void setUseManifoldSphere(bool v) noexcept { m_useManifoldSphere.store(v); }
void setUseManifoldSphere(bool v) noexcept { m_useManifoldSphere.store(v); }
void setWarnOverlappingRoots(bool v) noexcept { m_warnOverlappingRoots.store(v); }

// Call once per frame from the main (Vulkan) thread.
// Returns a finished BuildResult when one is ready, nullptr otherwise.
Expand Down Expand Up @@ -86,6 +87,7 @@ class MeshBuilder {
// Incremented by requestBuild(); read by poll() to detect stale results.
std::atomic<int> m_currentGen{0};
std::atomic<bool> m_useManifoldSphere{false};
std::atomic<bool> m_warnOverlappingRoots{false};

// Readable from main thread for UI without locks.
std::atomic<BuildPhase> m_phase{BuildPhase::Idle};
Expand Down
69 changes: 69 additions & 0 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ CsgScene CsgEvaluator::evaluate(const ParseResult& result) {
CsgScene CsgEvaluator::evaluate(const ParseResult& result, Interpreter& interp) {
m_interp = &interp;

// Index module definitions by name for O(1) lookup during calls
m_moduleDefs.clear();
for (const auto& def : result.moduleDefs)
m_moduleDefs[def.name] = &def;

CsgScene scene;
scene.globalFn = result.globalFn;
scene.globalFs = result.globalFs;
Expand All @@ -32,6 +37,7 @@ CsgScene CsgEvaluator::evaluate(const ParseResult& result, Interpreter& interp)
}

m_interp = nullptr;
m_moduleDefs.clear();
return scene;
}

Expand All @@ -51,6 +57,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
return evalIf(n, xform);
else if constexpr (std::is_same_v<T, ForNode>)
return evalFor(n, xform);
else if constexpr (std::is_same_v<T, ModuleCallNode>)
return evalModuleCall(n, xform);
return nullptr;
}, node);
}
Expand Down Expand Up @@ -250,4 +258,65 @@ CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) {
return makeBoolean(std::move(u));
}

// ---------------------------------------------------------------------------
// Module call — bind args, evaluate body, restore environment
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::mat4& xform) {
auto it = m_moduleDefs.find(call.name);
if (it == m_moduleDefs.end()) return nullptr; // undefined module

const ModuleDef& def = *it->second;

// Snapshot the interpreter env so we can restore it after the call
auto savedEnv = m_interp->snapshotEnv();

// Bind positional and named arguments to module parameters
std::size_t posIdx = 0;
for (const auto& arg : call.args) {
if (arg.name.empty()) {
// Positional: bind to parameter at posIdx
if (posIdx < def.params.size())
m_interp->setVar(def.params[posIdx].name,
Value::fromNumber(m_interp->evalNumber(*arg.value)));
++posIdx;
} else {
// Named: bind to the matching parameter
m_interp->setVar(arg.name,
Value::fromNumber(m_interp->evalNumber(*arg.value)));
}
}

// Fill in defaults for parameters that were not supplied
for (std::size_t i = 0; i < def.params.size(); ++i) {
const auto& param = def.params[i];
// Skip params already bound by positional or named args
bool alreadyBound = (i < posIdx);
if (!alreadyBound) {
for (const auto& arg : call.args)
if (arg.name == param.name) { alreadyBound = true; break; }
}
if (!alreadyBound && param.defaultVal)
m_interp->setVar(param.name,
Value::fromNumber(m_interp->evalNumber(*param.defaultVal)));
}

// Evaluate the module body and collect geometry
std::vector<CsgNodePtr> all;
for (const auto& child : def.body) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
}

// Restore the caller's environment
m_interp->restoreEnv(std::move(savedEnv));

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
Loading
Loading