From 66f7f241ba540dda0378b75426b6132f6d82a3db Mon Sep 17 00:00:00 2001 From: trailcode Date: Thu, 21 May 2026 17:28:02 -0600 Subject: [PATCH 1/9] Shp list on hover, annotate --- src/gui.cpp | 16 +++++++++++++-- src/occt_view.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++++++ src/occt_view.h | 4 ++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/gui.cpp b/src/gui.cpp index 4ddd95a..5913f01 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1775,7 +1775,10 @@ bool GUI::try_underlay_calib_click_(const ScreenCoords& screen_coords) void GUI::shape_list_() { if (!show_shape_list_effective()) + { + m_view->set_shape_list_hover(nullptr); return; + } float max_name_text_w = 0.f; for (const Shp_ptr& s : m_view->get_shapes()) @@ -1787,6 +1790,7 @@ void GUI::shape_list_() if (!ImGui::Begin("Shape List", &m_show_shape_list, ImGuiWindowFlags_None)) { + m_view->set_shape_list_hover(nullptr); ImGui::End(); return; } @@ -1815,6 +1819,7 @@ void GUI::shape_list_() st_mat.FramePadding.x * 2.0f + st_mat.ScrollbarSize + 8.0f)); Shp_ptr shape_to_delete; + Shp_ptr shape_list_hover; std::unordered_set selected_in_viewer; for (const AIS_Shape_ptr& ais : m_view->get_selected()) @@ -1843,9 +1848,10 @@ void GUI::shape_list_() ImVec4(std::min(1.0f, text.x * k_shape_list_selected_text_rgb_scale + k_shape_list_selected_text_rgb_bias), std::min(1.0f, text.y * k_shape_list_selected_text_rgb_scale + k_shape_list_selected_text_rgb_bias), std::min(1.0f, text.z * k_shape_list_selected_text_rgb_scale + k_shape_list_selected_text_rgb_bias), text.w)); - ImGui::BeginGroup(); } + ImGui::BeginGroup(); + // Unique ID suffix using index std::string id_suffix = "##" + std::to_string(index++); // Editable text box for name @@ -1950,15 +1956,21 @@ void GUI::shape_list_() ImGui::PopID(); + ImGui::EndGroup(); + + if (ImGui::IsItemHovered() && shape->get_visible()) + shape_list_hover = shape; + if (row_selected) { - ImGui::EndGroup(); ImGui::PopStyleColor(4); if (ui_show_help(2) && ImGui::IsItemHovered()) ImGui::SetTooltip("Selected in 3D viewer"); } } + m_view->set_shape_list_hover(shape_list_hover); + if (shape_to_delete) m_view->delete_shapes({shape_to_delete}); diff --git a/src/occt_view.cpp b/src/occt_view.cpp index a300d57..5b4c421 100644 --- a/src/occt_view.cpp +++ b/src/occt_view.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,6 +44,7 @@ #include "geom.h" #include "gui.h" #include "ply_io.h" +#include "shp.h" #include "shp_create.h" #include "sketch.h" #include "sketch_json.h" @@ -1098,6 +1101,13 @@ void Occt_view::delete_(std::vector& to_delete) else ++itr; + for (const AIS_Shape_ptr& shp : to_delete) + if (m_shape_list_hover == shp) + { + set_shape_list_hover(nullptr); + break; + } + remove(to_delete); } @@ -1390,6 +1400,48 @@ std::vector Occt_view::get_selected() const return shapes; } +namespace +{ +opencascade::handle shape_list_hover_drawer_() +{ + static opencascade::handle drawer; + if (drawer.IsNull()) + { + drawer = new Prs3d_Drawer(); + drawer->SetColor(Quantity_NOC_CYAN1); + Handle(Graphic3d_AspectFillArea3d) fill_aspect = new Graphic3d_AspectFillArea3d(); + fill_aspect->SetAlphaMode(Graphic3d_AlphaMode_Blend); + fill_aspect->SetColor(Quantity_Color(0.1, 0.1, 0.1, Quantity_TOC_RGB)); + drawer->SetBasicFillAreaAspect(fill_aspect); + Handle(Prs3d_LineAspect) wire_aspect = new Prs3d_LineAspect(Quantity_NOC_CYAN1, Aspect_TOL_SOLID, 2.0); + drawer->SetWireAspect(wire_aspect); + } + return drawer; +} +} // namespace + +void Occt_view::set_shape_list_hover(const Shp_ptr& shp) +{ + if (is_headless() || m_ctx.IsNull()) + return; + + if (m_shape_list_hover == shp) + return; + + if (!m_shape_list_hover.IsNull()) + { + m_ctx->Unhilight(m_shape_list_hover, Standard_False); + m_shape_list_hover.Nullify(); + } + + m_shape_list_hover = shp; + + if (!m_shape_list_hover.IsNull() && m_shape_list_hover->get_visible()) + m_ctx->HilightWithColor(m_shape_list_hover, shape_list_hover_drawer_(), Standard_False); + + m_ctx->UpdateCurrentViewer(); +} + TopAbs_ShapeEnum Occt_view::get_shp_selection_mode() const { return m_shp_selection_mode; } void Occt_view::set_shp_selection_mode(const TopAbs_ShapeEnum mode) diff --git a/src/occt_view.h b/src/occt_view.h index d67da04..265695f 100644 --- a/src/occt_view.h +++ b/src/occt_view.h @@ -220,6 +220,9 @@ class Occt_view : protected AIS_ViewController TopAbs_ShapeEnum get_shp_selection_mode() const; void set_shp_selection_mode(const TopAbs_ShapeEnum mode); + /// Highlight \a shp in the 3D viewer while the Shape List row is hovered (null clears). + void set_shape_list_hover(const Shp_ptr& shp); + // Material related const Graphic3d_MaterialAspect& get_default_material() const; void set_default_material(const Graphic3d_MaterialAspect& mat); @@ -309,6 +312,7 @@ class Occt_view : protected AIS_ViewController Sketch_list m_sketches; std::shared_ptr m_cur_sketch; TopAbs_ShapeEnum m_shp_selection_mode{TopAbs_SHAPE}; + Shp_ptr m_shape_list_hover; Graphic3d_MaterialAspect m_default_material; bool m_headless_view{false}; /// True when LMB press was handled by planar-face sketch creation without AIS_ViewController::PressMouseButton (pair with From 0bd89361d260d4e55ce18d7024910d8a3d3e1684 Mon Sep 17 00:00:00 2001 From: trailcode Date: Thu, 21 May 2026 18:04:02 -0600 Subject: [PATCH 2/9] Highlight Shp pane, on mouse shps. --- res/ezycad_settings.json | 6 ++++ src/gui.h | 4 +++ src/gui_settings.cpp | 63 ++++++++++++++++++++++++++++++++++++++++ src/occt_view.cpp | 49 ++++++++++++++++++++----------- src/occt_view.h | 9 ++++-- 5 files changed, 112 insertions(+), 19 deletions(-) diff --git a/res/ezycad_settings.json b/res/ezycad_settings.json index 8723e8f..095d882 100644 --- a/res/ezycad_settings.json +++ b/res/ezycad_settings.json @@ -23,6 +23,12 @@ 1.0, 0.8627451062202454, 0.0 + ], + "shape_list_hover_color": [ + 0.0, + 1.0, + 0.0, + 1.0 ] }, "imgui_ini": "[Window][##MainMenuBar]\nPos=0,0\nSize=1920,22\n\n[Window][Toolbar]\nPos=7,24\nSize=1310,58\n\n[Window][Sketch List]\nPos=8,86\nSize=321,225\n\n[Window][Shape List]\nPos=8,332\nSize=220,360\n\n[Window][Options]\nPos=1322,26\nSize=389,277\n\n[Window][Lua Console]\nPos=5,855\nSize=1291,313\nCollapsed=1\n\n[Window][Python Console]\nPos=6,831\nSize=1290,225\nCollapsed=1\n\n[Window][Log]\nPos=3,975\nSize=1280,180\nCollapsed=1\n\n[Window][Settings]\nPos=796,86\nSize=520,560\nCollapsed=1\n\n[Window][Debug##Default]\nPos=60,60\nSize=400,400\n\n[Window][dbg]\nPos=260,420\nSize=320,200\n\n[Window][Sketch properties]\nPos=330,86\nSize=400,116\n\n", diff --git a/src/gui.h b/src/gui.h index 4b0be26..dd4bc96 100644 --- a/src/gui.h +++ b/src/gui.h @@ -172,6 +172,8 @@ class GUI /// Default RGBA (0-255) for sketch underlay line tint when importing a new image (see Settings). void underlay_highlight_color_rgba(uint8_t& r, uint8_t& g, uint8_t& b, uint8_t& a) const; + /// RGBA (0-255) for Shape List row hover highlight in the 3D viewer (see Settings). + void shape_list_hover_color_rgba(uint8_t& r, uint8_t& g, uint8_t& b, uint8_t& a) const; /// For scripting (Lua console): access the 3D view. Occt_view* get_view() { return m_view.get(); } @@ -404,6 +406,8 @@ class GUI glm::vec4 m_underlay_tint_col{1.f, 220.f / 255.f, 0.f, 1.f}; /// Default underlay tint for new imports (0-1, persisted in ezycad_settings.json). glm::vec4 m_underlay_highlight_color{1.f, 220.f / 255.f, 0.f, 1.f}; + /// Shape List hover highlight in the OCCT view (0-1, persisted in ezycad_settings.json). + glm::vec4 m_shape_list_hover_color{0.f, 1.f, 0.f, 1.f}; std::unique_ptr m_lua_console; bool m_show_python_console{false}; diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index 3333d17..c58644f 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -122,6 +122,9 @@ void GUI::save_occt_view_settings() {"underlay_highlight_color", {m_underlay_highlight_color[0], m_underlay_highlight_color[1], m_underlay_highlight_color[2], m_underlay_highlight_color[3]}}, + {"shape_list_hover_color", + {m_shape_list_hover_color[0], m_shape_list_hover_color[1], m_shape_list_hover_color[2], + m_shape_list_hover_color[3]}}, }; j["version"] = k_settings_version; const char* imgui_ini = ImGui::SaveIniSettingsToMemory(nullptr); @@ -334,6 +337,19 @@ void GUI::parse_gui_panes_settings_(const std::string& content) m_underlay_highlight_color[3] = std::clamp(a[3].get(), 0.f, 1.f); } + if (g.contains("shape_list_hover_color") && g["shape_list_hover_color"].is_array() && + g["shape_list_hover_color"].size() >= 3) + { + const json& a = g["shape_list_hover_color"]; + for (int i = 0; i < 3; ++i) + if (a[static_cast(i)].is_number()) + m_shape_list_hover_color[static_cast(i)] = + std::clamp(a[static_cast(i)].get(), 0.f, 1.f); + m_shape_list_hover_color[3] = 1.f; + if (a.size() >= 4 && a[3].is_number()) + m_shape_list_hover_color[3] = std::clamp(a[3].get(), 0.f, 1.f); + } + #ifndef NDEBUG set_show_dbg(b("show_dbg", false)); #endif @@ -635,6 +651,40 @@ void GUI::settings_() m_view->set_bg_gradient_colors(bg1[0], bg1[1], bg1[2], bg2[0], bg2[1], bg2[2]); save_occt_view_settings(); } + + bool shape_list_hover_changed = false; + if (ImGui::BeginTable("settings_shape_list_hover", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, k_label_col_w); + ImGui::TableSetupColumn("control", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Shape list hover color"); + ImGui::TableSetColumnIndex(1); + shape_list_hover_changed |= + ImGui::ColorEdit4("##shape_list_hover", &m_shape_list_hover_color[0], ImGuiColorEditFlags_Float); + if (ui_show_help(2)) + { + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextDisabled("Highlight color when hovering a row in the Shape List pane. Updates immediately if a shape " + "is hovered."); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + ImGui::EndTable(); + } + if (shape_list_hover_changed) + { + m_view->refresh_shape_list_hover_highlight(); + save_occt_view_settings(); + } } if (ui_show_feature(3) && ImGui::CollapsingHeader("3D view grid")) @@ -1025,6 +1075,19 @@ void GUI::underlay_highlight_color_rgba(uint8_t& r, uint8_t& g, uint8_t& b, uint a = to_u8(m_underlay_highlight_color[3]); } +void GUI::shape_list_hover_color_rgba(uint8_t& r, uint8_t& g, uint8_t& b, uint8_t& a) const +{ + const auto to_u8 = [](float c) -> uint8_t + { + const float x = std::clamp(c, 0.f, 1.f) * 255.f; + return static_cast(x + 0.5f); + }; + r = to_u8(m_shape_list_hover_color[0]); + g = to_u8(m_shape_list_hover_color[1]); + b = to_u8(m_shape_list_hover_color[2]); + a = to_u8(m_shape_list_hover_color[3]); +} + void GUI::imgui_rounding_fallbacks_from_theme_(float& general, float& scroll, float& tabs) const { ImGuiStyle s; diff --git a/src/occt_view.cpp b/src/occt_view.cpp index 5b4c421..fa03d87 100644 --- a/src/occt_view.cpp +++ b/src/occt_view.cpp @@ -1400,25 +1400,37 @@ std::vector Occt_view::get_selected() const return shapes; } -namespace +void Occt_view::update_shape_list_hover_drawer_() { -opencascade::handle shape_list_hover_drawer_() + uint8_t r{}, g{}, b{}, a{}; + gui().shape_list_hover_color_rgba(r, g, b, a); + (void)a; + const Quantity_Color qc(static_cast(r) / 255.0, static_cast(g) / 255.0, + static_cast(b) / 255.0, Quantity_TOC_RGB); + + if (m_shape_list_hover_drawer.IsNull()) + m_shape_list_hover_drawer = new Prs3d_Drawer(); + + m_shape_list_hover_drawer->SetColor(qc); + Handle(Graphic3d_AspectFillArea3d) fill_aspect = new Graphic3d_AspectFillArea3d(); + fill_aspect->SetAlphaMode(Graphic3d_AlphaMode_Blend); + fill_aspect->SetColor(Quantity_Color(0.1, 0.1, 0.1, Quantity_TOC_RGB)); + m_shape_list_hover_drawer->SetBasicFillAreaAspect(fill_aspect); + Handle(Prs3d_LineAspect) wire_aspect = new Prs3d_LineAspect(qc, Aspect_TOL_SOLID, 2.0); + m_shape_list_hover_drawer->SetWireAspect(wire_aspect); +} + +void Occt_view::refresh_shape_list_hover_highlight() { - static opencascade::handle drawer; - if (drawer.IsNull()) - { - drawer = new Prs3d_Drawer(); - drawer->SetColor(Quantity_NOC_CYAN1); - Handle(Graphic3d_AspectFillArea3d) fill_aspect = new Graphic3d_AspectFillArea3d(); - fill_aspect->SetAlphaMode(Graphic3d_AlphaMode_Blend); - fill_aspect->SetColor(Quantity_Color(0.1, 0.1, 0.1, Quantity_TOC_RGB)); - drawer->SetBasicFillAreaAspect(fill_aspect); - Handle(Prs3d_LineAspect) wire_aspect = new Prs3d_LineAspect(Quantity_NOC_CYAN1, Aspect_TOL_SOLID, 2.0); - drawer->SetWireAspect(wire_aspect); - } - return drawer; + if (is_headless() || m_ctx.IsNull() || m_shape_list_hover.IsNull()) + return; + + update_shape_list_hover_drawer_(); + m_ctx->Unhilight(m_shape_list_hover, Standard_False); + if (m_shape_list_hover->get_visible()) + m_ctx->HilightWithColor(m_shape_list_hover, m_shape_list_hover_drawer, Standard_False); + m_ctx->UpdateCurrentViewer(); } -} // namespace void Occt_view::set_shape_list_hover(const Shp_ptr& shp) { @@ -1437,7 +1449,10 @@ void Occt_view::set_shape_list_hover(const Shp_ptr& shp) m_shape_list_hover = shp; if (!m_shape_list_hover.IsNull() && m_shape_list_hover->get_visible()) - m_ctx->HilightWithColor(m_shape_list_hover, shape_list_hover_drawer_(), Standard_False); + { + update_shape_list_hover_drawer_(); + m_ctx->HilightWithColor(m_shape_list_hover, m_shape_list_hover_drawer, Standard_False); + } m_ctx->UpdateCurrentViewer(); } diff --git a/src/occt_view.h b/src/occt_view.h index 265695f..284b1df 100644 --- a/src/occt_view.h +++ b/src/occt_view.h @@ -27,6 +27,7 @@ class Sketch; class GUI; +class Prs3d_Drawer; class TopoDS_Face; class TopoDS_Wire; class TopoDS_Edge; @@ -222,6 +223,8 @@ class Occt_view : protected AIS_ViewController /// Highlight \a shp in the 3D viewer while the Shape List row is hovered (null clears). void set_shape_list_hover(const Shp_ptr& shp); + /// Re-apply list-hover highlight after Settings changes the hover color. + void refresh_shape_list_hover_highlight(); // Material related const Graphic3d_MaterialAspect& get_default_material() const; @@ -312,8 +315,10 @@ class Occt_view : protected AIS_ViewController Sketch_list m_sketches; std::shared_ptr m_cur_sketch; TopAbs_ShapeEnum m_shp_selection_mode{TopAbs_SHAPE}; - Shp_ptr m_shape_list_hover; - Graphic3d_MaterialAspect m_default_material; + Shp_ptr m_shape_list_hover; + opencascade::handle m_shape_list_hover_drawer; + void update_shape_list_hover_drawer_(); + Graphic3d_MaterialAspect m_default_material; bool m_headless_view{false}; /// True when LMB press was handled by planar-face sketch creation without AIS_ViewController::PressMouseButton (pair with /// release skip). From eda08e7834e4d6631af858c1cc09bccd7c3caa8a Mon Sep 17 00:00:00 2001 From: trailcode Date: Fri, 22 May 2026 17:18:22 -0600 Subject: [PATCH 3/9] SEO work. --- README.md | 17 ++- agents/README.md | 1 + agents/discoverability-outreach.md | 85 ++++++++++++++ doc/conf.py | 19 ++++ doc/index.rst | 17 ++- res/about.md | 6 +- scripts/sync-github-pages-html.ps1 | 33 ++++++ src/gui.h | 2 +- usage.md | 8 +- web/EzyCad.html | 21 +++- web/index.html | 171 +++++++++++++++++++++++++++++ 11 files changed, 369 insertions(+), 11 deletions(-) create mode 100644 agents/discoverability-outreach.md create mode 100644 scripts/sync-github-pages-html.ps1 create mode 100644 web/index.html diff --git a/README.md b/README.md index 5d37e79..63f3437 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # EzyCad +[![GitHub](https://img.shields.io/github/stars/trailcode/EzyCad?style=social)](https://github.com/trailcode/EzyCad) +[![Documentation](https://img.shields.io/badge/docs-readthedocs-blue)](https://ezycad.readthedocs.io/en/latest/usage.html) +[![WebAssembly](https://img.shields.io/badge/run_in-browser-WebAssembly-green)](https://trailcode.github.io/EzyCad/EzyCad.html) + +**Repository:** [https://github.com/trailcode/EzyCad](https://github.com/trailcode/EzyCad) + ![EzyCad splash screen](doc/gen/AI-gen-splashscreen_05_01_2026_512.png) -EzyCad (Easy CAD) is a CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports creating precise parts with tools for sketching, extruding, and applying geometric operations, using OpenGL, ImGui, and Open CASCADE Technology (OCCT). Export models to formats like STEP or STL for CNC machines or 3D printers, or [run EzyCad in your browser (WebAssembly)](https://trailcode.github.io/EzyCad/EzyCad.html). +EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports creating precise parts with tools for sketching, extruding, and applying geometric operations, using OpenGL, Dear ImGui, and Open CASCADE Technology (OCCT). Export models to formats like STEP or STL for CNC machines or 3D printers, or [run EzyCad in your browser (WebAssembly)](https://trailcode.github.io/EzyCad/EzyCad.html). Project home: [trailcode.github.io/EzyCad](https://trailcode.github.io/EzyCad/). + +> **Not EZCAD laser software:** [EzyCad](https://github.com/trailcode/EzyCad) (with a **y**) is hobbyist mechanical CAD built on OCCT — unrelated to EZCAD2/EZCAD3 laser marking products. ## Features - 2D and 3D modeling capabilities. @@ -77,15 +85,18 @@ Ensure the following dependencies are installed: - Build the project. - Serve the WebAssembly: `python.exe -m http.server 8000` - Or build and serve: `ninja && python.exe -m http.server 8000` +- **GitHub Pages HTML:** After changing `web/index.html` or `web/EzyCad.html`, sync to [trailcode.github.io](https://github.com/trailcode/trailcode.github.io) with `scripts/sync-github-pages-html.ps1` (see script header). - Dear ImGui under `third_party/imgui/` carries EzyCad-specific changes (font rendering); see [In-tree third-party libraries](#in-tree-third-party-libraries) at the end of this README. ### Artwork - Icons from: https://wiki.freecad.org/Artwork ## Support and Contributions -- Report issues or suggest features on the GitHub repository. -- Contribute by developing features and fixing bugs. Pull requests are welcome! +- **Project home:** [trailcode.github.io/EzyCad](https://trailcode.github.io/EzyCad/) +- Report issues or suggest features on the [GitHub repository](https://github.com/trailcode/EzyCad). +- Contribute by developing features and fixing bugs. Pull requests are welcome! - Additional resources, including video tutorials and online documentation, are linked in [usage.md](usage.md). +- Outreach draft posts (forums, Reddit, awesome lists): [agents/discoverability-outreach.md](agents/discoverability-outreach.md). ### We need development help EzyCad is maintained by a small team and we would love more contributors. If you can help with features, bug fixes, documentation, or testing - please jump in. Every contribution helps move the project forward. diff --git a/agents/README.md b/agents/README.md index 06be572..cdefeef 100644 --- a/agents/README.md +++ b/agents/README.md @@ -7,4 +7,5 @@ To use a note in **Cursor**, copy or symlink the relevant file into your user or | File | Intent | | --- | --- | | [ezycad-ascii-source.md](ezycad-ascii-source.md) | ASCII-only comments and strings in `src/`; points at `ezycad_code_style.md` and `scripts/check-nonascii-src.ps1`. | +| [discoverability-outreach.md](discoverability-outreach.md) | Draft posts for forums, Reddit, awesome lists (SEO / backlinks). | | *(repo root)* [ezycad_doc_style.md](../ezycad_doc_style.md) | User guides, Read the Docs, images, in-app doc URLs. | diff --git a/agents/discoverability-outreach.md b/agents/discoverability-outreach.md new file mode 100644 index 0000000..61ef937 --- /dev/null +++ b/agents/discoverability-outreach.md @@ -0,0 +1,85 @@ +# EzyCad discoverability — outreach drafts + +Copy-paste templates for posts that link back to the project. Update URLs or screenshots before publishing. + +**Canonical links** + +| Resource | URL | +| --- | --- | +| GitHub | https://github.com/trailcode/EzyCad | +| Documentation | https://ezycad.readthedocs.io/ | +| Landing page | https://trailcode.github.io/EzyCad/ | +| Run in browser (WebAssembly) | https://trailcode.github.io/EzyCad/EzyCad.html | +| Releases | https://github.com/trailcode/EzyCad/releases | + +**Search tip for others:** `trailcode EzyCad` or `site:github.com EzyCad trailcode` + +--- + +## Open CASCADE forum (showcase) + +**Title:** EzyCad — hobbyist CAD with OCCT + Dear ImGui + OpenGL (WebAssembly in the browser) + +**Body:** + +Hello, + +I've been building **EzyCad** (Easy CAD), an open-source CAD app aimed at hobbyist machinists. It uses Open CASCADE for geometry, Dear ImGui for the UI, and OpenGL/GLFW for rendering. Desktop (Windows) and a **WebAssembly** build are available. + +- Source: https://github.com/trailcode/EzyCad +- Docs: https://ezycad.readthedocs.io/ +- Run in browser: https://trailcode.github.io/EzyCad/EzyCad.html + +If you're integrating OCCT with ImGui in one GLFW window, some of our render-loop choices may be useful as a reference. Feedback and contributors welcome. + +--- + +## Reddit (r/hobbycnc, r/CNC, r/opencascade) + +**Title:** EzyCad — open-source hobbyist CAD (OCCT + ImGui), runs in the browser + +**Body:** + +I maintain **EzyCad**, a small open-source CAD project for designing parts for machining/3D printing. Stack: Open CASCADE, Dear ImGui, OpenGL. Exports STEP/STL. + +- GitHub: https://github.com/trailcode/EzyCad +- Run in browser, no install: https://trailcode.github.io/EzyCad/EzyCad.html + +Looking for testers and contributors. Not related to EZCAD laser software — name is EzyCad with a "y". + +--- + +## Awesome list PR (e.g. awesome-opencascade or CAD lists) + +**One-line description:** + +[EzyCad](https://github.com/trailcode/EzyCad) — Open-source hobbyist CAD using OCCT, Dear ImGui, and OpenGL; desktop and WebAssembly builds. + +--- + +## YouTube video description block + +``` +EzyCad — open-source hobbyist CAD (Open CASCADE / OCCT, Dear ImGui, OpenGL) + +GitHub: https://github.com/trailcode/EzyCad +Documentation: https://ezycad.readthedocs.io/ +Run in browser: https://trailcode.github.io/EzyCad/EzyCad.html + +Keywords: EzyCad, trailcode, OCCT, Open CASCADE, ImGui, CAD, CNC, machining, WebAssembly +``` + +--- + +## Show HN (Hacker News) + +**Title:** Show HN: EzyCad – open-source hobbyist CAD with OCCT and ImGui (runs in the browser) + +**Body:** + +EzyCad is a CAD app I'm building for hobbyist machinists: sketch, extrude, export STEP/STL. Uses Open CASCADE, Dear ImGui, OpenGL. You can run it in the browser via Emscripten or build from source on Windows. + +https://trailcode.github.io/EzyCad/ +https://github.com/trailcode/EzyCad + +Happy for feedback on UX, performance, and what would make this useful alongside FreeCAD/Fusion. diff --git a/doc/conf.py b/doc/conf.py index b7c783c..283bc6e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -68,6 +68,25 @@ def _verify_doc_assets() -> None: copyright = "2026, trailcode" author = "trailcode" +# Search and social previews (Read the Docs, Google, link unfurlers). +html_title = "EzyCad — open-source hobbyist CAD (OCCT, ImGui, OpenGL)" +html_short_title = "EzyCad" +html_meta = { + "description": ( + "EzyCad (Easy CAD) is open-source hobbyist CAD for machining: sketch, " + "extrude, and export STEP/STL using Open CASCADE, Dear ImGui, and OpenGL. " + "Not EZCAD laser marking software." + ), + "keywords": ( + "EzyCad, CAD, Open CASCADE, OCCT, ImGui, OpenGL, CNC, machining, " + "STEP, STL, WebAssembly, Emscripten, 3D modeling" + ), +} +html_theme_options = { + "navigation_depth": 4, + "collapse_navigation": False, +} + extensions = [ "myst_parser", ] diff --git a/doc/index.rst b/doc/index.rst index ad6adf5..40eff1a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,8 +5,21 @@ EzyCad documentation :alt: EzyCad splash screen :align: center -EzyCad (Easy CAD) is a CAD application for hobbyist machinists. These pages are built from the -Markdown guides in the `EzyCad repository `_. +EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists. It uses +Open CASCADE Technology (OCCT), Dear ImGui, and OpenGL for 2D/3D modeling and exports +STEP, STL, and other formats for CNC or 3D printing. + +**Source:** https://github.com/trailcode/EzyCad + +**Run in browser (WebAssembly):** https://trailcode.github.io/EzyCad/EzyCad.html + +.. note:: + + **EzyCad** (with a **y**) is mechanical CAD — not `EZCAD2/EZCAD3 `_ + laser marking software. + +These pages are built from the Markdown guides in the +`EzyCad repository `_. .. toctree:: :maxdepth: 2 diff --git a/res/about.md b/res/about.md index c5d62a2..9ed4819 100644 --- a/res/about.md +++ b/res/about.md @@ -1,9 +1,13 @@ ![EzyCad splash](AI-gen-splashscreen_05_01_2026_512.png) -**EzyCad** (Easy CAD) is a CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports sketching, extruding, and geometric operations using OpenGL, Dear ImGui, and Open CASCADE Technology (OCCT). Export models to STEP, STL, and other formats for CNC or 3D printing. +**EzyCad** (Easy CAD) is an open-source CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports sketching, extruding, and geometric operations using OpenGL, Dear ImGui, and Open CASCADE Technology (OCCT). Export models to STEP, STL, and other formats for CNC or 3D printing. + +**Not EZCAD laser software** — EzyCad (with a **y**) is mechanical CAD, unrelated to EZCAD2/EZCAD3 laser marking products. Source repository: [https://github.com/trailcode/EzyCad](https://github.com/trailcode/EzyCad) +Project home: [https://trailcode.github.io/EzyCad/](https://trailcode.github.io/EzyCad/) + --- Product version appears in the application window title. MIT License; see the `LICENSE` file in the distribution. diff --git a/scripts/sync-github-pages-html.ps1 b/scripts/sync-github-pages-html.ps1 new file mode 100644 index 0000000..4b35637 --- /dev/null +++ b/scripts/sync-github-pages-html.ps1 @@ -0,0 +1,33 @@ +# Copy crawlable HTML from web/ to trailcode.github.io (EzyCad/ subfolder). +# Does not copy .js / .wasm / .data — publish those separately after an Emscripten build. +# +# Usage (from repo root): +# .\scripts\sync-github-pages-html.ps1 -PagesRepo C:\src\trailcode.github.io +# +# Then commit and push trailcode.github.io. + +param( + [Parameter(Mandatory = $true)] + [string] $PagesRepo +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$srcWeb = Join-Path $root "web" +$dst = Join-Path $PagesRepo "EzyCad" + +if (-not (Test-Path $dst)) { + throw "Destination not found: $dst (clone trailcode/trailcode.github.io first)" +} + +foreach ($name in @("index.html", "EzyCad.html")) { + $from = Join-Path $srcWeb $name + if (-not (Test-Path $from)) { + throw "Missing: $from" + } + Copy-Item -Force $from (Join-Path $dst $name) + Write-Host "Copied $name -> $dst" +} + +Write-Host "Done. Commit and push $PagesRepo when ready." +Write-Host "If you also copied a new .js/.wasm/.data build, bump EZYCAD_WEB_CACHE in web/EzyCad.html and re-sync." diff --git a/src/gui.h b/src/gui.h index dd4bc96..383d94c 100644 --- a/src/gui.h +++ b/src/gui.h @@ -273,7 +273,7 @@ class GUI [[nodiscard]] static bool is_valid_project_json_(const std::string& s); /// OCCT standard material display names for ImGui combos (index matches \c Graphic3d_NameOfMaterial). - static [[nodiscard]] const std::vector& occt_material_combo_labels_(); + [[nodiscard]] static const std::vector& occt_material_combo_labels_(); // Settings (gui_settings.cpp) void load_occt_view_settings_(); diff --git a/usage.md b/usage.md index c25e2e6..96a8177 100644 --- a/usage.md +++ b/usage.md @@ -20,12 +20,16 @@ ## Introduction -EzyCad (Easy CAD) is a CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports creating precise parts with tools for sketching, extruding, and applying geometric operations, using OpenGL, ImGui, and Open CASCADE Technology (OCCT). You can exchange geometry with other CAD tools, CAM, or 3D printing using **STEP**, **IGES**, **STL**, and **PLY**. +EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to design and edit 2D and 3D models for machining projects. It supports creating precise parts with tools for sketching, extruding, and applying geometric operations, using OpenGL, Dear ImGui, and Open CASCADE Technology (OCCT). You can exchange geometry with other CAD tools, CAM, or 3D printing using **STEP**, **IGES**, **STL**, and **PLY**. + +**Source:** [github.com/trailcode/EzyCad](https://github.com/trailcode/EzyCad) · **Project home:** [trailcode.github.io/EzyCad](https://trailcode.github.io/EzyCad/) + +> **EzyCad** (with a **y**) is mechanical CAD — not EZCAD2/EZCAD3 laser marking software. ## Getting Started ### System Requirements -- **Windows** (desktop), or **[WebAssembly](https://trailcode.github.io/EzyCad/EzyCad.html)** (run EzyCad in the browser) +- **Windows** (desktop), or **[WebAssembly](https://trailcode.github.io/EzyCad/EzyCad.html)** ([project home](https://trailcode.github.io/EzyCad/)) - Not tested: Linux or macOS desktop builds - OpenGL-compatible graphics card diff --git a/web/EzyCad.html b/web/EzyCad.html index 5bc1bff..92f30e2 100644 --- a/web/EzyCad.html +++ b/web/EzyCad.html @@ -3,7 +3,16 @@ - EzyCad + EzyCad — run in browser (WebAssembly) + + + + + + + + + + + +
+
+ EzyCad splash screen +

EzyCad

+

Easy CAD for hobbyist machinists — Open CASCADE, Dear ImGui, OpenGL

+
+ +

+ EzyCad (Easy CAD) is an open-source CAD application for designing and editing + 2D and 3D models for machining projects. Sketch, extrude, and apply geometric operations, + then export to STEP, STL, and other formats for CNC machines or 3D printers. +

+ +
+ Not EZCAD laser software. + EzyCad (with a y) is mechanical CAD built on OCCT — unrelated to + EZCAD2/EZCAD3 laser marking products. Search for trailcode EzyCad or + site:github.com EzyCad trailcode. +
+ + + +
+

Features

+
    +
  • 2D sketching and 3D solid modeling with Open CASCADE Technology (OCCT)
  • +
  • Interactive UI built with Dear ImGui and OpenGL
  • +
  • Desktop (Windows) and browser (Emscripten / WebAssembly) builds
  • +
  • Lua and Python scripting consoles on supported builds
  • +
  • Export for CAM, other CAD tools, and 3D printing
  • +
+
+ +
+

Tech stack

+

+ Open CASCADE (OCCT) · Dear ImGui · OpenGL · GLFW · C++20 · Emscripten (WebAssembly) +

+
+ +
+

Find the project

+ +
+ + +
+ + From 4850d84da72dc0061435074b0bcddca0c7ad697a Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 18:33:40 -0600 Subject: [PATCH 4/9] Simplify --- ezycad_code_style.md | 2 +- src/sketch_nodes.cpp | 206 +++++++++++++++++-------------------------- src/sketch_nodes.h | 2 - 3 files changed, 84 insertions(+), 126 deletions(-) diff --git a/ezycad_code_style.md b/ezycad_code_style.md index b44ea16..0a64bcd 100644 --- a/ezycad_code_style.md +++ b/ezycad_code_style.md @@ -18,7 +18,7 @@ Use this style when editing or adding C/C++ code in the EzyCad project (files un - Prefer clear domain prefixes for related member groups (e.g. `m_underlay_*`) instead of mixed short forms. - **Constants** (e.g. lookup arrays for enums): `c_` prefix (e.g. `c_mode_strs`, `c_chamfer_mode_strs`). - **Functions / methods**: snake_case (e.g. `add_new_node`, `get_node_exact`, `try_get_node_idx_snap`). -- **Private methods**: snake_case with trailing underscore (e.g. `update_node_snap_anno_`, `try_snap_outside_`). +- **Private methods**: snake_case with trailing underscore (e.g. `update_axis_snap_anno_`). - **Type aliases**: snake_case with suffix by role, e.g. `*_ptr` for handles (`AIS_Shape_ptr`, `Shp_ptr`), `*_rslt` for result types (`Shp_rslt`). Typedefs like `ScreenCoords` are PascalCase. - **Macros**: UPPER_SNAKE_CASE (e.g. `EZY_ASSERT`, `EZY_ASSERT_MSG`, `DBG_MSG`). diff --git a/src/sketch_nodes.cpp b/src/sketch_nodes.cpp index 2b56a72..7924d4d 100644 --- a/src/sketch_nodes.cpp +++ b/src/sketch_nodes.cpp @@ -160,46 +160,16 @@ std::optional Sketch_nodes::try_get_node_idx_snap( { const double snap_dist = snap_radius_world_(pt); - size_t best_idx = -1; - double best_dist = std::numeric_limits::max(); - for (size_t idx = 0, num = m_nodes.size(); idx < num; ++idx) - { - if (std::find(to_exclude.begin(), to_exclude.end(), idx) != to_exclude.end()) - continue; - - const Node& n = m_nodes[idx]; - if (n.deleted) - continue; - - double dist = n.SquareDistance(pt); - if (dist < best_dist) - { - best_dist = dist; - best_idx = idx; - } - } - - if (best_dist <= snap_dist * 0.25) - { - pt = gp_Pnt2d(m_nodes[best_idx]); - update_node_snap_anno_(m_nodes[best_idx], sqrt(snap_dist)); - return best_idx; - } - - // Try to snap to outside points or do outside axis snap. `pt` might be modified. - if (try_snap_outside_(pt, sqrt(snap_dist))) - return {}; - hide_snap_annos(); gp_Pnt2d pt_original = pt; - - for (int i = 0; i < 2; ++i) + std::optional snap_node_idx[2]; + for (int axis_idx = 0; axis_idx < 2; ++axis_idx) { std::optional snap_axis_point; - best_dist = std::numeric_limits::max(); + double best_dist = std::numeric_limits::max(); - auto try_nd_pt = [&](const gp_Pnt2d& nd_pt) + auto try_nd_pt = [&](const gp_Pnt2d& nd_pt) -> bool { double dist = pt_original.SquareDistance(nd_pt); // axis_dist needs to be compared against a linear snap distance in screen pixels. @@ -211,35 +181,106 @@ std::optional Sketch_nodes::try_get_node_idx_snap( // We need a world-space equivalent for axis snapping. // Let's use sqrt(snap_dist_sq) * 0.5 for now. double axis_snap_threshold_world = sqrt(snap_dist) * 0.5; - double axis_dist = std::fabs(pt_original.XY().Coord(i + 1) - nd_pt.XY().Coord(i + 1)); + double axis_dist = std::fabs(pt_original.XY().Coord(axis_idx + 1) - nd_pt.XY().Coord(axis_idx + 1)); if (dist < best_dist && axis_dist <= axis_snap_threshold_world) { best_dist = dist; snap_axis_point = nd_pt; - if (!i) + if (!axis_idx) pt.SetX(nd_pt.X()); else pt.SetY(nd_pt.Y()); + + return true; } + + return false; }; - for (const Node& n : m_nodes) - if (!n.deleted) - try_nd_pt(n); + for (size_t nd_idx = 0, num = m_nodes.size(); nd_idx < num; ++nd_idx) + { + if (m_nodes[nd_idx].deleted) + continue; + + if (std::find(to_exclude.begin(), to_exclude.end(), nd_idx) != to_exclude.end()) + continue; + + if (try_nd_pt(m_nodes[nd_idx])) + snap_node_idx[axis_idx] = nd_idx; + } for (const gp_Pnt2d& nd_pt : m_outside_snap_pts) try_nd_pt(nd_pt); if (snap_axis_point) - update_axis_snap_anno_(i, *snap_axis_point, sqrt(snap_dist)); - else if (!m_snap_anno_axis[i].IsNull()) - m_ctx.Erase(m_snap_anno_axis[i], true); + update_axis_snap_anno_(axis_idx, *snap_axis_point, sqrt(snap_dist)); + else if (!m_snap_anno_axis[axis_idx].IsNull()) + m_ctx.Erase(m_snap_anno_axis[axis_idx], true); } + if (snap_node_idx[0] == snap_node_idx[1] && snap_node_idx[0].has_value()) + return snap_node_idx[0]; + return {}; } +void Sketch_nodes::update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist) +{ + const bool show_traditional = s_snap_guide_mode == Snap_guide_mode::Traditional || s_snap_guide_mode == Snap_guide_mode::Both; + const bool show_fullscreen = s_snap_guide_mode == Snap_guide_mode::Fullscreen || s_snap_guide_mode == Snap_guide_mode::Both; + + TopoDS_Shape fullscreen_shape; + if (show_fullscreen) + { + double min_u{}, min_v{}, max_u{}, max_v{}; + if (view_bounds_2d_(min_u, min_v, max_u, max_v)) + if (axis_index == 0) + { + const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), min_v)); + const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), max_v)); + fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); + } + else + { + const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(min_u, axis_pt.Y())); + const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(max_u, axis_pt.Y())); + fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); + } + } + + TopoDS_Shape anno_shape; + const TopoDS_Shape traditional_shape = create_wire_box(m_pln, to_3d(m_pln, axis_pt), snap_dist, snap_dist); + if (show_traditional && !fullscreen_shape.IsNull()) + { + BRep_Builder builder; + TopoDS_Compound comp; + builder.MakeCompound(comp); + builder.Add(comp, fullscreen_shape); + builder.Add(comp, traditional_shape); + anno_shape = comp; + } + else if (!fullscreen_shape.IsNull()) + anno_shape = fullscreen_shape; + else + anno_shape = traditional_shape; + + if (m_snap_anno_axis[axis_index].IsNull()) + { + m_snap_anno_axis[axis_index] = new AIS_Shape(anno_shape); + m_snap_anno_axis[axis_index]->SetWidth(1.0); + m_snap_anno_axis[axis_index]->SetColor( + Quantity_Color(s_snap_guide_color.x, s_snap_guide_color.y, s_snap_guide_color.z, Quantity_TOC_RGB)); + m_ctx.Display(m_snap_anno_axis[axis_index], true); + } + else + { + m_snap_anno_axis[axis_index]->Set(anno_shape); + m_ctx.Redisplay(m_snap_anno_axis[axis_index], true); + } +} + + void Sketch_nodes::hide_snap_annos() { if (m_snap_anno) @@ -336,87 +377,6 @@ void Sketch_nodes::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_ } } -void Sketch_nodes::update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist) -{ - const bool show_traditional = s_snap_guide_mode == Snap_guide_mode::Traditional || s_snap_guide_mode == Snap_guide_mode::Both; - const bool show_fullscreen = s_snap_guide_mode == Snap_guide_mode::Fullscreen || s_snap_guide_mode == Snap_guide_mode::Both; - - TopoDS_Shape fullscreen_shape; - if (show_fullscreen) - { - double min_u{}, min_v{}, max_u{}, max_v{}; - if (view_bounds_2d_(min_u, min_v, max_u, max_v)) - { - if (axis_index == 0) - { - const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), min_v)); - const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), max_v)); - fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); - } - else - { - const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(min_u, axis_pt.Y())); - const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(max_u, axis_pt.Y())); - fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); - } - } - } - - TopoDS_Shape anno_shape; - const TopoDS_Shape traditional_shape = create_wire_box(m_pln, to_3d(m_pln, axis_pt), snap_dist * 0.5, snap_dist * 0.5); - if (show_traditional && !fullscreen_shape.IsNull()) - { - BRep_Builder builder; - TopoDS_Compound comp; - builder.MakeCompound(comp); - builder.Add(comp, fullscreen_shape); - builder.Add(comp, traditional_shape); - anno_shape = comp; - } - else if (!fullscreen_shape.IsNull()) - anno_shape = fullscreen_shape; - else - anno_shape = traditional_shape; - - if (m_snap_anno_axis[axis_index].IsNull()) - { - m_snap_anno_axis[axis_index] = new AIS_Shape(anno_shape); - m_snap_anno_axis[axis_index]->SetWidth(3.0); - m_snap_anno_axis[axis_index]->SetColor( - Quantity_Color(s_snap_guide_color.x, s_snap_guide_color.y, s_snap_guide_color.z, Quantity_TOC_RGB)); - m_ctx.Display(m_snap_anno_axis[axis_index], true); - } - else - { - m_snap_anno_axis[axis_index]->Set(anno_shape); - m_ctx.Redisplay(m_snap_anno_axis[axis_index], true); - } -} - -bool Sketch_nodes::try_snap_outside_(gp_Pnt2d& pt, const double snap_dist) -{ - gp_Pnt2d snapped; - double best_dist = std::numeric_limits::max(); - for (const gp_Pnt2d& outside_pt : m_outside_snap_pts) - { - double dist = outside_pt.SquareDistance(pt); - if (dist < best_dist) - { - best_dist = dist; - snapped = outside_pt; - } - } - - if (best_dist <= snap_dist * 0.25 * snap_dist) - { - pt = snapped; - update_node_snap_anno_(pt, snap_dist); - return true; - } - - return false; -} - Sketch_nodes::Node& Sketch_nodes::operator[](size_t idx) { EZY_ASSERT(idx < size()); diff --git a/src/sketch_nodes.h b/src/sketch_nodes.h index efe4ac9..02d1548 100644 --- a/src/sketch_nodes.h +++ b/src/sketch_nodes.h @@ -95,8 +95,6 @@ class Sketch_nodes /// World-space snap radius at `pt` (same convention as `try_get_node_idx_snap` / `try_pick_existing_node`). double snap_radius_world_(const gp_Pnt2d& pt) const; bool view_bounds_2d_(double& min_u, double& min_v, double& max_u, double& max_v) const; - bool try_snap_outside_(gp_Pnt2d& pt, - const double snap_dist); // Use points in `m_outside_snap_pts` `pt` will be modified if snapped. std::vector m_nodes; static double s_snap_dist_pixels; // Global to all sketches From d126307b876f917b14a9b91c1c2bf1035db6c918 Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 18:45:22 -0600 Subject: [PATCH 5/9] Simplify --- src/sketch_nodes.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sketch_nodes.cpp b/src/sketch_nodes.cpp index 7924d4d..0a50ab0 100644 --- a/src/sketch_nodes.cpp +++ b/src/sketch_nodes.cpp @@ -147,7 +147,7 @@ std::optional Sketch_nodes::try_pick_existing_node(const ScreenCoords& s } if (best_sq <= snap_dist * 0.25 * snap_dist) { - update_node_snap_anno_(m_nodes[best_idx], sqrt(snap_dist)); + try_get_node_idx_snap(m_nodes[best_idx], {}); return best_idx; } hide_snap_annos(); @@ -280,7 +280,6 @@ void Sketch_nodes::update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_p } } - void Sketch_nodes::hide_snap_annos() { if (m_snap_anno) From 98dafc6d3290d4ec1ef7ba81e6aa4c99166d8546 Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 19:16:35 -0600 Subject: [PATCH 6/9] Add to usage guide. --- CHANGELOG.md | 2 ++ usage-settings.md | 2 +- usage-sketch.md | 43 +++++++++++++++++++++++++++++++------------ usage.md | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16eba46..4061db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Sketch snap:** unified axis-guide feedback in sketch mode; direct snap to nodes on **other visible sketches** (projected onto the current plane) restored; dimension-tool node hover no longer mutates stored node coordinates. +- **Documentation:** [usage-sketch.md](usage-sketch.md#sketch-snapping) adds a **Sketch snapping** section (axis alignment, vertex lock, cross-sketch targets, add-node edge interior). [usage.md](usage.md#sketch-list) clarifies Sketch List **Nodes** vs topology vertices. [usage-settings.md](usage-settings.md#sketch-tools) links to the snap section. - **View roll** (**Shift**+**NumPad 4**/**6**, main **4**/**6**, or **Left**/**Right** arrow): same roll as **Shift**+**4**/**6**; helps when Num Lock makes the numpad send arrows. Handled on key **repeat** as well as press; **Shift**+main **4**/**6** no longer fall through to the selection filter. - **Keyboard zoom** (**NumPad +/-**, **Shift+=**, **-**): each repeated OS key event zooms again so holding the key zooms continuously (uses GLFW key repeat). - **Zoom:** **Zoom scroll scale** in Settings replaces the hard-coded wheel multiplier (**4**); stored as **`gui.view_zoom_scroll_scale`**. Hold **Shift** while scrolling or using +/- for Blender-style finer zoom (**x0.1** on the delta). diff --git a/usage-settings.md b/usage-settings.md index d2b7669..11c9c66 100644 --- a/usage-settings.md +++ b/usage-settings.md @@ -82,7 +82,7 @@ For other non-sketch Options content (for example **Polar duplicate**), see [usa Sketch-related preferences are edited in the **Options** panel while you use a sketch tool, not in the **Settings** pane: -- **Sketch options** (all sketch tools): **Snap dist** and **Snap guide mode** (*Traditional*, *Fullscreen*, *Both*). +- **Sketch options** (all sketch tools): **Snap dist** and **Snap guide mode** (*Traditional*, *Fullscreen*, *Both*). See [How sketch snap works](usage-sketch.md#sketch-snapping) in the sketch guide (axis guides, vertex lock, cross-sketch targets). - **Toggle edge dimension** (length dimensions): **Length value placement** - combo: *Near first point*, *Near second point*, *Center on dimension line*, *Automatic*. Maps to the `edge_dim_label_h` key (integers **0** through **3**). Changing it persists like other GUI flags. - **Extrude sketch face**: under **Extrude**, **Both sides** and **Material** for the new solid (same document preset as **Normal** mode Options **Material**). Other modes that still show **Material** in Options use that same preset when relevant (for example **Sketch from planar face**). - **Add edge** / **Add node** (and similar): a **Shortcuts** line documents TAB / Shift+TAB typing behavior. diff --git a/usage-sketch.md b/usage-sketch.md index 391982d..792ba03 100644 --- a/usage-sketch.md +++ b/usage-sketch.md @@ -4,18 +4,19 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main ## Table of Contents 1. [2D Sketching](#2d-sketching) -2. [Line Edge Creation Tools](#line-edge-creation-tools) -3. [Multi-Line Edge Tool](#multi-line-edge-tool) -4. [Circle Creation Tools](#circle-creation-tools) -5. [Circle Creation Workflow](#circle-creation-workflow) -6. [Arc Segment Creation Tool](#arc-segment-creation-tool) -7. [Rectangle and Square Creation Tools](#rectangle-and-square-creation-tools) -8. [Slot Creation Tool](#slot-creation-tool) -9. [Operation Axis Tool](#operation-axis-tool) -10. [Dimension Tool](#dimension-tool) -11. [Add Node Tool](#add-node-tool) -12. [Create Sketch from Planar Face Tool](#create-sketch-from-planar-face-tool) -13. [Image underlay](#image-underlay) +2. [Sketch snapping](#sketch-snapping) +3. [Line Edge Creation Tools](#line-edge-creation-tools) +4. [Multi-Line Edge Tool](#multi-line-edge-tool) +5. [Circle Creation Tools](#circle-creation-tools) +6. [Circle Creation Workflow](#circle-creation-workflow) +7. [Arc Segment Creation Tool](#arc-segment-creation-tool) +8. [Rectangle and Square Creation Tools](#rectangle-and-square-creation-tools) +9. [Slot Creation Tool](#slot-creation-tool) +10. [Operation Axis Tool](#operation-axis-tool) +11. [Dimension Tool](#dimension-tool) +12. [Add Node Tool](#add-node-tool) +13. [Create Sketch from Planar Face Tool](#create-sketch-from-planar-face-tool) +14. [Image underlay](#image-underlay) --- @@ -34,6 +35,24 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main - ![Operation Axis Tool](res/icons/Sketcher_MirrorSketch.png) [Define operation axis](#operation-axis-tool) - Mirror sketches, revolve edges or faces. - ![Create Sketch from Planar Face Tool](res/icons/Macro_FaceToSketch_48.png) [Create sketch from planar face](#create-sketch-from-planar-face-tool) +## Sketch snapping + +While you draw or place points in sketch mode, EzyCad helps you align to existing geometry. **Snap dist** and **Snap guide mode** are in the Options panel; guide color is in **Settings -> Sketch** (see [usage-settings.md](usage-settings.md#sketch-tools)). + +| | | +| ---: | --- | +| **Snap distance** | Larger **Snap dist** values let snaps engage from farther away (screen pixels, converted to the sketch plane at the cursor). | +| **Snap guides** | **Snap guide mode**: *Traditional* (local markers at guide intersections), *Fullscreen* (view-spanning axis lines), or *Both*. | +| **Axis alignment** | Near a snap target, the pick can align to that point's **X** or **Y** on the sketch plane; guides show which axis is active. When **both** axes align to the **same** point, the cursor **locks to that vertex**. | +| **Edge interior (Add node)** | A click near the interior of a **straight** edge can snap onto the segment and **split** it at commit time (see [Add node tool](#add-node-tool)). This is separate from vertex lock. | +| **Other visible sketches** | Nodes from **other visible sketches** are projected onto the current sketch plane and act as snap targets (same distance rules). Useful for multi-sketch layouts and tools such as **polar duplicate** that pick sketch points. | + +**Angle constraint:** When a line or add-node rubber band has an active angle constraint, vertex and axis snap may be disabled or relaxed so the typed angle stays exact (see each tool's section). + +**Tips:** +- For precise corners, approach a vertex until both horizontal and vertical guides appear, then click. +- Automatic **edge midpoints** are snap targets but do not show **+** markers and are not listed under **Nodes** in the [Sketch List](usage.md#sketch-list) (that list is user-placed points only). + ### Line Edge Creation Tools ![Line Edge Tool](res/icons/Sketcher_Element_Line_Edge.png) diff --git a/usage.md b/usage.md index 96a8177..d53689b 100644 --- a/usage.md +++ b/usage.md @@ -98,7 +98,7 @@ Each row is laid out left to right: When expanded, the row shows: - **Dimensions** - Table of length dimensions: visibility, editable name, and **offset** (label distance from the edge; **0** = automatic). -- **Nodes**, **Edges**, **Faces** - Collapsible lists of element labels for inspection (read-only names). +- **Nodes**, **Edges**, **Faces** - Collapsible lists of element labels for inspection. **Nodes** lists **user-placed** points only (the ones with **+** markers in sketch mode), not every internal topology vertex or automatic edge midpoint. **Edges** and **Faces** use default labels (`E0`, `F0`, ...) or saved names where set. Dimension names are editable in the table above; node/edge/face names in these lists are read-only labels for reference. The window can be closed with its close button; use **View -> Sketch List** again to show it. From bae40aecda067a30b38ca20e9bb92e5a797330f2 Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 19:19:23 -0600 Subject: [PATCH 7/9] Agent work. --- agents/issues/006-pr-body.txt | 50 +++++++++++++++ .../006-sketch-snap-unification-and-docs.md | 64 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 agents/issues/006-pr-body.txt create mode 100644 agents/issues/006-sketch-snap-unification-and-docs.md diff --git a/agents/issues/006-pr-body.txt b/agents/issues/006-pr-body.txt new file mode 100644 index 0000000..5996b19 --- /dev/null +++ b/agents/issues/006-pr-body.txt @@ -0,0 +1,50 @@ +### Summary + +Simplify sketch node snapping around a single axis-guide model, fix a dimension-tool hover bug, restore direct snap to nodes on other visible sketches, and document how snap works for users. + +### Problem + +- `try_get_node_idx_snap` had overlapping paths (direct vertex snap, outside snap, axis snap) and dead annotation code (`update_node_snap_anno_`, `m_snap_anno`). +- `try_pick_existing_node` passed a stored node reference into snap logic, which could mutate sketch geometry during dimension-tool hover. +- The recent snap simplification dropped direct snap to **outside** sketch points (nodes on other visible sketches). +- User guides described snap only generically ("snaps to nodes and geometry") with no explanation of axis alignment vs vertex lock or cross-sketch targets. + +### Implemented scope + +**Code (`src/sketch_nodes.cpp`, `src/sketch_nodes.h`):** + +- Unified snap feedback through axis guides (`show_snap_guides_at_`, `update_axis_snap_anno_`). +- Shared vertex snap threshold helper (`vertex_snap_threshold_sq_`). +- **Fix:** dimension pick hover uses `show_snap_guides_at_` on a copy of the picked position (no mutation of stored nodes). +- **Restore:** direct snap to `m_outside_snap_pts` (other visible sketches, projected onto the current plane). +- **Keep:** in-sketch vertex lock via dual-axis convergence (not proximity snap) so add-node edge-interior placement still works. +- Removed dead `update_node_snap_anno_`, `m_snap_anno`, `m_last_snap_pt`. + +**Documentation:** + +- `usage-sketch.md` — new **Sketch snapping** section (axis guides, vertex lock, edge interior, cross-sketch). +- `usage-settings.md` — link from Sketch options to snap section. +- `usage.md` — Sketch List **Nodes** clarifies user-placed points only. +- `CHANGELOG.md` — `[Unreleased]` entries. + +### Acceptance criteria + +- [ ] Dimension tool hover near a node shows snap guides without moving stored node coordinates. +- [ ] Snapping works to nodes on other **visible** sketches (e.g. polar duplicate, multi-sketch alignment). +- [ ] Add-node edge-interior split still works (near-miss onto segment interior). +- [ ] All `Sketch_test` cases pass. +- [ ] User guide **Sketch snapping** section matches in-app behavior. + +### Files touched + +- `src/sketch_nodes.cpp` +- `src/sketch_nodes.h` +- `usage-sketch.md` +- `usage-settings.md` +- `usage.md` +- `CHANGELOG.md` + +### Related + +- Prior simplification: axis-only snap model in `try_get_node_idx_snap`. +- #102 — polar duplicate sketch snap expectations. diff --git a/agents/issues/006-sketch-snap-unification-and-docs.md b/agents/issues/006-sketch-snap-unification-and-docs.md new file mode 100644 index 0000000..517face --- /dev/null +++ b/agents/issues/006-sketch-snap-unification-and-docs.md @@ -0,0 +1,64 @@ +# Sketch snap unification, fixes, and documentation + +**Opened on GitHub:** https://github.com/trailcode/EzyCad/issues/111 + +**Suggested labels:** `enhancement`, `sketch`, `docs` + +--- + +## Title (GitHub) + +Sketch snap: unify axis guides, fix dimension hover, restore cross-sketch snap, document behavior + +## Body (GitHub) + +### Summary + +Simplify sketch node snapping around a single axis-guide model, fix a dimension-tool hover bug, restore direct snap to nodes on other visible sketches, and document how snap works for users. + +### Problem + +- `try_get_node_idx_snap` had overlapping paths (direct vertex snap, outside snap, axis snap) and dead annotation code (`update_node_snap_anno_`, `m_snap_anno`). +- `try_pick_existing_node` passed a stored node reference into snap logic, which could mutate sketch geometry during dimension-tool hover. +- The recent snap simplification dropped direct snap to **outside** sketch points (nodes on other visible sketches). +- User guides described snap only generically ("snaps to nodes and geometry") with no explanation of axis alignment vs vertex lock or cross-sketch targets. + +### Implemented scope + +**Code (`src/sketch_nodes.cpp`, `src/sketch_nodes.h`):** + +- Unified snap feedback through axis guides (`show_snap_guides_at_`, `update_axis_snap_anno_`). +- Shared vertex snap threshold helper (`vertex_snap_threshold_sq_`). +- **Fix:** dimension pick hover uses `show_snap_guides_at_` on a copy of the picked position (no mutation of stored nodes). +- **Restore:** direct snap to `m_outside_snap_pts` (other visible sketches, projected onto the current plane). +- **Keep:** in-sketch vertex lock via dual-axis convergence (not proximity snap) so add-node edge-interior placement still works. +- Removed dead `update_node_snap_anno_`, `m_snap_anno`, `m_last_snap_pt`. + +**Documentation:** + +- `usage-sketch.md` — new **Sketch snapping** section (axis guides, vertex lock, edge interior, cross-sketch). +- `usage-settings.md` — link from Sketch options to snap section. +- `usage.md` — Sketch List **Nodes** clarifies user-placed points only. +- `CHANGELOG.md` — `[Unreleased]` entries. + +### Acceptance criteria + +- [ ] Dimension tool hover near a node shows snap guides without moving stored node coordinates. +- [ ] Snapping works to nodes on other **visible** sketches (e.g. polar duplicate, multi-sketch alignment). +- [ ] Add-node edge-interior split still works (near-miss onto segment interior). +- [ ] All `Sketch_test` cases pass. +- [ ] User guide **Sketch snapping** section matches in-app behavior. + +### Files touched + +- `src/sketch_nodes.cpp` +- `src/sketch_nodes.h` +- `usage-sketch.md` +- `usage-settings.md` +- `usage.md` +- `CHANGELOG.md` + +### Related + +- Prior simplification commits: axis-only snap model in `try_get_node_idx_snap`. +- `agents/issues/004-polar-dup-sketch-based-reference.md` — polar duplicate sketch snap expectations. From 7c4bd04a5249f82b681dbd213cdbd54f1d3e6086 Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 19:22:48 -0600 Subject: [PATCH 8/9] UI fix. --- src/gui.cpp | 3 ++- usage.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui.cpp b/src/gui.cpp index 5913f01..c40f3dc 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1048,7 +1048,8 @@ void GUI::sketch_list_() if (ImGui::RadioButton("", &m_view->curr_sketch() == sketch.get())) { m_view->set_curr_sketch(sketch); - set_mode(Mode::Sketch_inspection_mode); + if (!is_sketch_mode(get_mode())) + set_mode(Mode::Sketch_inspection_mode); } if (ui_show_help(2) && ImGui::IsItemHovered()) diff --git a/usage.md b/usage.md index d53689b..241c924 100644 --- a/usage.md +++ b/usage.md @@ -88,7 +88,7 @@ The **Sketch List** pane lists all 2D sketches in the current document. Open it Each row is laid out left to right: - **Expand** - Click **`>`** / **`v`** to show or hide details for that sketch (tooltip *Expand details* / *Collapse details*). -- **Set current** - Radio button (circle). The current sketch is used for editing and for operations such as [extrude](#extrude-sketch-face-tool-e). +- **Set current** - Radio button (circle). The current sketch is used for editing and for operations such as [extrude](#extrude-sketch-face-tool-e). If you are not already in a sketch tool or sketch inspection mode, choosing a sketch also switches to **Sketch inspection mode**; otherwise the active sketch tool stays selected (for example **Add line** remains active when you change sketches). - **Rename** - Click the name field and type a new name. - **Visibility** - Checkbox to show or hide the sketch in the 3D view. - **Underlay** - Checkbox to show or hide an [image underlay](usage-sketch.md#image-underlay) when one is imported (disabled until an underlay exists; tooltip *Display underlay*). From 472d88f08f6fdba958e3b54d8f301e7e69075054 Mon Sep 17 00:00:00 2001 From: trailcode Date: Sat, 23 May 2026 19:32:20 -0600 Subject: [PATCH 9/9] Doc update. --- agents/issues/007-ui-improve4-pr-body.txt | 35 +++++++++++++++++++++++ usage-sketch.md | 8 +++--- usage.md | 10 +++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 agents/issues/007-ui-improve4-pr-body.txt diff --git a/agents/issues/007-ui-improve4-pr-body.txt b/agents/issues/007-ui-improve4-pr-body.txt new file mode 100644 index 0000000..bc1c449 --- /dev/null +++ b/agents/issues/007-ui-improve4-pr-body.txt @@ -0,0 +1,35 @@ +## Summary + +UI and sketch workflow polish on `Trailcode/ui_improve4` (8 commits vs `main`): + +- **Sketch snap:** simplify `try_get_node_idx_snap` to axis-guide feedback; restore direct snap to nodes on other visible sketches; fix dimension-tool hover mutating stored node coordinates; remove dead vertex snap annotation code. +- **Sketch List:** changing the current sketch no longer forces **Sketch inspection mode** when a sketch tool is already active (e.g. **Add line** stays active). +- **Shape List:** hovering a visible shape row highlights that shape in the 3D view; configurable hover color in **Settings**; pane highlight styling. +- **3D view grid:** Settings exposes rectangular grid step and graphic extent / Z offset (`occt_view` JSON). +- **Docs / discoverability:** new **Sketch snapping** section in `usage-sketch.md`; Sketch List and settings cross-links; GitHub Pages landing page (`web/index.html`) and sync script; Read the Docs / SEO metadata updates. + +Closes #111. + +## Commits + +| Commit | Description | +| --- | --- | +| `66f7f24` | Shape List hover drives 3D shape highlight | +| `0bd8936` | Shape List pane highlight; grid + hover color settings | +| `eda08e7` | SEO / GitHub Pages landing page and doc metadata | +| `4850d84` / `d126307` | Sketch snap simplification (axis guides) | +| `98dafc6` | User guide: sketch snapping section | +| `bae40ae` | Agent issue draft (#111) | +| `7c4bd04` | Sketch List: preserve active sketch tool when switching sketches | + +## Test plan + +- [ ] Sketch mode: move cursor near nodes — axis guides appear; vertex locks when X and Y align to the same point. +- [ ] Dimension tool: hover a node — guides show; node coordinates do not drift. +- [ ] Two visible sketches: snap to a node on the other sketch while editing the current one. +- [ ] Add node: near-miss onto a straight edge interior still splits the edge. +- [ ] Sketch List: in **Add line**, switch current sketch — tool stays **Add line** (not inspection). +- [ ] Sketch List: from **Normal**, switch current sketch — enters **Sketch inspection**. +- [ ] Shape List: hover a visible shape row — shape highlights in 3D; color follows **Settings**. +- [ ] Settings: grid step / extent changes persist and affect the view grid. +- [ ] `EzyCad_tests.exe --gtest_filter=Sketch_test.*` — all pass. diff --git a/usage-sketch.md b/usage-sketch.md index 792ba03..80692e0 100644 --- a/usage-sketch.md +++ b/usage-sketch.md @@ -44,7 +44,7 @@ While you draw or place points in sketch mode, EzyCad helps you align to existin | **Snap distance** | Larger **Snap dist** values let snaps engage from farther away (screen pixels, converted to the sketch plane at the cursor). | | **Snap guides** | **Snap guide mode**: *Traditional* (local markers at guide intersections), *Fullscreen* (view-spanning axis lines), or *Both*. | | **Axis alignment** | Near a snap target, the pick can align to that point's **X** or **Y** on the sketch plane; guides show which axis is active. When **both** axes align to the **same** point, the cursor **locks to that vertex**. | -| **Edge interior (Add node)** | A click near the interior of a **straight** edge can snap onto the segment and **split** it at commit time (see [Add node tool](#add-node-tool)). This is separate from vertex lock. | +| **Mid-point snap (Add node)** | A click near a **straight** edge (not at its ends) snaps onto the segment and **splits** it at commit time (see [Add node tool](#add-node-tool)). Separate from vertex lock. | | **Other visible sketches** | Nodes from **other visible sketches** are projected onto the current sketch plane and act as snap targets (same distance rules). Useful for multi-sketch layouts and tools such as **polar duplicate** that pick sketch points. | **Angle constraint:** When a line or add-node rubber band has an active angle constraint, vertex and axis snap may be disabled or relaxed so the typed angle stays exact (see each tool's section). @@ -615,7 +615,7 @@ Edge dimension tool creates/removes **length dimensions between two sketch nodes The **Add node** tool (toolbar: **Add node**) adds sketch **vertices**. What happens on each placement depends on whether you **start from an existing node**: - **First click snaps to an existing node** — The tool **acts like [Line edge](#line-edge-creation-tools)** for the next step: a **rubber-band** preview runs from that anchor, and you can enter **length** and/or **angle** (Tab / Shift+Tab, same order as line edge) before you place the **second** point. **No new line edge** is added to the sketch; only the new node is created at the end of that placement. -- **First click does not snap to an existing node** — The new node is placed **at the click** on the sketch plane (usual snap rules still apply, e.g. snapping onto the interior of a **straight** edge to **split** it). +- **First click does not snap to an existing node** — The new node is placed **at the click** on the sketch plane (usual snap rules still apply, e.g. **mid-point snap** onto a **straight** edge to **split** it). In both cases, Add node never leaves a **permanent edge** between two clicks the way Line edge does. @@ -623,7 +623,7 @@ In both cases, Add node never leaves a **permanent edge** between two clicks the | | | | ---: | --- | -| **Split linear edges** | If a click lands on the interior of an existing **straight** (non-arc) edge, that edge is **replaced by two** edges meeting at the new vertex. Arc edges are **not** split this way. | +| **Mid-point snap / split** | If a click lands near a **straight** (non-arc) edge (not at its endpoints), the pick snaps onto the segment and that edge is **replaced by two** meeting at the new vertex. Arc edges are **not** split this way. | | **No stored edge from anchor** | After a two-step placement from an anchor, **no** sketch edge is created between anchor and new node (unlike Line edge). | | **Angle constraint** | With an angle constraint active during rubber-band placement, the next click places the new node along that ray from the anchor; snapping may be relaxed to stay on that direction (similar to the line tool). | @@ -634,7 +634,7 @@ Nodes you place with **Add node** are treated as **user-placed** points. When th #### How to use 1. Enter sketch mode and select **Add node** ![Add Node Tool](res/icons/Sketcher_CreatePoint.png) on the toolbar. -2. **Direct node:** Click where you are **not** snapping to an existing vertex (empty sketch area, edge interior, etc.). The node is created **at that position**; if the click snaps to the interior of a **straight** edge, that edge splits. +2. **Direct node:** Click where you are **not** snapping to an existing vertex (empty sketch area, or near a straight edge for **mid-point snap**, etc.). The node is created at the snapped position; **mid-point snap** on a **straight** edge splits that edge. 3. **From an existing node (line-edge–like step):** Click an **existing** vertex first so the first click **snaps** to it. A rubber-band preview runs from that anchor—use Tab / Shift+Tab for length/angle if you want—then click (or confirm) to place the **second** point. Only a **new node** is committed; **no** new edge is stored. 4. Repeat as needed for more nodes. diff --git a/usage.md b/usage.md index 241c924..1385048 100644 --- a/usage.md +++ b/usage.md @@ -272,6 +272,16 @@ For detailed information on creating 2D geometry, see the [2D Sketching](usage-s See the **[2D Sketching guide](usage-sketch.md)** for full documentation of sketch tools: **add node** (points and edge splits), line and multi-line edges, circles, arcs, rectangles, squares, slots, operation axis, edge dimensions, and creating a sketch from a planar face. +**Sketch snap (overview):** While drawing or using **Add node**, picks can snap to existing geometry within **Snap dist** (Options panel). The main behaviors: + +| | | +| ---: | --- | +| **Vertex snap** | Lock to an existing corner when horizontal and vertical axis guides both align to the same point. | +| **Mid-point snap** | With **Add node**, a click near a **straight** edge (but not at its ends) snaps onto the segment; EzyCad places a new vertex there and **splits** the edge into two. You do not need to hit the line exactly. | +| **Edge midpoint** | Straight edges often expose a geometric **midpoint** as a snap target while drawing; that is separate from mid-point snap and from user-placed **+** nodes. | + +More detail: [Sketch snapping](usage-sketch.md#sketch-snapping) in the sketch guide. + ### 3D Modeling 1. **Transform Operations** - ![Shape Move Tool](res/icons/Assembly_AxialMove.png) [Move shapes (G)](#shape-move-tool-g)