diff --git a/CHANGELOG.md b/CHANGELOG.md index fdde891..4ee61f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Add bone** sketch tool (Shift+U): click the two circle centers, then radius 1, radius 2, and waist width (like other sketch tools). Waist is the minimum neck thickness (offset from mid when end radii differ). Live length dim only for center-to-center; radii / holes / waist use the geometry preview (Tab still enters exact values). Options: **Add center nodes** (default on) and **Holes** (**None** / **One radius** / **Two radii**). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs), optional hole circles, and optional permanent **Bone A** / **Bone B** center nodes, then exits to **Sketch inspection** (does not stay in Add bone for another). Remappable as **`mode.add_bone`**. +- **Add bone** sketch tool (Shift+U): click the two circle centers, then radius at the second center, radius at the first center, and waist width (like other sketch tools). Waist is the minimum neck thickness (offset from mid when end radii differ). Live length dim only for center-to-center; radii / holes / waist use the geometry preview (Tab still enters exact values). Options: **Add center nodes** (default on) and **Holes** (**None** / **One radius** / **Two radii**). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs), optional hole circles, and optional permanent **Bone A** / **Bone B** center nodes, then exits to **Sketch inspection** (does not stay in Add bone for another). Remappable as **`mode.add_bone`**. - **Shape local frame**: Shape List right-click on a solid toggles **Show axes** / **Show plane** / **Show up**, **Reset frame to bbox**, **Set from planar/cylindrical face**, and **Flip up** / **Flip axis (Z)**. Frame display flags persist in `.ezy` as `frameDisplay`. Face picks use `Mode::Shape_set_frame` (no toolbar hotkey). diff --git a/agents/README.md b/agents/README.md index 692ca66..3036c38 100644 --- a/agents/README.md +++ b/agents/README.md @@ -9,7 +9,7 @@ Root markers: [AGENTS.md](../AGENTS.md) / [agents.md](../agents.md). | Need | File | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ASCII / `src/` edits | [conventions/ascii-source.md](conventions/ascii-source.md) — after edits: `python scripts/agent_check.py ` | -| C++ style (full) | [docs/ezycad_code_style.md](../docs/ezycad_code_style.md) — optional local: `python scripts/code_style_check.py` (not CI) | +| C++ style (full) | [docs/ezycad_code_style.md](../docs/ezycad_code_style.md) — Vertical rhythm includes a blank line after an early-return `if`; optional local: `python scripts/code_style_check.py` (not CI) | | User docs when UI changes | [conventions/user-docs-sync.md](conventions/user-docs-sync.md) | | Sketch module (dev doc) | [src/doc/sketch.md](../src/doc/sketch.md) — read when editing sketch code; update if API/architecture changes | | Shape module (dev doc) | [src/doc/shape.md](../src/doc/shape.md) — read when editing `shp_*` code; update if API/operations change | diff --git a/docs/ezycad_code_style.md b/docs/ezycad_code_style.md index 470abf0..73da9ae 100644 --- a/docs/ezycad_code_style.md +++ b/docs/ezycad_code_style.md @@ -96,6 +96,7 @@ default: ++result.other_curve_count; break; Not enforced by clang-format. Treat each **logical beat** as its own short paragraph: - Put a **blank line between consecutive `if` statements** that have no `else` (each is its own block). +- After an `if` with no `else` whose then-body **exits** (`return` / `continue` / `break` / `CHK_RET`), put a blank line before the next statement in the same block (the success path). - After a **local lambda or helper** closes (`};`), put a blank line before the first use or following statement. - Inside a short helper, separate **setup**, **early return**, and **main body** with blank lines when each is a distinct step. - After a multi-statement `if { ... }` (or single-statement `if` that mutates state), put a blank line before trailing same-row UI such as `ImGui::SameLine`, unit labels, or `GUI_DOC_HELP_`. diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 0cf4188..544b22a 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -720,8 +720,8 @@ The bone tool builds a connecting-rod outline: the outer arcs of the two end cir 1. Select **Add bone** on the toolbar (default Shift+U). 2. Click the first circle center (normal sketch snap). 3. Click the second circle center. Tab sets the center-to-center distance; Shift+Tab sets the bone-axis angle. -4. Click to set **radius 1** (distance from the first center). Tab enters an exact radius. -5. Click to set **radius 2** (distance from the second center). Tab enters an exact radius. +4. Click to set **radius 2** (distance from the second center, where the cursor just finished). Tab enters an exact radius. +5. Click to set **radius 1** (distance from the first center). Tab enters an exact radius. 6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). When the end radii differ, the neck is offset toward the smaller end, not at the center midpoint. Tab enters an exact waist. If **Holes** is **None**, the tool commits after this click. 7. If **Holes** is **One radius**, click once from the first center to set both hole radii. If **Two radii**, click hole radius at end A, then at end B. Each hole must be smaller than that end's outer radius. diff --git a/scripts/code_style_check.py b/scripts/code_style_check.py index 490f9bb..3c7c6e5 100644 --- a/scripts/code_style_check.py +++ b/scripts/code_style_check.py @@ -2,7 +2,9 @@ """Optional local check of C++ against docs/ezycad_code_style.md. Not part of CI or the default agent_check.py run. Start with Vertical rhythm -(blank lines). Do not add groups here to CI unless a rule is as objective as ASCII. +(blank lines), including a blank line after an early-exit `if` (return / continue / +break / CHK_RET) before the next statement in the same block. Do not add groups +here to CI unless a rule is as objective as ASCII. Usage: python scripts/code_style_check.py [paths...] @@ -46,6 +48,7 @@ IMGUI_TEXT_BEATS = ("ImGui::TextWrapped", "ImGui::TextDisabled") IMGUI_SPACING = "ImGui::Spacing" IMGUI_BUTTON = "ImGui::Button" +EARLY_EXIT_IDENTS = frozenset({"return", "continue", "break", "CHK_RET"}) @dataclass(frozen=True) @@ -705,6 +708,26 @@ def starts_with_any(code: str, prefixes: tuple[str, ...]) -> bool: return any(code.startswith(p) for p in prefixes) +def if_then_exits(src: Source, stmt: Stmt) -> bool: + """True when an `if` (no else required here) then-body ends with return/continue/break/CHK_RET.""" + if stmt.kind != "if" or not stmt.bodies: + return False + b0, b1 = stmt.bodies[0] + i = skip_ws_and_pp(src.masked, b0, b1) + if i >= b1: + return False + if src.masked[i] == "{": + close = skip_balanced(src.masked, i, b1, "{", "}") + inner = parse_statements(src, i + 1, close - 1) + else: + inner = parse_statements(src, b0, b1) + if not inner: + return peek_ident(src.masked, i, b1) in EARLY_EXIT_IDENTS + last = inner[-1] + lead = skip_ws_and_pp(src.masked, last.start, last.end) + return peek_ident(src.masked, lead, last.end) in EARLY_EXIT_IDENTS + + def check_vertical_rhythm(src: Source) -> list[Finding]: findings: list[Finding] = [] _check_span(src, 0, len(src.masked), findings) @@ -740,6 +763,17 @@ def _check_span(src: Source, start: int, end: int, findings: list[Finding]) -> N ) continue + if a.kind == "if" and not a.has_else and if_then_exits(src, a): + findings.append( + Finding( + src.path, + line, + "vertical-rhythm", + "blank line required after early-return if before the next statement", + ) + ) + continue + if a.kind == "if" and starts_with_any(b_lead, SAME_ROW_UI_PREFIXES): findings.append( Finding( diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..c2464d1 --- /dev/null +++ b/src/config.h @@ -0,0 +1,7 @@ +#pragma once + +// Compile-time developer extras (not persisted, not a user setting). +// Set to 0 for release builds. Override with -DDEV_MODE=0 if needed. +#ifndef DEV_MODE +#define DEV_MODE 1 +#endif diff --git a/src/doc/gui.md b/src/doc/gui.md index 84b4d5d..d7d3d1c 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -97,6 +97,8 @@ GUI (gui.h / gui.cpp) +-- scr_lua_console / scr_python_console (scripting UI) ``` +Compile-time `DEV_MODE` lives in [`config.h`](../config.h) (default on). When it is 1, Add-bone Options shows **Debug vis** checkboxes for construction overlays (cut circles, tangents, contacts, ...). The flags are session-only. + CMake IDE group: `src\gui` (files matching `gui*` or `occt*` prefix). ## ImGui docking and viewports @@ -222,7 +224,8 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio | `Sketch_inspection_mode` | `options_sketch_common_` | | Each sketch tool mode | Matching `options_sketch_*_mode_` | | `Sketch_operation_axis` | Mirror / Revolve / Clear axis | -| `Sketch_face_extrude` | Both sides, Twist, material; help mentions Settings fast preview | +| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview | +| `Sketch_add_bone` | Center nodes, holes; when `DEV_MODE` in `config.h`, **Debug vis** construction overlays (not persisted) | ### Options panel layout (sketch tools) diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 2945eda..d5f6e6e 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -71,6 +71,7 @@ Sketch (coordinator: skt.cpp, skt.h) Supporting (not owned sub-objects): skt_edge.* Sketch_edge type, linear/arc predicates + skt_bone.* bone outline / waist cutters (`compute_bone_geom`); DEV_MODE debug overlay skt_ais.* Sketch_AIS_edge, Sketch_AIS_node_mark, Sketch_face_shp skt_display.cpp visibility, edge styling, list hover, set_current skt_operations.cpp operation axis, mirror, revolve @@ -204,6 +205,7 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::listundo_stack_size(), m_view->redo_stack_size()); ImGui::Separator(); // Get the available content region width @@ -4351,12 +4352,14 @@ void GUI::export_file_dialog_(Export_format fmt, Export_unit unit) show_message(s.message()); return; } + std::ifstream in(mem_path, std::ios::binary); if (!in) { show_message("Export read failed."); return; } + const std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); download_blob_async(download_name, bytes); show_message("Exported: " + download_name); @@ -4523,6 +4526,7 @@ void GUI::save_file_dialog_() "changed."); return; } + std::string default_file = m_last_saved_path.empty() ? "project.ezy" : std::filesystem::path(m_last_saved_path).filename().string(); save_file_dialog_async("Save EzyCad project", default_file, ezy_bytes); @@ -4626,6 +4630,7 @@ std::string format_log_line_(const std::string& base, size_t repeat_count) { if (repeat_count <= 1) return base; + return base + " #" + std::to_string(repeat_count); } diff --git a/src/gui.h b/src/gui.h index a7ce18d..875ed6d 100644 --- a/src/gui.h +++ b/src/gui.h @@ -19,6 +19,10 @@ #include #include +#include "config.h" +#if DEV_MODE +#include "skt_bone.h" +#endif #include "utl_geom.h" #include "imgui.h" #include "imgui_markdown.h" @@ -309,6 +313,10 @@ class GUI bool get_bone_add_center_nodes() const { return m_bone_add_center_nodes; } /// Add-bone Options: hole clicks after waist (`gui.bone_holes`). Bone_holes get_bone_holes() const { return m_bone_holes; } +#if DEV_MODE + /// Add-bone construction overlays (DEV_MODE Options checkboxes; not persisted). + const Bone_debug_flags& get_bone_debug_flags() const { return m_bone_debug; } +#endif bool get_edge_from_center() const { return m_edge_from_center; } bool get_hide_all_shapes() const { return m_hide_all_shapes; } void set_hide_all_shapes(bool hide) { m_hide_all_shapes = hide; } @@ -662,6 +670,9 @@ class GUI bool m_add_mid_pt_slot_edges = false; bool m_bone_add_center_nodes = true; Bone_holes m_bone_holes = Bone_holes::None; +#if DEV_MODE + Bone_debug_flags m_bone_debug; +#endif bool m_edge_from_center = false; /// Degrees per numpad orbit (8/2/4/6) and Blender-style roll (Shift+NumPad 4/6); persisted in `gui.view_roll_step_deg`. double m_view_roll_step_deg = k_gui_view_roll_step_deg_default; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index ed0b3c6..6f5985e 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -13,6 +13,7 @@ #include #include +#include "config.h" #include "utl_geom.h" #include "gui.h" #include "imgui.h" @@ -303,6 +304,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) m_hotkey_capture_error.clear(); return; } + cancel_underlay_calib_(); hide_sketch_origin_set_edit(false); hide_dist_edit(false); @@ -329,6 +331,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) // Rotate / Align shafts finalize on Enter in their mode key handlers. if (get_mode() == Mode::Rotate || get_mode() == Mode::Shape_shaft_align) break; + hide_sketch_origin_set_edit(true); hide_dist_edit(); hide_angle_edit(); @@ -1092,8 +1095,10 @@ void GUI::options_sketch_face_extrude_mode_() { EZY_ASSERT(get_mode() == Mode::Sketch_face_extrude); - options_sketch_common_(); + // Tool-specific Extrude options above shared Sketch options (see src/doc/gui.md Options panel layout). + options_sketch_mode_header_(); + ImGui::TextUnformatted("Extrude"); if (ImGui::BeginTable("options_sketch_extrude", 2, k_options_table_flags)) { options_table_setup_columns_(options_sketch_label_col_w_(), k_options_sketch_control_col_w); @@ -1133,6 +1138,9 @@ void GUI::options_sketch_face_extrude_mode_() ImGui::EndTable(); } + + ImGui::Separator(); + options_sketch_shared_controls_(); } void GUI::options_sketch_dim_anno_mode_() @@ -1271,6 +1279,7 @@ void GUI::options_sketch_add_bone_mode_() m_bone_add_center_nodes = add_centers; save_occt_view_settings(); } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); GUI_DOC_HELP_("When on, commits permanent Bone A and Bone B nodes at the end centers. " "Click ? to open the user guide.", @@ -1288,6 +1297,7 @@ void GUI::options_sketch_add_bone_mode_() save_occt_view_settings(); } } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); GUI_DOC_HELP_("None: outline only. One radius: one click sets both holes. Two radii: set each end. " "Hole radius must be smaller than that end's outer radius. Click ? to open the user guide.", @@ -1296,6 +1306,22 @@ void GUI::options_sketch_add_bone_mode_() ImGui::TextWrapped( "Click center A, center B, radius 1, radius 2, waist, then holes if enabled."); +#if DEV_MODE + ImGui::Separator(); + ImGui::TextUnformatted("Debug vis"); + ImGui::TextDisabled("Construction overlays (DEV_MODE; not saved)."); + + bool vis_changed = false; + vis_changed |= ImGui::Checkbox("Cut circles", &m_bone_debug.cut_circles); + vis_changed |= ImGui::Checkbox("End circles", &m_bone_debug.end_circles); + vis_changed |= ImGui::Checkbox("Capsule tangents", &m_bone_debug.capsule_tangents); + vis_changed |= ImGui::Checkbox("Cutter centers", &m_bone_debug.cutter_centers); + vis_changed |= ImGui::Checkbox("Contact points", &m_bone_debug.contacts); + vis_changed |= ImGui::Checkbox("Cutter radials", &m_bone_debug.cutter_radials); + if (vis_changed) + m_view->curr_sketch().refresh_bone_preview(); +#endif + ImGui::Separator(); options_sketch_shared_controls_(); options_sketch_len_angle_hotkeys_(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index dffd52c..acc0b35 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -1838,6 +1838,7 @@ void Occt_view::angle_input(const ScreenCoords& screen_coords) m_shp_extrude.begin_angle_input(screen_coords); return; } + curr_sketch().angle_input(screen_coords); } @@ -3248,6 +3249,7 @@ Sketch* Occt_view::sketch_owner_of_list_ais_(const AIS_Shape_ptr& ais) if (auto* node = dynamic_cast(ais.get())) return &node->owner_sketch; + return nullptr; } @@ -4047,8 +4049,10 @@ std::string Occt_view::to_json() const json fd; if (s->show_frame_axes()) fd["axes"] = true; + if (s->show_frame_plane()) fd["plane"] = true; + if (s->show_frame_up()) fd["up"] = true; shp_json["frameDisplay"] = fd; @@ -4149,13 +4153,16 @@ void Occt_view::load(const std::string& json_str, bool restore_view) shp = new Shp(*m_ctx, shape); if (s.contains("frame") && s["frame"].is_object()) shp->set_frame(from_json_pln(s["frame"]).Position()); + if (s.contains("frameDisplay") && s["frameDisplay"].is_object()) { const json& fd = s["frameDisplay"]; if (fd.contains("axes") && fd["axes"].is_boolean()) shp->set_show_frame_axes(fd["axes"].get()); + if (fd.contains("plane") && fd["plane"].is_boolean()) shp->set_show_frame_plane(fd["plane"].get()); + if (fd.contains("up") && fd["up"].is_boolean()) shp->set_show_frame_up(fd["up"].get()); } @@ -4661,6 +4668,7 @@ Section_import_counts import_section_edges_into_sketch_(Sketch& sketch, const To ++counts.skipped; break; } + sketch.add_linear_edge(pt_a, pt_b); ++counts.imported; break; diff --git a/src/main.cpp b/src/main.cpp index a66ca54..ec0e268 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -161,6 +161,7 @@ int main(int argc, char** argv) GLFWwindow* window = glfwCreateWindow((int)(1280 * main_scale), (int)(800 * main_scale), "EzyCad", nullptr, nullptr); if (window == nullptr) return 1; + glfwMakeContextCurrent(window); glfwSwapInterval(1); // Enable vsync #ifndef __EMSCRIPTEN__ @@ -278,6 +279,7 @@ int main(int argc, char** argv) std::fprintf(stderr, "EzyCad: --listen: %s\n", parse_err.c_str()); return 1; } + gui.ensure_python_console(); Python_console* console = gui.get_python_console(); if (!console || !console->is_python_ok()) @@ -285,6 +287,7 @@ int main(int argc, char** argv) std::fprintf(stderr, "EzyCad: --listen requires a working Python interpreter\n"); return 1; } + py_queue = std::make_unique(); py_remote = std::make_unique(*py_queue); std::string start_err; @@ -293,6 +296,7 @@ int main(int argc, char** argv) std::fprintf(stderr, "EzyCad: remote listen failed: %s\n", start_err.c_str()); return 1; } + std::fprintf(stderr, "EzyCad: Python remote listening on %s:%u\n", ep.host.c_str(), static_cast(ep.port)); } #endif @@ -497,6 +501,7 @@ bool parse_cli_listen_(int argc, char** argv, bool& want_listen, std::string& li error = "--listen requires [host:]port"; return false; } + want_listen = true; listen_arg = argv[++i]; } diff --git a/src/scr_lua_console.cpp b/src/scr_lua_console.cpp index ffb992d..62706a4 100644 --- a/src/scr_lua_console.cpp +++ b/src/scr_lua_console.cpp @@ -743,6 +743,7 @@ Lua_console::Lua_console(GUI* gui) m_L = luaL_newstate(); if (!m_L) return; + luaL_openlibs(m_L); lua_pushlightuserdata(m_L, m_gui); lua_setfield(m_L, LUA_REGISTRYINDEX, k_registry_gui); @@ -889,6 +890,7 @@ void Lua_console::load_scripts() if (!m_L) return; #ifdef __EMSCRIPTEN__ + const std::filesystem::path scripts_dir("/res/scripts/lua"); #else const std::filesystem::path scripts_dir("res/scripts/lua"); @@ -901,6 +903,7 @@ void Lua_console::load_scripts() { if (!entry.is_regular_file() || entry.path().extension() != ".lua") continue; + lua_files.push_back(entry.path()); } std::sort(lua_files.begin(), lua_files.end()); @@ -961,6 +964,7 @@ void Lua_console::execute(const std::string& code) lua_pop(m_L, 1); return; } + int n = lua_gettop(m_L); if (n > 0) { diff --git a/src/scr_python_console.cpp b/src/scr_python_console.cpp index de02207..4b9d9b3 100644 --- a/src/scr_python_console.cpp +++ b/src/scr_python_console.cpp @@ -873,6 +873,7 @@ void Python_console::load_scripts() if (!m_python_ok) return; #ifdef __EMSCRIPTEN__ + const std::filesystem::path scripts_dir("/res/scripts/python"); #else const std::filesystem::path scripts_dir("res/scripts/python"); diff --git a/src/scr_python_remote.cpp b/src/scr_python_remote.cpp index 045f927..abb0d08 100644 --- a/src/scr_python_remote.cpp +++ b/src/scr_python_remote.cpp @@ -416,6 +416,7 @@ void Python_remote_server::accept_loop_() closesocket_compat(client); break; } + handle_client_(static_cast(client)); } } diff --git a/src/shp.cpp b/src/shp.cpp index f93478b..7c7ff35 100644 --- a/src/shp.cpp +++ b/src/shp.cpp @@ -179,6 +179,7 @@ void Shp::set_show_frame_axes(bool show) { if (m_show_frame_axes == show) return; + m_show_frame_axes = show; update_frame_display(); } @@ -187,6 +188,7 @@ void Shp::set_show_frame_plane(bool show) { if (m_show_frame_plane == show) return; + m_show_frame_plane = show; update_frame_display(); } @@ -195,6 +197,7 @@ void Shp::set_show_frame_up(bool show) { if (m_show_frame_up == show) return; + m_show_frame_up = show; update_frame_display(); } @@ -203,6 +206,7 @@ void Shp::set_frame_display_suppressed(bool suppressed) { if (m_frame_display_suppressed == suppressed) return; + m_frame_display_suppressed = suppressed; update_frame_display(); } diff --git a/src/shp_common.cpp b/src/shp_common.cpp index ed940a9..211d0b5 100644 --- a/src/shp_common.cpp +++ b/src/shp_common.cpp @@ -65,5 +65,6 @@ Status Shp_common::selected_common() Shp_rslt r = common(std::move(m_shps)); if (!r.is_ok()) return Status(r.status(), r.message()); + return Status::ok(); } diff --git a/src/skt.cpp b/src/skt.cpp index 794fd65..e1bb68b 100644 --- a/src/skt.cpp +++ b/src/skt.cpp @@ -14,6 +14,7 @@ #include #include "utl_geom.h" +#include "skt_bone.h" #include "mode.h" #include "gui_occt_view.h" #include "skt_op_recorder.h" @@ -152,6 +153,8 @@ void Sketch::add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const void Sketch::rebuild_faces() { update_faces_(); } +void Sketch::refresh_bone_preview() { m_tools.refresh_bone_preview(); } + void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist, bool add_center_nodes, std::optional hole_r1, std::optional hole_r2) { @@ -172,6 +175,7 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double return true; return *hole > Precision::Confusion() && *hole + Precision::Confusion() < outer_r; }; + if (!hole_ok(r1, hole_r1) || !hole_ok(r2, hole_r2)) return; @@ -206,6 +210,7 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double add_arc_circle_(p.c2_minus, p.waist_minus, p.c1_minus, rec); if (hole_r1) add_hole_circle(g->c1, *hole_r1); + if (hole_r2) add_hole_circle(g->c2, *hole_r2); rec.commit(); @@ -570,6 +575,7 @@ Sketch_face_shp_ptr Sketch::inspector_face(size_t index) const { if (index >= m_topo.faces().size()) return {}; + return m_topo.faces()[index]; } diff --git a/src/skt.h b/src/skt.h index 98feff5..ef6bbf9 100644 --- a/src/skt.h +++ b/src/skt.h @@ -175,6 +175,9 @@ class Sketch bool add_center_nodes = true, std::optional hole_r1 = std::nullopt, std::optional hole_r2 = std::nullopt); + /// Rebuild Add-bone AIS preview (outline + DEV_MODE construction overlays). + void refresh_bone_preview(); + private: friend class Sketch_json; friend class Sketch_access; diff --git a/src/skt_bone.cpp b/src/skt_bone.cpp new file mode 100644 index 0000000..26e1562 --- /dev/null +++ b/src/skt_bone.cpp @@ -0,0 +1,385 @@ +#include "skt_bone.h" + +#include "utl_dbg.h" +#include "utl_geom.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +gp_Pnt2d mirror_across_axis_(const gp_Pnt2d& p, const gp_Pnt2d& c1, const gp_Vec2d& axis) +{ + const double len = axis.Magnitude(); + if (len <= Precision::Confusion()) + return p; + + const gp_Vec2d a(axis / len); + gp_Vec2d v(c1, p); + const double along = v.Dot(a); + gp_Vec2d perp = v - a * along; + return gp_Pnt2d(c1).Translated(a * along - perp); +} + +std::optional circle_circle_intersect_pick_side_(const gp_Pnt2d& c1, double ra, const gp_Pnt2d& c2, double rb, + const gp_Pnt2d& mid, const gp_Vec2d& n, bool positive_n_side) +{ + const double eps = Precision::Confusion(); + gp_Vec2d axis(c1, c2); + const double d = axis.Magnitude(); + if (d <= eps || ra <= eps || rb <= eps) + return std::nullopt; + + if (d > ra + rb + eps || d + eps < std::fabs(ra - rb)) + return std::nullopt; + + const double a = (ra * ra - rb * rb + d * d) / (2.0 * d); + const double h2 = ra * ra - a * a; + if (h2 < -eps) + return std::nullopt; + + const double h = std::sqrt(std::max(0.0, h2)); + const gp_Vec2d ad(axis / d); + gp_Pnt2d p2 = gp_Pnt2d(c1).Translated(ad * a); + gp_Vec2d perp(-ad.Y() * h, ad.X() * h); + + const gp_Pnt2d i0 = p2.Translated(perp); + const gp_Pnt2d i1 = p2.Translated(-perp); + + const double d0 = gp_Vec2d(mid, i0).Dot(n); + const double d1 = gp_Vec2d(mid, i1).Dot(n); + if (positive_n_side) + return d0 >= d1 ? i0 : i1; + + return d0 <= d1 ? i0 : i1; +} + +double bone_waist_gap_(const gp_Pnt2d& c_top, const gp_Pnt2d& c_bot, double cut_r) +{ + // Min gap between the two cutter circles (along the line of centers). + return c_top.Distance(c_bot) - 2.0 * cut_r; +} + +std::optional bone_geom_with_cut_radius_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, + double cut_r, const gp_Vec2d& axis, const gp_Vec2d& n, + const gp_Pnt2d& mid, double vx, double vy, double a, double h) +{ + const double eps = Precision::Confusion(); + if (cut_r <= eps) + return std::nullopt; + + // External tangency with both ends: cutter center lies on the circle of radius (cut_r + r1) + // around c1 and (cut_r + r2) around c2. That intersection is the cutter center. The bone + // outline later meets each end circle on the line of those centers (see get_bone_profile). + const std::optional c_top = + circle_circle_intersect_pick_side_(c1, cut_r + r1, c2, cut_r + r2, mid, n, true); + if (!c_top) + return std::nullopt; + + Bone_geom g; + g.c1 = c1; + g.c2 = c2; + g.r1 = r1; + g.r2 = r2; + g.cut_radius = cut_r; + g.cut_plus = *c_top; + g.cut_minus = mirror_across_axis_(*c_top, c1, axis); + g.waist = bone_waist_gap_(g.cut_plus, g.cut_minus, cut_r); + + // Capsule (external common) tangents of the end circles, ignoring waist cutters. + // (nx, ny) is the shared unit direction from each center to its contact; same n on both + // circles keeps the radii parallel so t1--t2 is the tangent segment. sign picks the side. + auto tangent_pair = [&](double sign, gp_Pnt2d& t1, gp_Pnt2d& t2) + { + const double nx = vx * a - sign * vy * h; + const double ny = vy * a + sign * vx * h; + t1 = gp_Pnt2d(c1.X() + r1 * nx, c1.Y() + r1 * ny); + t2 = gp_Pnt2d(c2.X() + r2 * nx, c2.Y() + r2 * ny); + }; + + tangent_pair(1.0, g.tan_top_a, g.tan_top_b); + tangent_pair(-1.0, g.tan_bot_a, g.tan_bot_b); + return g; +} + +double bone_waist_for_cut_radius_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_r, + const gp_Vec2d& axis, const gp_Vec2d& n, const gp_Pnt2d& mid, double vx, double vy, + double a, double h) +{ + const std::optional g = bone_geom_with_cut_radius_(c1, c2, r1, r2, cut_r, axis, n, mid, vx, vy, a, h); + if (!g) + return std::numeric_limits::quiet_NaN(); + + return g->waist; +} + +std::optional solve_cut_radius_for_waist_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, + double target_waist, const gp_Vec2d& axis, const gp_Vec2d& n, + const gp_Pnt2d& mid, double vx, double vy, double a, double h) +{ + const double eps = Precision::Confusion(); + if (target_waist <= eps) + return std::nullopt; + + const double dist = axis.Magnitude(); + double r_lo = (dist - r1 - r2) / 2.0; + if (r_lo <= eps) + r_lo = eps; + + double w_lo = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_lo, axis, n, mid, vx, vy, a, h); + if (!std::isfinite(w_lo)) + return std::nullopt; + + if (w_lo >= target_waist) + return std::nullopt; + + double r_hi = std::max(r_lo + 1.0, r1 + r2); + for (int i = 0; i < 48; ++i) + { + const double w_hi = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_hi, axis, n, mid, vx, vy, a, h); + if (!std::isfinite(w_hi) || w_hi < target_waist) + r_hi *= 2.0; + else + break; + } + + const double w_hi = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_hi, axis, n, mid, vx, vy, a, h); + if (!std::isfinite(w_hi) || w_hi < target_waist) + return std::nullopt; + + for (int i = 0; i < 64; ++i) + { + const double r_mid = (r_lo + r_hi) * 0.5; + const double w_mid = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_mid, axis, n, mid, vx, vy, a, h); + if (!std::isfinite(w_mid)) + return std::nullopt; + + if (w_mid < target_waist) + r_lo = r_mid; + else + r_hi = r_mid; + } + + return (r_lo + r_hi) * 0.5; +} + +#if DEV_MODE +void add_debug_seg_(TopoDS_Compound& comp, BRep_Builder& bb, const gp_Pln& pln, const gp_Pnt2d& a, const gp_Pnt2d& b) +{ + if (a.Distance(b) <= Precision::Confusion()) + return; + + bb.Add(comp, BRepBuilderAPI_MakeEdge(to_3d(pln, a), to_3d(pln, b)).Edge()); +} + +void add_debug_circle_(TopoDS_Compound& comp, BRep_Builder& bb, const gp_Pln& pln, const gp_Pnt2d& c, double r) +{ + if (r <= Precision::Confusion()) + return; + + bb.Add(comp, make_circle_wire(pln, c, gp_Pnt2d(c.X() + r, c.Y()))); +} +#endif +} // namespace + +std::optional compute_bone_geom(const Bone_params& p) +{ + const double eps = Precision::Confusion(); + if (p.r1 <= eps || p.r2 <= eps) + return std::nullopt; + + if (p.drive == Bone_drive::Cut_radius && p.cut_radius <= eps) + return std::nullopt; + + if (p.drive == Bone_drive::Waist && p.waist <= eps) + return std::nullopt; + + const gp_Vec2d axis(p.c1, p.c2); + const double dist = axis.Magnitude(); + if (dist <= eps) + return std::nullopt; + + if (dist + eps < std::fabs(p.r1 - p.r2)) + return std::nullopt; + + // Similar-triangles setup for external common tangents of unequal circles: + // (vx, vy) unit bone axis, a = (r1 - r2) / dist, h = sqrt(1 - a^2) perpendicular part. + // Passed into bone_geom_with_cut_radius_ for the capsule tangent endpoints. + const double vx = axis.X() / dist; + const double vy = axis.Y() / dist; + const double a = (p.r1 - p.r2) / dist; + const double h2 = 1.0 - a * a; + if (h2 < -eps) + return std::nullopt; + + const double h = std::sqrt(std::max(0.0, h2)); + const gp_Pnt2d mid = get_midpoint(p.c1, p.c2); + const gp_Vec2d n = gp_Vec2d(vx, vy).Rotated(std::numbers::pi / 2.0); + + double cut_r = p.cut_radius; + if (p.drive == Bone_drive::Waist) + { + const std::optional solved = + solve_cut_radius_for_waist_(p.c1, p.c2, p.r1, p.r2, p.waist, axis, n, mid, vx, vy, a, h); + if (!solved) + return std::nullopt; + + cut_r = *solved; + } + + return bone_geom_with_cut_radius_(p.c1, p.c2, p.r1, p.r2, cut_r, axis, n, mid, vx, vy, a, h); +} + +Bone_profile get_bone_profile(const Bone_geom& g) +{ + gp_Vec2d axis(g.c1, g.c2); + const double dist = axis.Magnitude(); + EZY_ASSERT(dist > Precision::Confusion()); + const gp_Vec2d ad(axis / dist); + + // External tangency of end circle (c, r) with cutter (cut, cut_radius): + // |c - cut| = r + cut_radius, so the contact sits on the line of centers, a fraction + // r / |c - cut| from the end center toward the cutter (equivalently cut_radius from the cutter). + auto contact = [](const gp_Pnt2d& c, double r, const gp_Pnt2d& cut) -> gp_Pnt2d + { + gp_Vec2d v(c, cut); + const double d = v.Magnitude(); + EZY_ASSERT(d > Precision::Confusion()); + return gp_Pnt2d(c).Translated(v * (r / d)); + }; + + Bone_profile p; + p.c1_plus = contact(g.c1, g.r1, g.cut_plus); + p.c1_minus = contact(g.c1, g.r1, g.cut_minus); + p.c2_plus = contact(g.c2, g.r2, g.cut_plus); + p.c2_minus = contact(g.c2, g.r2, g.cut_minus); + p.c1_outer = gp_Pnt2d(g.c1).Translated(-ad * g.r1); + p.c2_outer = gp_Pnt2d(g.c2).Translated(ad * g.r2); + + // Skinniest section: points of closest approach between the two cutter circles. + // When r1 != r2 this is offset along the bone axis from the center midpoint. + gp_Vec2d to_minus(g.cut_plus, g.cut_minus); + const double cut_sep = to_minus.Magnitude(); + EZY_ASSERT(cut_sep > Precision::Confusion()); + to_minus /= cut_sep; + p.waist_plus = gp_Pnt2d(g.cut_plus).Translated(to_minus * g.cut_radius); + p.waist_minus = gp_Pnt2d(g.cut_minus).Translated(-to_minus * g.cut_radius); + return p; +} + +TopoDS_Wire make_bone_wire(const gp_Pln& pln, const Bone_geom& g) +{ + const Bone_profile p = get_bone_profile(g); + + auto arc = [&](const gp_Pnt2d& a, const gp_Pnt2d& mid, const gp_Pnt2d& b) -> TopoDS_Edge + { + GC_MakeArcOfCircle maker(to_3d(pln, a), to_3d(pln, mid), to_3d(pln, b)); + return BRepBuilderAPI_MakeEdge(maker.Value()).Edge(); + }; + + BRepBuilderAPI_MakeWire wire; + // End-cap arcs (outer poles) then waist arcs. Join points are the cutter/end contacts + // (the far ends of the Debug vis cutter radials). + wire.Add(arc(p.c1_minus, p.c1_outer, p.c1_plus)); + wire.Add(arc(p.c1_plus, p.waist_plus, p.c2_plus)); + wire.Add(arc(p.c2_plus, p.c2_outer, p.c2_minus)); + wire.Add(arc(p.c2_minus, p.waist_minus, p.c1_minus)); + return wire.Wire(); +} + +TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g) +{ + return make_bone_wire(pln, g); +} + +#if DEV_MODE +std::optional make_bone_debug_shape(const gp_Pln& pln, const Bone_geom& g, const Bone_debug_flags& flags) +{ + if (!flags.cut_circles && !flags.end_circles && !flags.capsule_tangents && !flags.cutter_centers && !flags.contacts && + !flags.cutter_radials) + return std::nullopt; + + const Bone_profile pr = get_bone_profile(g); + + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bool any = false; + + auto mark = [&]() { any = true; }; + + if (flags.cut_circles) + { + add_debug_circle_(comp, bb, pln, g.cut_plus, g.cut_radius); + add_debug_circle_(comp, bb, pln, g.cut_minus, g.cut_radius); + mark(); + } + + if (flags.end_circles) + { + add_debug_circle_(comp, bb, pln, g.c1, g.r1); + add_debug_circle_(comp, bb, pln, g.c2, g.r2); + mark(); + } + + // Stadium-side segments between the two end circles (see tangent_pair in bone_geom_with_cut_radius_). + if (flags.capsule_tangents) + { + add_debug_seg_(comp, bb, pln, g.tan_top_a, g.tan_top_b); + add_debug_seg_(comp, bb, pln, g.tan_bot_a, g.tan_bot_b); + mark(); + } + + // Four spokes, one per (cutter, end) pair. Each is the cutter radius to the tangency + // with that end circle. Continuing the same line past the contact would hit the end + // center: cutter, contact, and end center are collinear. The committed outline switches + // from end-cap arc to waist arc at that contact. + if (flags.cutter_radials) + { + add_debug_seg_(comp, bb, pln, g.cut_plus, pr.c1_plus); + add_debug_seg_(comp, bb, pln, g.cut_plus, pr.c2_plus); + add_debug_seg_(comp, bb, pln, g.cut_minus, pr.c1_minus); + add_debug_seg_(comp, bb, pln, g.cut_minus, pr.c2_minus); + mark(); + } + + const double half = std::max(g.cut_radius, std::max(g.r1, g.r2)) * 0.08; + auto plus = [&](const gp_Pnt2d& p) + { + bb.Add(comp, create_plus_cross_shape(pln, to_3d(pln, p), std::max(half, Precision::Confusion() * 50.0))); + mark(); + }; + + if (flags.cutter_centers) + { + plus(g.cut_plus); + plus(g.cut_minus); + } + + if (flags.contacts) + { + plus(pr.c1_plus); + plus(pr.c1_minus); + plus(pr.c2_plus); + plus(pr.c2_minus); + plus(pr.waist_plus); + plus(pr.waist_minus); + } + + if (!any) + return std::nullopt; + + return comp; +} +#endif diff --git a/src/skt_bone.h b/src/skt_bone.h new file mode 100644 index 0000000..78217ed --- /dev/null +++ b/src/skt_bone.h @@ -0,0 +1,93 @@ +#pragma once + +#include "config.h" + +#include +#include + +class gp_Pln; +class TopoDS_Shape; +class TopoDS_Wire; + +/// Which dialog value drives the waist cutter radius (the other is derived). +enum class Bone_drive +{ + Cut_radius, + Waist, +}; + +/// Two end circles, two waist cutters, and the capsule external tangents. +struct Bone_params +{ + gp_Pnt2d c1; + gp_Pnt2d c2; + double r1 = 0; + double r2 = 0; + double cut_radius = 0; + double waist = 0; + Bone_drive drive = Bone_drive::Waist; +}; + +struct Bone_geom +{ + gp_Pnt2d c1; + gp_Pnt2d c2; + gp_Pnt2d cut_plus; // waist cutter center, + side of the bone axis + gp_Pnt2d cut_minus; // mirrored cutter center + double r1 = 0; + double r2 = 0; + double cut_radius = 0; + double waist = 0; + // External common tangents of the two end circles alone (Debug vis "Capsule tangents"; + // not part of the trimmed bone outline). Each pair is one stadium-side segment. + gp_Pnt2d tan_top_a; // contact on end 1 + gp_Pnt2d tan_top_b; // contact on end 2 + gp_Pnt2d tan_bot_a; + gp_Pnt2d tan_bot_b; +}; + +/// Trimmed bone outline: outer end-circle caps and inner waist arcs (no full circles, no capsule tangents). +/// +/// Each `c*_plus` / `c*_minus` contact is the external tangency of that end circle with a waist +/// cutter. Because the circles are externally tangent, cutter center, contact, and end center are +/// collinear: the Debug vis "Cutter radials" segments are exactly those shared radii. +struct Bone_profile +{ + gp_Pnt2d c1_plus; // tangency, end 1 / cutter + (on line cut_plus -> c1) + gp_Pnt2d c1_outer; // outer pole of end 1 + gp_Pnt2d c1_minus; // tangency, end 1 / cutter - + gp_Pnt2d c2_plus; // tangency, end 2 / cutter + (on line cut_plus -> c2) + gp_Pnt2d c2_outer; + gp_Pnt2d c2_minus; + gp_Pnt2d waist_plus; // inner bulge of cutter + (closest approach of the two cutters) + gp_Pnt2d waist_minus; +}; + +/// Null when centers coincide, a circle is inside the other, or waist/cut radius cannot be solved. +std::optional compute_bone_geom(const Bone_params& p); + +Bone_profile get_bone_profile(const Bone_geom& g); + +/// Closed wire of the four outline arcs (preview and extrusion profile). +TopoDS_Wire make_bone_wire(const gp_Pln& pln, const Bone_geom& g); + +/// Same as \\a make_bone_wire (AIS preview). +TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g); + +#if DEV_MODE +/// Session-only Add-bone construction overlays (Options checkboxes; not persisted). +struct Bone_debug_flags +{ + bool cut_circles = false; + bool end_circles = false; + /// External common tangents of the end circles (stadium sides; not the trimmed outline). + bool capsule_tangents = false; + bool cutter_centers = false; + bool contacts = false; + /// Cutter center to end-circle tangency (shared radius of the tangent pair). + bool cutter_radials = false; +}; + +/// Construction geometry for the live bone preview. Null when every flag is off. +std::optional make_bone_debug_shape(const gp_Pln& pln, const Bone_geom& g, const Bone_debug_flags& flags); +#endif diff --git a/src/skt_node_marks.cpp b/src/skt_node_marks.cpp index 0dfcd72..25ea733 100644 --- a/src/skt_node_marks.cpp +++ b/src/skt_node_marks.cpp @@ -129,6 +129,7 @@ Sketch_AIS_node_mark_ptr Sketch_node_marks::get(size_t node_idx) const { if (node_idx >= m_marks.size()) return {}; + return m_marks[node_idx]; } diff --git a/src/skt_nodes.cpp b/src/skt_nodes.cpp index d990584..1f1df5d 100644 --- a/src/skt_nodes.cpp +++ b/src/skt_nodes.cpp @@ -104,6 +104,7 @@ class Sketch_nodes::Impl if (n.origin && !m_origin_snap_enabled) return false; + return true; } diff --git a/src/skt_tools.cpp b/src/skt_tools.cpp index b2340b3..e5fc02a 100644 --- a/src/skt_tools.cpp +++ b/src/skt_tools.cpp @@ -914,6 +914,10 @@ void Sketch_tools::finalize_operation_axis_(Sketch_op_recorder& rec) bool Sketch_tools::clear_tmps() { m_sketch.m_view.remove(m_tmp_shp); +#if DEV_MODE + m_sketch.m_view.remove(m_tmp_debug_shp); + m_tmp_debug_shp = nullptr; +#endif for (Sketch_edge& e : m_tmp_edges) m_sketch.m_view.remove(e.shp); diff --git a/src/skt_tools.h b/src/skt_tools.h index f95874e..1ae637d 100644 --- a/src/skt_tools.h +++ b/src/skt_tools.h @@ -6,11 +6,13 @@ #include #include +#include "config.h" #include "skt_edge.h" #include "utl_types.h" class Sketch; class Sketch_op_recorder; +struct Bone_geom; /// Interactive sketch drawing session: tmp edges/nodes, previews, finalize/cancel. class Sketch_tools @@ -40,6 +42,9 @@ class Sketch_tools /// Clears rubber-band / tmp geometry. Returns true if tmp edges were present (used by `cancel()`). bool clear_tmps(); + /// Rebuild Add-bone preview from current tmp state (Options debug checkboxes). + void refresh_bone_preview(); + void clear_tmp_node_idxs() { m_tmp_node_idxs.clear(); } [[nodiscard]] size_t tmp_node_count() const { return m_tmp_node_idxs.size(); } @@ -84,6 +89,10 @@ class Sketch_tools [[nodiscard]] bool bone_after_hole_b_(double hole_r); [[nodiscard]] bool bone_try_commit_(); void bone_update_preview_(); +#if DEV_MODE + void bone_hide_debug_(); + void bone_show_debug_(const Bone_geom& g); +#endif [[nodiscard]] std::optional bone_axis_perp_() const; [[nodiscard]] std::optional bone_waist_from_pt_(const gp_Pnt2d& pt) const; @@ -103,6 +112,9 @@ class Sketch_tools std::vector m_tmp_node_idxs; std::vector m_tmp_edges; AIS_Shape_ptr m_tmp_shp; +#if DEV_MODE + AIS_Shape_ptr m_tmp_debug_shp; +#endif std::optional> m_bone_centers; std::optional m_bone_r1; std::optional m_bone_r2; diff --git a/src/skt_tools_bone.cpp b/src/skt_tools_bone.cpp index 732dc65..9320817 100644 --- a/src/skt_tools_bone.cpp +++ b/src/skt_tools_bone.cpp @@ -2,7 +2,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -10,10 +13,12 @@ #include #include +#include "config.h" #include "gui.h" #include "gui_occt_view.h" #include "mode.h" #include "skt.h" +#include "skt_bone.h" #include "utl_geom.h" #include "utl_occt.h" #include "utl.h" @@ -46,12 +51,14 @@ void Sketch_tools::bone_on_enter_() m_sketch.m_dims.clear_typed_constraints(); } + if (!m_bone_centers && !m_tmp_edges.empty() && m_tmp_edges.back().node_idx_b.has_value()) { const Sketch_edge& e = m_tmp_edges.back(); bone_on_centers_ready_(m_sketch.m_nodes[e.node_idx_a], m_sketch.m_nodes[*e.node_idx_b]); return; } + if (!m_bone_centers || !m_sketch.m_dims.entered_edge_len().has_value()) return; @@ -67,20 +74,23 @@ void Sketch_tools::bone_on_enter_() m_sketch.m_dims.clear_typed_constraints(); + // Radii after centers: second end first (mouse is near c2), then first end. if (m_tmp_edges.size() == 2) { - m_bone_r1 = len; - bone_begin_next_edge_from_(m_bone_centers->second); + m_bone_r2 = len; + bone_begin_next_edge_from_(m_bone_centers->first); } else if (m_tmp_edges.size() == 3) { - m_bone_r2 = len; + m_bone_r1 = len; bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); } else if (m_tmp_edges.size() == 4) (void)bone_after_waist_(len); + else if (m_tmp_edges.size() == 5) (void)bone_after_hole_a_(len); + else if (m_tmp_edges.size() == 6) (void)bone_after_hole_b_(len); } @@ -144,14 +154,15 @@ void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) m_sketch.update_edge_end_pt_(last, node_idx); const double len = pt_a.Distance(pt_b); + // Radii after centers: second end first (mouse is near c2), then first end. if (m_tmp_edges.size() == 2) { - m_bone_r1 = len; - bone_begin_next_edge_from_(m_bone_centers->second); + m_bone_r2 = len; + bone_begin_next_edge_from_(m_bone_centers->first); } else if (m_tmp_edges.size() == 3) { - m_bone_r2 = len; + m_bone_r1 = len; bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); } else if (m_tmp_edges.size() == 4) @@ -162,6 +173,7 @@ void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) } else if (m_tmp_edges.size() == 5) (void)bone_after_hole_a_(len); + else if (m_tmp_edges.size() == 6) (void)bone_after_hole_b_(len); }; @@ -257,7 +269,8 @@ void Sketch_tools::bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2 return; m_bone_centers = std::make_pair(c1, c2); - bone_begin_next_edge_from_(c1); + // Start radius at the second center (where the cursor just placed c2). + bone_begin_next_edge_from_(c2); } bool Sketch_tools::bone_after_waist_(double waist) @@ -298,6 +311,7 @@ bool Sketch_tools::bone_after_hole_a_(double hole_r) m_sketch.m_view.gui().show_message("Hole radius must be smaller than the end circle."); return false; } + m_bone_hole_r1 = hole_r; m_bone_hole_r2 = hole_r; return bone_try_commit_(); @@ -305,6 +319,7 @@ bool Sketch_tools::bone_after_hole_a_(double hole_r) if (holes != Bone_holes::Two_radii) return false; + if (!bone_hole_radius_ok_(*m_bone_r1, hole_r)) { if (bone_hole_radius_too_large_(*m_bone_r1, hole_r)) @@ -321,6 +336,7 @@ bool Sketch_tools::bone_after_hole_b_(double hole_r) { if (!m_bone_centers || !m_bone_r2 || !m_bone_waist || !m_bone_hole_r1) return false; + if (!bone_hole_radius_ok_(*m_bone_r2, hole_r)) { if (bone_hole_radius_too_large_(*m_bone_r2, hole_r)) @@ -364,6 +380,7 @@ void Sketch_tools::finalize_bone_() { if (m_sketch.m_view.gui().get_bone_holes() != Bone_holes::None) return; + const std::optional waist = bone_waist_from_pt_(*m_last_pt); if (waist) (void)bone_after_waist_(*waist); @@ -411,10 +428,41 @@ std::optional Sketch_tools::bone_waist_from_pt_(const gp_Pnt2d& pt) cons return waist; } +void Sketch_tools::refresh_bone_preview() { bone_update_preview_(); } + +#if DEV_MODE +void Sketch_tools::bone_hide_debug_() +{ + m_sketch.m_view.remove(m_tmp_debug_shp); + m_tmp_debug_shp = nullptr; +} + +void Sketch_tools::bone_show_debug_(const Bone_geom& g) +{ + const std::optional dbg = + make_bone_debug_shape(m_sketch.m_pln, g, m_sketch.m_view.gui().get_bone_debug_flags()); + if (!dbg) + { + bone_hide_debug_(); + return; + } + + show(m_sketch.m_ctx, m_tmp_debug_shp, *dbg); + m_tmp_debug_shp->SetColor(Quantity_NOC_CYAN); + m_tmp_debug_shp->SetWidth(1.5); + m_sketch.m_ctx.Deactivate(m_tmp_debug_shp); +} +#endif + void Sketch_tools::bone_update_preview_() { if (!m_bone_centers) + { +#if DEV_MODE + bone_hide_debug_(); +#endif return; + } const gp_Pnt2d& c1 = m_bone_centers->first; const gp_Pnt2d& c2 = m_bone_centers->second; @@ -422,10 +470,11 @@ void Sketch_tools::bone_update_preview_() auto circle_at = [&](const gp_Pnt2d& c, double r) -> TopoDS_Wire { return make_circle_wire(m_sketch.m_pln, c, gp_Pnt2d(c.X() + r, c.Y())); }; + // Live preview: edge 2 sizes c2, edge 3 sizes c1 (same order as placement). const double r1 = m_bone_r1.value_or( - (m_tmp_edges.size() == 2 && m_last_pt) ? c1.Distance(*m_last_pt) : 0.0); + (m_tmp_edges.size() == 3 && m_last_pt) ? c1.Distance(*m_last_pt) : 0.0); const double r2 = m_bone_r2.value_or( - (m_tmp_edges.size() == 3 && m_last_pt) ? c2.Distance(*m_last_pt) : 0.0); + (m_tmp_edges.size() == 2 && m_last_pt) ? c2.Distance(*m_last_pt) : 0.0); auto show_compound = [&](const TopoDS_Compound& comp) { show(m_sketch.m_ctx, m_tmp_shp, comp); }; @@ -475,6 +524,7 @@ void Sketch_tools::bone_update_preview_() : (m_last_pt ? c1.Distance(*m_last_pt) : 0.0); if (bone_hole_radius_ok_(*m_bone_r1, hr)) bb.Add(comp, circle_at(c1, hr)); + if (bone_hole_radius_ok_(*m_bone_r2, hr)) bb.Add(comp, circle_at(c2, hr)); } @@ -486,12 +536,18 @@ void Sketch_tools::bone_update_preview_() } show_compound(comp); +#if DEV_MODE + bone_show_debug_(*g); +#endif return; } } m_sketch.m_view.remove(m_tmp_shp); m_tmp_shp = nullptr; +#if DEV_MODE + bone_hide_debug_(); +#endif return; } @@ -504,6 +560,7 @@ void Sketch_tools::bone_update_preview_() bb.Add(comp, circle_at(c1, r1)); any = true; } + if (r2 > Precision::Confusion()) { bb.Add(comp, circle_at(c2, r2)); @@ -514,8 +571,14 @@ void Sketch_tools::bone_update_preview_() { m_sketch.m_view.remove(m_tmp_shp); m_tmp_shp = nullptr; +#if DEV_MODE + bone_hide_debug_(); +#endif return; } show_compound(comp); +#if DEV_MODE + bone_hide_debug_(); +#endif } diff --git a/src/utl.cpp b/src/utl.cpp index 301d9fc..eec52a4 100644 --- a/src/utl.cpp +++ b/src/utl.cpp @@ -91,6 +91,7 @@ std::optional, int, int>> decode_image_bytes(con return std::nullopt; } + const std::size_t n = static_cast(w) * static_cast(h) * 4u; std::vector rgba(n); std::memcpy(rgba.data(), data, n); diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 7fe67af..9d1b70c 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -369,256 +369,6 @@ TopoDS_Wire make_slot_wire(const gp_Pln& plane, const gp_Pnt2d& pt_a, const gp_P return wire_maker.Wire(); } -namespace -{ -gp_Pnt2d mirror_across_axis_(const gp_Pnt2d& p, const gp_Pnt2d& c1, const gp_Vec2d& axis) -{ - const double len = axis.Magnitude(); - if (len <= Precision::Confusion()) - return p; - - const gp_Vec2d a(axis / len); - gp_Vec2d v(c1, p); - const double along = v.Dot(a); - gp_Vec2d perp = v - a * along; - return gp_Pnt2d(c1).Translated(a * along - perp); -} - -std::optional circle_circle_intersect_pick_side_(const gp_Pnt2d& c1, double ra, const gp_Pnt2d& c2, double rb, - const gp_Pnt2d& mid, const gp_Vec2d& n, bool positive_n_side) -{ - const double eps = Precision::Confusion(); - gp_Vec2d axis(c1, c2); - const double d = axis.Magnitude(); - if (d <= eps || ra <= eps || rb <= eps) - return std::nullopt; - - if (d > ra + rb + eps || d + eps < std::fabs(ra - rb)) - return std::nullopt; - - const double a = (ra * ra - rb * rb + d * d) / (2.0 * d); - const double h2 = ra * ra - a * a; - if (h2 < -eps) - return std::nullopt; - - const double h = std::sqrt(std::max(0.0, h2)); - const gp_Vec2d ad(axis / d); - gp_Pnt2d p2 = gp_Pnt2d(c1).Translated(ad * a); - gp_Vec2d perp(-ad.Y() * h, ad.X() * h); - - const gp_Pnt2d i0 = p2.Translated(perp); - const gp_Pnt2d i1 = p2.Translated(-perp); - - const double d0 = gp_Vec2d(mid, i0).Dot(n); - const double d1 = gp_Vec2d(mid, i1).Dot(n); - if (positive_n_side) - return d0 >= d1 ? i0 : i1; - return d0 <= d1 ? i0 : i1; -} - -double bone_waist_gap_(const gp_Pnt2d& c_top, const gp_Pnt2d& c_bot, double cut_r) -{ - // Min gap between the two cutter circles (along the line of centers). - return c_top.Distance(c_bot) - 2.0 * cut_r; -} - -std::optional bone_geom_with_cut_radius_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, - double cut_r, const gp_Vec2d& axis, const gp_Vec2d& n, - const gp_Pnt2d& mid, double vx, double vy, double a, double h) -{ - const double eps = Precision::Confusion(); - if (cut_r <= eps) - return std::nullopt; - - const std::optional c_top = - circle_circle_intersect_pick_side_(c1, cut_r + r1, c2, cut_r + r2, mid, n, true); - if (!c_top) - return std::nullopt; - - Bone_geom g; - g.c1 = c1; - g.c2 = c2; - g.r1 = r1; - g.r2 = r2; - g.cut_radius = cut_r; - g.cut_plus = *c_top; - g.cut_minus = mirror_across_axis_(*c_top, c1, axis); - g.waist = bone_waist_gap_(g.cut_plus, g.cut_minus, cut_r); - - auto tangent_pair = [&](double sign, gp_Pnt2d& t1, gp_Pnt2d& t2) - { - const double nx = vx * a - sign * vy * h; - const double ny = vy * a + sign * vx * h; - t1 = gp_Pnt2d(c1.X() + r1 * nx, c1.Y() + r1 * ny); - t2 = gp_Pnt2d(c2.X() + r2 * nx, c2.Y() + r2 * ny); - }; - - tangent_pair(1.0, g.tan_top_a, g.tan_top_b); - tangent_pair(-1.0, g.tan_bot_a, g.tan_bot_b); - return g; -} - -double bone_waist_for_cut_radius_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_r, - const gp_Vec2d& axis, const gp_Vec2d& n, const gp_Pnt2d& mid, double vx, double vy, - double a, double h) -{ - const std::optional g = bone_geom_with_cut_radius_(c1, c2, r1, r2, cut_r, axis, n, mid, vx, vy, a, h); - if (!g) - return std::numeric_limits::quiet_NaN(); - return g->waist; -} - -std::optional solve_cut_radius_for_waist_(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, - double target_waist, const gp_Vec2d& axis, const gp_Vec2d& n, - const gp_Pnt2d& mid, double vx, double vy, double a, double h) -{ - const double eps = Precision::Confusion(); - if (target_waist <= eps) - return std::nullopt; - - const double dist = axis.Magnitude(); - double r_lo = (dist - r1 - r2) / 2.0; - if (r_lo <= eps) - r_lo = eps; - - double w_lo = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_lo, axis, n, mid, vx, vy, a, h); - if (!std::isfinite(w_lo)) - return std::nullopt; - - if (w_lo >= target_waist) - return std::nullopt; - - double r_hi = std::max(r_lo + 1.0, r1 + r2); - for (int i = 0; i < 48; ++i) - { - const double w_hi = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_hi, axis, n, mid, vx, vy, a, h); - if (!std::isfinite(w_hi) || w_hi < target_waist) - r_hi *= 2.0; - else - break; - } - - const double w_hi = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_hi, axis, n, mid, vx, vy, a, h); - if (!std::isfinite(w_hi) || w_hi < target_waist) - return std::nullopt; - - for (int i = 0; i < 64; ++i) - { - const double r_mid = (r_lo + r_hi) * 0.5; - const double w_mid = bone_waist_for_cut_radius_(c1, c2, r1, r2, r_mid, axis, n, mid, vx, vy, a, h); - if (!std::isfinite(w_mid)) - return std::nullopt; - - if (w_mid < target_waist) - r_lo = r_mid; - else - r_hi = r_mid; - } - - return (r_lo + r_hi) * 0.5; -} -} // namespace - -std::optional compute_bone_geom(const Bone_params& p) -{ - const double eps = Precision::Confusion(); - if (p.r1 <= eps || p.r2 <= eps) - return std::nullopt; - - if (p.drive == Bone_drive::Cut_radius && p.cut_radius <= eps) - return std::nullopt; - if (p.drive == Bone_drive::Waist && p.waist <= eps) - return std::nullopt; - - const gp_Vec2d axis(p.c1, p.c2); - const double dist = axis.Magnitude(); - if (dist <= eps) - return std::nullopt; - - if (dist + eps < std::fabs(p.r1 - p.r2)) - return std::nullopt; - - const double vx = axis.X() / dist; - const double vy = axis.Y() / dist; - const double a = (p.r1 - p.r2) / dist; - const double h2 = 1.0 - a * a; - if (h2 < -eps) - return std::nullopt; - - const double h = std::sqrt(std::max(0.0, h2)); - const gp_Pnt2d mid = get_midpoint(p.c1, p.c2); - const gp_Vec2d n = gp_Vec2d(vx, vy).Rotated(std::numbers::pi / 2.0); - - double cut_r = p.cut_radius; - if (p.drive == Bone_drive::Waist) - { - const std::optional solved = - solve_cut_radius_for_waist_(p.c1, p.c2, p.r1, p.r2, p.waist, axis, n, mid, vx, vy, a, h); - if (!solved) - return std::nullopt; - cut_r = *solved; - } - - return bone_geom_with_cut_radius_(p.c1, p.c2, p.r1, p.r2, cut_r, axis, n, mid, vx, vy, a, h); -} - -Bone_profile get_bone_profile(const Bone_geom& g) -{ - gp_Vec2d axis(g.c1, g.c2); - const double dist = axis.Magnitude(); - EZY_ASSERT(dist > Precision::Confusion()); - const gp_Vec2d ad(axis / dist); - - auto contact = [](const gp_Pnt2d& c, double r, const gp_Pnt2d& cut) -> gp_Pnt2d - { - gp_Vec2d v(c, cut); - const double d = v.Magnitude(); - EZY_ASSERT(d > Precision::Confusion()); - return gp_Pnt2d(c).Translated(v * (r / d)); - }; - - Bone_profile p; - p.c1_plus = contact(g.c1, g.r1, g.cut_plus); - p.c1_minus = contact(g.c1, g.r1, g.cut_minus); - p.c2_plus = contact(g.c2, g.r2, g.cut_plus); - p.c2_minus = contact(g.c2, g.r2, g.cut_minus); - p.c1_outer = gp_Pnt2d(g.c1).Translated(-ad * g.r1); - p.c2_outer = gp_Pnt2d(g.c2).Translated(ad * g.r2); - - // Skinniest section: points of closest approach between the two cutter circles. - // When r1 != r2 this is offset along the bone axis from the center midpoint. - gp_Vec2d to_minus(g.cut_plus, g.cut_minus); - const double cut_sep = to_minus.Magnitude(); - EZY_ASSERT(cut_sep > Precision::Confusion()); - to_minus /= cut_sep; - p.waist_plus = gp_Pnt2d(g.cut_plus).Translated(to_minus * g.cut_radius); - p.waist_minus = gp_Pnt2d(g.cut_minus).Translated(-to_minus * g.cut_radius); - return p; -} - -TopoDS_Wire make_bone_wire(const gp_Pln& pln, const Bone_geom& g) -{ - const Bone_profile p = get_bone_profile(g); - - auto arc = [&](const gp_Pnt2d& a, const gp_Pnt2d& mid, const gp_Pnt2d& b) -> TopoDS_Edge - { - GC_MakeArcOfCircle maker(to_3d(pln, a), to_3d(pln, mid), to_3d(pln, b)); - return BRepBuilderAPI_MakeEdge(maker.Value()).Edge(); - }; - - BRepBuilderAPI_MakeWire wire; - wire.Add(arc(p.c1_minus, p.c1_outer, p.c1_plus)); - wire.Add(arc(p.c1_plus, p.waist_plus, p.c2_plus)); - wire.Add(arc(p.c2_plus, p.c2_outer, p.c2_minus)); - wire.Add(arc(p.c2_minus, p.waist_minus, p.c1_minus)); - return wire.Wire(); -} - -TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g) -{ - return make_bone_wire(pln, g); -} - // Function to get the directional vectors at the start and end of a Geom_TrimmedCurve std::pair get_start_end_tangents(const Geom_TrimmedCurve_ptr& curve) { @@ -1566,6 +1316,7 @@ bool ezy_geom::is_valid(const polygon_2d& poly) return false; // The construction logic in to_boost already guarantees closed, oriented rings. // This is intentionally lightweight. + return true; } diff --git a/src/utl_geom.h b/src/utl_geom.h index 92d2bc3..9897a96 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -77,65 +77,6 @@ Slot_pnts get_slot_points(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const gp_P TopoDS_Wire make_slot_wire(const gp_Pln& plane, const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const gp_Pnt2d& pt_c); -/// Which dialog value drives the waist cutter radius (the other is derived). -enum class Bone_drive -{ - Cut_radius, - Waist, -}; - -/// Two end circles, two waist cutters, and the capsule external tangents. -struct Bone_params -{ - gp_Pnt2d c1; - gp_Pnt2d c2; - double r1{0}; - double r2{0}; - double cut_radius{0}; - double waist{0}; - Bone_drive drive{Bone_drive::Waist}; -}; - -struct Bone_geom -{ - gp_Pnt2d c1; - gp_Pnt2d c2; - gp_Pnt2d cut_plus; - gp_Pnt2d cut_minus; - double r1{0}; - double r2{0}; - double cut_radius{0}; - double waist{0}; - gp_Pnt2d tan_top_a; - gp_Pnt2d tan_top_b; - gp_Pnt2d tan_bot_a; - gp_Pnt2d tan_bot_b; -}; - -/// Trimmed bone outline: outer end-circle caps and inner waist arcs (no full circles, no capsule tangents). -struct Bone_profile -{ - gp_Pnt2d c1_plus; // contact, end 1 / waist cutter + - gp_Pnt2d c1_outer; // outer pole of end 1 - gp_Pnt2d c1_minus; // contact, end 1 / waist cutter - - gp_Pnt2d c2_plus; - gp_Pnt2d c2_outer; - gp_Pnt2d c2_minus; - gp_Pnt2d waist_plus; // inner bulge of cutter + - gp_Pnt2d waist_minus; -}; - -/// Null when centers coincide, a circle is inside the other, or waist/cut radius cannot be solved. -std::optional compute_bone_geom(const Bone_params& p); - -Bone_profile get_bone_profile(const Bone_geom& g); - -/// Closed wire of the four outline arcs (preview and extrusion profile). -TopoDS_Wire make_bone_wire(const gp_Pln& pln, const Bone_geom& g); - -/// Same as \\a make_bone_wire (AIS preview). -TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g); - // Function to get the directional vectors at the start and end of a Geom_TrimmedCurve std::pair get_start_end_tangents(const Geom_TrimmedCurve_ptr& curve); diff --git a/src/utl_settings.cpp b/src/utl_settings.cpp index 99c8b09..bd6bcde 100644 --- a/src/utl_settings.cpp +++ b/src/utl_settings.cpp @@ -118,6 +118,7 @@ std::string load_defaults() return {}; } + std::ostringstream os; os << f.rdbuf(); std::string result = os.str(); diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 50d36ba..056a9e5 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -14,6 +14,7 @@ #include "skt_edge.h" #include "skt_json.h" #include "skt_nodes.h" +#include "skt_bone.h" #include "utl_geom.h" #include "utl_types.h"