From fbb9f2ae35a6055c0b7006d2ea4cf566f090fa14 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 2 Sep 2026 17:23:16 -0600 Subject: [PATCH 01/12] Start --- CHANGELOG.md | 2 + docs/usage-sketch.md | 41 +++++++- docs/usage.md | 2 + res/ezycad_settings.json | 1 + src/doc/gui.md | 2 +- src/doc/sketch.md | 2 +- src/gui.cpp | 5 + src/gui.h | 11 ++ src/gui_add.cpp | 86 ++++++++++++++++ src/gui_hotkeys.cpp | 1 + src/gui_hotkeys.h | 1 + src/gui_mode.cpp | 14 +++ src/mode.cpp | 1 + src/mode.h | 1 + src/skt.cpp | 65 ++++++++++++ src/skt.h | 5 + src/skt_tools.cpp | 147 ++++++++++++++++++++++++++- src/skt_tools.h | 12 +++ src/utl_geom.cpp | 210 +++++++++++++++++++++++++++++++++++++++ src/utl_geom.h | 42 ++++++++ tests/skt_tests.cpp | 86 ++++++++++++++++ 21 files changed, 729 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04955a2b..0d2678f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Add bone** sketch tool (Shift+U): click two circle centers, then a dialog for the two end radii, waist cut radius, and waist thickness. Waist cutters are tangent to both end circles; edit **Waist** or **Cut radius** and the other value updates. Commits end circles, waist cutters, capsule tangents, and permanent **Bone A** / **Bone B** center nodes. 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). - **Shape List Zoom to**: right-click a shape or group name (or the **M** button on a solid) and choose **Zoom to** to frame that solid, or all descendant solids of a group, in the 3D view while keeping the current camera orientation. diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 81b83430..bafa4a50 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -14,11 +14,12 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main 9. [Arc Segment Creation Tool](#arc-segment-creation-tool) 10. [Rectangle and Square Creation Tools](#rectangle-and-square-creation-tools) 11. [Slot Creation Tool](#slot-creation-tool) -12. [Operation Axis Tool](#operation-axis-tool) -13. [Dimension Tool](#dimension-tool) -14. [Add Node Tool](#add-node-tool) -15. [Create Sketch from Planar Face Tool](#create-sketch-from-planar-face-tool) -16. [Image underlay](#image-underlay) +12. [Bone Creation Tool](#bone-creation-tool) +13. [Operation Axis Tool](#operation-axis-tool) +14. [Dimension Tool](#dimension-tool) +15. [Add Node Tool](#add-node-tool) +16. [Create Sketch from Planar Face Tool](#create-sketch-from-planar-face-tool) +17. [Image underlay](#image-underlay) --- @@ -31,6 +32,7 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main - ![Circle Tool](res/icons/Sketcher_CreateCircle.png) [Create circles](#circle-creation-tools) - ![Rectangle Tool](res/icons/Sketcher_CreateRectangle.png) ![Square Tool](res/icons/Sketcher_CreateSquare.png) [Draw rectangles and squares](#rectangle-and-square-creation-tools) - ![Slot Tool](res/icons/Sketcher_CreateSlot.png) [Add slots](#slot-creation-tool) + - ![Bone Tool](res/icons/Sketcher_CreateCircle.png) [Add bone](#bone-creation-tool) - ![Dimension Tool](res/icons/TechDraw_LengthDimension.png) [Dimension tool](#dimension-tool) - ![Add Node Tool](res/icons/Sketcher_CreatePoint.png) [Add nodes](#add-node-tool) @@ -109,6 +111,7 @@ Common keyboard shortcuts (hotkeys) while working in 2D sketch mode or with sket | Q / B / Shift+B | Add square / rectangle / rectangle from center (defaults; remappable) | | O / Shift+O | Add circle / three-point circle (defaults; remappable) | | U | Add slot (default; remappable) | +| Shift+U | Add bone (default; remappable) | | P | Sketch from planar face (default; remappable) | | D | Activate the Dimension tool (default; remappable) | | Shift+D / Delete / Backspace | Delete the selected sketch element(s) or dimension | @@ -705,6 +708,34 @@ The slot tool allows you to create an oblong or oval-shaped slot with rounded en - Creating rounded-end cutouts in parts - Designing slots for sliding mechanisms +(bone-creation-tool)= +## Bone Creation Tool + +![Bone Tool](res/icons/Sketcher_CreateCircle.png) + +The bone tool builds a connecting-rod profile: two end circles, two waist cut circles, and the capsule's external tangents. Topology splits those edges into a bone-shaped face you can extrude. Permanent nodes **Bone A** and **Bone B** stay at the two centers for later holes, dimensions, or an operation axis. + +**How to use:** + +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. In the **Add bone** dialog, enter: + - **Radius 1** / **Radius 2** — end circle radii + - **Cut radius** — both waist cutters (mirrored across the bone axis) + - **Waist** — remaining thickness at the midpoint, perpendicular to the center line +5. A live preview updates as you edit. **Add** commits one undo step; **Cancel** or Esc drops the preview. + +Values are in project units (same as other Add dialogs). **Add** stays disabled when a radius or the waist is not positive. Waist cutters are tangent to both end circles; edit **Waist** or **Cut radius** and the other value updates to match. Geometry also fails when the two centers coincide or one circle sits inside the other (no external tangents). + +The sketch keeps the four full circles plus the two tangent segments. Extrude the leftover bone face with the normal **Extrude sketch face** tool. + +**Tips:** + +- Snap the two centers to existing nodes, or to the sketch origin. +- After Add, use **Add circle** snapped to **Bone A** / **Bone B** if you want a different hole radius than the end circles. +- **Waist** and **Cut radius** are linked: the last field you edit drives the solve; the other updates in the dialog. + ## Operation Axis Tool ![Operation Axis Tool](res/icons/Sketcher_MirrorSketch.png) diff --git a/docs/usage.md b/docs/usage.md index 666df3a9..5438842d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -772,6 +772,7 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu | O | Add circle | | Shift+O | Add circle (three points) | | U | Add slot | +| Shift+U | Add bone | | Shift+P | Polar duplicate | | Shift+X | Cross-section | | Ctrl+Shift+C | Shape cut | @@ -928,6 +929,7 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for - ![Sketcher_CreateCircle](res/icons/Sketcher_CreateCircle.png) - Add circle (center and radius) - ![Sketcher_Create3PointCircle](res/icons/Sketcher_Create3PointCircle.png) - Add circle from three points *(planned feature)* - ![Sketcher_CreateSlot](res/icons/Sketcher_CreateSlot.png) - Add slot +- ![Sketcher_CreateCircle](res/icons/Sketcher_CreateCircle.png) - Add bone (Shift+U) - ![TechDraw_LengthDimension](res/icons/TechDraw_LengthDimension.png) - Dimension tool (D) ### 3D Operations diff --git a/res/ezycad_settings.json b/res/ezycad_settings.json index 7741f053..16c5c880 100644 --- a/res/ezycad_settings.json +++ b/res/ezycad_settings.json @@ -53,6 +53,7 @@ "mode.add_circle": "O", "mode.add_circle_3_pts": "Shift+O", "mode.add_slot": "U", + "mode.add_bone": "Shift+U", "mode.polar_duplicate": "Shift+P", "mode.cross_section": "Shift+X", "mode.cyl_align": "J", diff --git a/src/doc/gui.md b/src/doc/gui.md index baf8bb8b..4336ba61 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -184,7 +184,7 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for | `Shape_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | | `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | | `Shape_polar_duplicate` | `shp_polar_dup().move_point` | -| Sketch tool modes (line, arc, rect, dim, axis, ...) | `curr_sketch().sketch_pt_move` | +| Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | | `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Always calls `m_view->on_mouse_move(screen_coords)` first. diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 79ce21c1..13e51d64 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -12,7 +12,7 @@ The class is a **coordinator**: it holds shared state (plane, viewer context, vi Typical uses: -- Interactive creation and editing via sketch tools (line, arc, rectangle, slot, add-node, dimension, operation axis). +- Interactive creation and editing via sketch tools (line, arc, rectangle, slot, bone, add-node, dimension, operation axis). - Face extraction for extrusion and revolve into 3D solids. - Mirror selected edges about an operation axis. - JSON save/load and undo/redo through stable sketch and node identity. diff --git a/src/gui.cpp b/src/gui.cpp index dcb05141..dc478978 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -165,6 +165,7 @@ void GUI::render_gui() add_cone_dialog_(); add_torus_dialog_(); add_sketch_dialog_(); + add_bone_dialog_(); log_window_(); lua_console_(); python_console_(); @@ -197,6 +198,7 @@ void GUI::initialize_toolbar_() {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add circle", Mode::Sketch_add_circle}, {load_texture("res/icons/Sketcher_Create3PointCircle.png"), false, "Add circle from three points", Mode::Sketch_add_circle_3_pts}, {load_texture("res/icons/Sketcher_CreateSlot.png"), false, "Add slot", Mode::Sketch_add_slot}, + {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add bone", Mode::Sketch_add_bone}, {load_texture("res/icons/TechDraw_LengthDimension.png"), false, "Length dimension", Mode::Sketch_dim_anno}, {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude}, {load_texture("res/icons/PartDesign_Chamfer.png"), false, "Chamfer", Mode::Shape_chamfer}, @@ -262,6 +264,7 @@ void GUI::sync_toolbar_hotkey_tooltips_() tip_mode(Mode::Sketch_add_circle, "Add circle", Gui_action::Mode_add_circle); tip_mode(Mode::Sketch_add_circle_3_pts, "Add circle from three points", Gui_action::Mode_add_circle_3_pts); tip_mode(Mode::Sketch_add_slot, "Add slot", Gui_action::Mode_add_slot); + tip_mode(Mode::Sketch_add_bone, "Add bone", Gui_action::Mode_add_bone); tip_mode(Mode::Shape_polar_duplicate, "Shape polar duplicate", Gui_action::Mode_polar_duplicate); tip_mode(Mode::Shape_cross_section, "Shape cross-section", Gui_action::Mode_cross_section); tip_cmd(Command::Shape_cut, "Shape cut", Gui_action::Cmd_shape_cut); @@ -4020,6 +4023,7 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) case Mode::Sketch_add_rectangle_center_pt: case Mode::Sketch_add_circle: case Mode::Sketch_add_slot: + case Mode::Sketch_add_bone: case Mode::Sketch_add_seg_circle_arc: case Mode::Sketch_dim_anno: m_view->curr_sketch().sketch_pt_move(screen_coords); break; case Mode::Sketch_face_extrude: m_view->sketch_face_extrude(screen_coords, true); break; @@ -4061,6 +4065,7 @@ void GUI::on_left_click_(const ScreenCoords& screen_coords) case Mode::Sketch_add_rectangle_center_pt: case Mode::Sketch_add_circle: case Mode::Sketch_add_slot: + case Mode::Sketch_add_bone: hide_dist_edit(); m_view->curr_sketch().add_sketch_pt(screen_coords); break; diff --git a/src/gui.h b/src/gui.h index e7949fa2..1a673509 100644 --- a/src/gui.h +++ b/src/gui.h @@ -336,6 +336,8 @@ class GUI void show_message(const std::string& message); /// Queue an ImGui error modal (logs title: message once; short title toast). Native and wasm. void show_error_dialog(const std::string& title, const std::string& message); + void open_add_bone_dialog(); + void dismiss_add_bone_dialog(); void log_message(const std::string& message); void set_show_options(bool v) { m_show_options = v; } void set_show_sketch_list(bool v) { m_show_sketch_list = v; } @@ -472,6 +474,7 @@ class GUI void options_sketch_add_circle_mode_(); void options_sketch_add_circle_three_pts_mode_(); void options_sketch_add_slot_mode_(); + void options_sketch_add_bone_mode_(); const char* current_mode_description_() const; @@ -511,6 +514,7 @@ class GUI void add_cone_dialog_(); void add_torus_dialog_(); void add_sketch_dialog_(); + void add_bone_dialog_(); void add_menu_items_(); void about_dialog_(); @@ -763,6 +767,13 @@ class GUI glm::dvec3 m_add_torus_origin{0.0, 0.0, 0.0}; double m_add_torus_R1{1}, m_add_torus_R2{0.5}; bool m_open_add_sketch_popup{false}; + bool m_open_add_bone_popup{false}; + bool m_add_bone_dialog_open{false}; + double m_add_bone_r1{1.0}; + double m_add_bone_r2{0.5}; + double m_add_bone_cut_radius{2.0}; + double m_add_bone_waist{0.4}; + Bone_drive m_add_bone_drive{Bone_drive::Waist}; int m_new_sketch_plane{0}; // 0=XY, 1=XZ, 2=YZ double m_new_sketch_offset{}; bool m_hide_all_shapes{false}; diff --git a/src/gui_add.cpp b/src/gui_add.cpp index 82cd61c4..a925a062 100644 --- a/src/gui_add.cpp +++ b/src/gui_add.cpp @@ -1,6 +1,7 @@ #include "gui.h" #include "utl_geom.h" #include "gui_occt_view.h" +#include "skt.h" namespace { @@ -407,6 +408,91 @@ void GUI::add_menu_items_() } } +void GUI::open_add_bone_dialog() +{ + m_open_add_bone_popup = true; + m_add_bone_dialog_open = true; +} + +void GUI::dismiss_add_bone_dialog() +{ + m_open_add_bone_popup = false; + m_add_bone_dialog_open = false; +} + +void GUI::add_bone_dialog_() +{ + if (m_open_add_bone_popup) + { + ImGui::OpenPopup("Add bone"); + m_open_add_bone_popup = false; + m_add_bone_dialog_open = true; + } + + if (!m_add_bone_dialog_open) + return; + + if (!ImGui::BeginPopupModal("Add bone", &m_add_bone_dialog_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (!m_add_bone_dialog_open && m_view) + m_view->curr_sketch().cancel_elm(); + + return; + } + + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); + ImGui::Spacing(); + if (ImGui::BeginTable("Add bone##table", 2, ImGuiTableFlags_SizingStretchProp)) + { + table_row_input_double_("Radius 1", "##bone_r1", &m_add_bone_r1); + table_row_input_double_("Radius 2", "##bone_r2", &m_add_bone_r2); + table_row_input_double_("Cut radius", "##bone_cut", &m_add_bone_cut_radius); + if (ImGui::IsItemDeactivatedAfterEdit()) + m_add_bone_drive = Bone_drive::Cut_radius; + table_row_input_double_("Waist", "##bone_waist", &m_add_bone_waist); + if (ImGui::IsItemDeactivatedAfterEdit()) + m_add_bone_drive = Bone_drive::Waist; + ImGui::EndTable(); + } + + const double scale = m_view->get_display_to_model_scale(); + const double r1 = m_add_bone_r1 * scale; + const double r2 = m_add_bone_r2 * scale; + const double cut = m_add_bone_cut_radius * scale; + const double waist = m_add_bone_waist * scale; + m_view->curr_sketch().update_bone_preview(r1, r2, cut, waist, m_add_bone_drive); + + if (const std::optional g = m_view->curr_sketch().last_bone_preview_geom()) + { + const double inv = 1.0 / scale; + if (m_add_bone_drive == Bone_drive::Waist) + m_add_bone_cut_radius = g->cut_radius * inv; + else + m_add_bone_waist = g->waist * inv; + } + + const bool can_add = m_add_bone_r1 > 0 && m_add_bone_r2 > 0 && m_add_bone_cut_radius > 0 && m_add_bone_waist > 0; + ImGui::Spacing(); + if (ImGui::Button("Add") && can_add) + { + if (m_view->curr_sketch().commit_pending_bone(r1, r2, cut, waist, m_add_bone_drive)) + { + ImGui::CloseCurrentPopup(); + m_add_bone_dialog_open = false; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + m_view->curr_sketch().cancel_elm(); + ImGui::CloseCurrentPopup(); + m_add_bone_dialog_open = false; + } + + ImGui::EndPopup(); +} + namespace { void table_row_input_double_(const char* label, const char* id, double* value) diff --git a/src/gui_hotkeys.cpp b/src/gui_hotkeys.cpp index 3d5ac885..5f589f47 100644 --- a/src/gui_hotkeys.cpp +++ b/src/gui_hotkeys.cpp @@ -42,6 +42,7 @@ constexpr Action_meta c_actions[] = { {Gui_action::Mode_add_circle, "mode.add_circle", "Add circle", {GLFW_KEY_O, 0}}, {Gui_action::Mode_add_circle_3_pts, "mode.add_circle_3_pts", "Add circle (3 pts)", {GLFW_KEY_O, GLFW_MOD_SHIFT}}, {Gui_action::Mode_add_slot, "mode.add_slot", "Add slot", {GLFW_KEY_U, 0}}, + {Gui_action::Mode_add_bone, "mode.add_bone", "Add bone", {GLFW_KEY_U, GLFW_MOD_SHIFT}}, {Gui_action::Mode_polar_duplicate, "mode.polar_duplicate", "Polar duplicate", {GLFW_KEY_P, GLFW_MOD_SHIFT}}, {Gui_action::Mode_cross_section, "mode.cross_section", "Cross-section", {GLFW_KEY_X, GLFW_MOD_SHIFT}}, {Gui_action::Mode_cyl_align, "mode.cyl_align", "Align shafts", {GLFW_KEY_J, 0}}, diff --git a/src/gui_hotkeys.h b/src/gui_hotkeys.h index 2bdd992c..06eeed21 100644 --- a/src/gui_hotkeys.h +++ b/src/gui_hotkeys.h @@ -30,6 +30,7 @@ enum class Gui_action Mode_add_circle, Mode_add_circle_3_pts, Mode_add_slot, + Mode_add_bone, Mode_polar_duplicate, Mode_cross_section, Mode_cyl_align, diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 74faaca4..0e9aad03 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -64,6 +64,7 @@ std::string GUI::get_doc_url_for_mode(Mode mode) {Mode::Sketch_add_circle, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#circle-creation-tools"}, {Mode::Sketch_add_circle_3_pts, ""}, // planned feature - no specific section in the docs yet; falls back to main guide {Mode::Sketch_add_slot, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#slot-creation-tool"}, + {Mode::Sketch_add_bone, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#bone-creation-tool"}, {Mode::Sketch_dim_anno, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#dimension-tool"}, {Mode::Shape_cross_section, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-cross-section-tool"}, {Mode::Shape_set_frame, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-list"}, @@ -148,6 +149,7 @@ Mode GUI::parent_mode_of(Mode mode) {Mode::Sketch_add_circle, Mode::Sketch_inspection_mode}, {Mode::Sketch_add_circle_3_pts, Mode::Sketch_inspection_mode}, {Mode::Sketch_add_slot, Mode::Sketch_inspection_mode}, + {Mode::Sketch_add_bone, Mode::Sketch_inspection_mode}, {Mode::Sketch_dim_anno, Mode::Sketch_inspection_mode}, {Mode::Shape_cross_section, Mode::Normal}, {Mode::Shape_set_frame, Mode::Normal}, @@ -402,6 +404,7 @@ void GUI::dispatch_hotkey_action_(Gui_action action) case Gui_action::Mode_add_circle: set_mode(Mode::Sketch_add_circle); break; case Gui_action::Mode_add_circle_3_pts: set_mode(Mode::Sketch_add_circle_3_pts); break; case Gui_action::Mode_add_slot: set_mode(Mode::Sketch_add_slot); break; + case Gui_action::Mode_add_bone: set_mode(Mode::Sketch_add_bone); break; case Gui_action::Mode_polar_duplicate: set_mode(Mode::Shape_polar_duplicate); break; case Gui_action::Mode_cross_section: set_mode(Mode::Shape_cross_section); break; case Gui_action::Cmd_shape_cut: @@ -536,6 +539,7 @@ void GUI::options_() case Mode::Sketch_add_circle: options_sketch_add_circle_mode_(); break; case Mode::Sketch_add_circle_3_pts: options_sketch_add_circle_three_pts_mode_(); break; case Mode::Sketch_add_slot: options_sketch_add_slot_mode_(); break; + case Mode::Sketch_add_bone: options_sketch_add_bone_mode_(); break; default: EZY_ASSERT_MSG(false, "Options panel: unhandled mode"); break; @@ -1252,6 +1256,16 @@ void GUI::options_sketch_add_slot_mode_() options_sketch_add_midpoint_nodes_(m_add_mid_pt_slot_edges); } +void GUI::options_sketch_add_bone_mode_() +{ + EZY_ASSERT(get_mode() == Mode::Sketch_add_bone); + + options_sketch_common_(); + options_sketch_len_angle_hotkeys_(); + ImGui::Separator(); + ImGui::TextWrapped("Click the two circle centers, then enter radii and waist thickness."); +} + void GUI::options_orthographic_projection_() { ImGui::Separator(); diff --git a/src/mode.cpp b/src/mode.cpp index 50ad9dee..dd489652 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -24,6 +24,7 @@ bool is_sketch_mode(const Mode mode) case Mode::Sketch_add_circle: case Mode::Sketch_add_circle_3_pts: case Mode::Sketch_add_slot: + case Mode::Sketch_add_bone: case Mode::Sketch_operation_axis: case Mode::Sketch_dim_anno: case Mode::Sketch_face_extrude: diff --git a/src/mode.h b/src/mode.h index 6d1ff60e..4e800247 100644 --- a/src/mode.h +++ b/src/mode.h @@ -28,6 +28,7 @@ X(Sketch_add_circle) \ X(Sketch_add_circle_3_pts) \ X(Sketch_add_slot) \ + X(Sketch_add_bone) /* two centers, then radii / waist dialog */ \ X(Sketch_dim_anno) \ X(Shape_cross_section) \ X(Shape_shaft_align) \ diff --git a/src/skt.cpp b/src/skt.cpp index 15926fd8..71c41589 100644 --- a/src/skt.cpp +++ b/src/skt.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -151,6 +152,70 @@ void Sketch::add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const void Sketch::rebuild_faces() { update_faces_(); } +void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_radius, double waist) +{ + Bone_params params; + params.c1 = c1; + params.c2 = c2; + params.r1 = r1; + params.r2 = r2; + params.cut_radius = cut_radius; + params.waist = waist; + params.drive = Bone_drive::Cut_radius; + const std::optional g = compute_bone_geom(params); + if (!g) + return; + + Sketch_op_recorder rec(m_view, *this); + { + auto add_circle = [&](const gp_Pnt2d& center, double radius) + { + const gp_Pnt2d edge(center.X() + radius, center.Y()); + std::array pts = xy_stencil_pnts(center, edge); + add_arc_circle_(pts[0], pts[2], pts[1], rec); + add_arc_circle_(pts[0], pts[3], pts[1], rec); + }; + + auto mark_center = [&](const gp_Pnt2d& c, const char* name) + { + const size_t idx = m_nodes.get_node_exact(c, true); + Sketch_nodes::Node& n = m_nodes[idx]; + n.permanent = true; + n.name = name; + rec.note_curr_node(idx); + }; + + mark_center(g->c1, "Bone A"); + mark_center(g->c2, "Bone B"); + add_circle(g->c1, g->r1); + add_circle(g->c2, g->r2); + add_circle(g->cut_plus, g->cut_radius); + add_circle(g->cut_minus, g->cut_radius); + add_edge_(g->tan_top_a, g->tan_top_b, rec); + add_edge_(g->tan_bot_a, g->tan_bot_b, rec); + rec.commit(); + } + + m_nodes.finalize(); + m_node_marks.sync(); + update_faces_(); +} + +void Sketch::update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive) +{ + m_tools.update_bone_preview(r1, r2, cut_radius, waist, drive); +} + +bool Sketch::commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive) +{ + return m_tools.commit_pending_bone(r1, r2, cut_radius, waist, drive); +} + +const std::optional& Sketch::last_bone_preview_geom() const +{ + return m_tools.last_bone_preview_geom(); +} + void Sketch::add_edge_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, Sketch_op_recorder& rec) { m_edges.add_edge(pt_a, pt_b, rec); diff --git a/src/skt.h b/src/skt.h index 391f2aff..a840dfa4 100644 --- a/src/skt.h +++ b/src/skt.h @@ -167,6 +167,11 @@ class Sketch void add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const gp_Pnt2d& pt_c); /// Rebuild closed-face topology after bulk edge import. void rebuild_faces(); + /// Add a bone profile (end circles, waist cutters, capsule tangents) and permanent center nodes. + void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_radius, double waist); + void update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive); + [[nodiscard]] bool commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive); + [[nodiscard]] const std::optional& last_bone_preview_geom() const; private: friend class Sketch_json; diff --git a/src/skt_tools.cpp b/src/skt_tools.cpp index 73c1686f..765de40a 100644 --- a/src/skt_tools.cpp +++ b/src/skt_tools.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include "gui.h" #include "mode.h" @@ -55,6 +58,7 @@ void Sketch_tools::on_click(const ScreenCoords& screen_coords) case Mode::Sketch_add_node: add_node_pt_ (screen_coords); break; case Mode::Sketch_add_edge: add_line_string_pt_ (screen_coords, Linestring_type::Single); break; case Mode::Sketch_add_slot: add_line_string_pt_ (screen_coords, Linestring_type::Two); break; + case Mode::Sketch_add_bone: add_bone_pt_ (screen_coords); break; case Mode::Sketch_add_multi_edges: add_line_string_pt_ (screen_coords, Linestring_type::Multiple); break; case Mode::Sketch_add_seg_circle_arc: add_arc_circle_pt_ (screen_coords); break; case Mode::Sketch_operation_axis: add_operation_axis_pt_(screen_coords); break; @@ -74,6 +78,7 @@ void Sketch_tools::on_move(const ScreenCoords& screen_coords) case Mode::Sketch_add_square: move_square_pt_ (screen_coords); break; case Mode::Sketch_add_circle: move_circle_pt_ (screen_coords); break; case Mode::Sketch_add_slot: move_slot_pt_ (screen_coords); break; + case Mode::Sketch_add_bone: move_bone_pt_ (screen_coords); break; case Mode::Sketch_add_edge: case Mode::Sketch_operation_axis: @@ -112,6 +117,27 @@ void Sketch_tools::on_enter() m_sketch.m_dims.check_dimension_seg_(static_cast(Linestring_type::Two)); break; + case Mode::Sketch_add_bone: + if (m_bone_centers) + break; + if (!m_tmp_edges.empty() && m_sketch.m_dims.entered_edge_len().has_value()) + { + Sketch_edge& edge = m_tmp_edges.back(); + const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; + m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * + m_sketch.m_dims.entered_edge_len()->len); + if (unique(pt_a, *m_last_pt)) + m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); + + m_sketch.m_dims.clear_typed_constraints(); + } + if (!m_tmp_edges.empty() && m_tmp_edges.back().node_idx_b.has_value()) + { + const Sketch_edge& e = m_tmp_edges.back(); + begin_bone_dialog_from_centers_(m_sketch.m_nodes[e.node_idx_a], m_sketch.m_nodes[*e.node_idx_b]); + } + break; + case Mode::Sketch_add_multi_edges: m_sketch.m_dims.check_dimension_seg_(static_cast(Linestring_type::Multiple)); break; @@ -144,6 +170,7 @@ void Sketch_tools::finalize() case Mode::Sketch_add_circle: finalize_circle_(rec); break; case Mode::Sketch_add_node: finalize_add_node_elm_cleanup_(); break; case Mode::Sketch_add_slot: finalize_slot_(rec); break; + case Mode::Sketch_add_bone: break; case Mode::Sketch_operation_axis: finalize_operation_axis_(rec); break; // clang-format on default: @@ -862,6 +889,121 @@ void Sketch_tools::move_slot_pt_(const ScreenCoords& screen_coords) if_edge_pt_valid_(l); } +void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) +{ + if (m_bone_centers) + return; + + if (m_tmp_edges.empty()) + { + add_line_string_pt_(screen_coords, Linestring_type::Multiple); + return; + } + + auto on_second = [&](size_t node_idx) + { + Sketch_edge& last = m_tmp_edges.back(); + if (node_idx == last.node_idx_a) + return; + + m_sketch.update_edge_end_pt_(last, node_idx); + begin_bone_dialog_from_centers_(m_sketch.m_nodes[last.node_idx_a], m_sketch.m_nodes[node_idx]); + }; + + if (m_sketch.m_dims.entered_edge_angle().has_value() && !m_tmp_edges.empty()) + { + std::optional pt_opt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); + if (!pt_opt) + return; + + const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_edges.back().node_idx_a]; + const double angle_rad = to_radians(*m_sketch.m_dims.entered_edge_angle()); + gp_Dir2d constrained_dir(std::cos(angle_rad), std::sin(angle_rad)); + gp_Vec2d to_click(pt_opt->X() - pt_a.X(), pt_opt->Y() - pt_a.Y()); + const double dist_along = to_click.Dot(gp_Vec2d(constrained_dir)); + gp_Pnt2d final_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(constrained_dir) * dist_along); + if (!unique(pt_a, final_pt)) + return; + + const size_t node_idx = m_sketch.m_nodes.get_node_exact(final_pt); + m_tmp_node_idxs.push_back(node_idx); + on_second(node_idx); + return; + } + + add_sketch_pt_(screen_coords, 1, on_second); +} + +void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) +{ + if (m_bone_centers) + return; + + move_line_string_pt_(screen_coords); +} + +void Sketch_tools::begin_bone_dialog_from_centers_(const gp_Pnt2d& c1, const gp_Pnt2d& c2) +{ + if (!unique(c1, c2)) + return; + + m_bone_centers = std::make_pair(c1, c2); + for (Sketch_edge& e : m_tmp_edges) + m_sketch.m_view.remove(e.shp); + + m_sketch.m_view.gui().open_add_bone_dialog(); +} + +void Sketch_tools::update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive) +{ + if (!m_bone_centers) + return; + + Bone_params params; + params.c1 = m_bone_centers->first; + params.c2 = m_bone_centers->second; + params.r1 = r1; + params.r2 = r2; + params.cut_radius = cut_radius; + params.waist = waist; + params.drive = drive; + + const std::optional g = compute_bone_geom(params); + if (!g) + { + m_last_bone_geom.reset(); + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + m_last_bone_geom = g; + show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); +} + +bool Sketch_tools::commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive) +{ + if (!m_bone_centers) + return false; + + Bone_params params; + params.c1 = m_bone_centers->first; + params.c2 = m_bone_centers->second; + params.r1 = r1; + params.r2 = r2; + params.cut_radius = cut_radius; + params.waist = waist; + params.drive = drive; + + const std::optional g = compute_bone_geom(params); + if (!g) + return false; + + m_sketch.add_bone(m_bone_centers->first, m_bone_centers->second, r1, r2, g->cut_radius, g->waist); + clear_tmps(); + return true; +} + void Sketch_tools::finalize_slot_(Sketch_op_recorder& rec) { EZY_ASSERT(m_tmp_edges.size() == 2); @@ -911,7 +1053,10 @@ bool Sketch_tools::clear_tmps() m_tmp_shp = nullptr; } - const bool operation_canceled = !m_tmp_edges.empty(); + const bool operation_canceled = !m_tmp_edges.empty() || m_bone_centers.has_value(); + m_bone_centers.reset(); + m_last_bone_geom.reset(); + m_sketch.m_view.gui().dismiss_add_bone_dialog(); clear_all(m_tmp_node_idxs, m_tmp_shp, m_tmp_edges); m_sketch.m_dims.on_clear_tmps(); diff --git a/src/skt_tools.h b/src/skt_tools.h index 617d2bc4..040b6364 100644 --- a/src/skt_tools.h +++ b/src/skt_tools.h @@ -2,9 +2,11 @@ #include #include +#include #include #include "skt_edge.h" +#include "utl_geom.h" #include "utl_types.h" class Sketch; @@ -38,6 +40,10 @@ class Sketch_tools /// Clears rubber-band / tmp geometry. Returns true if tmp edges were present (used by `cancel()`). bool clear_tmps(); + void update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive); + [[nodiscard]] bool commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive); + [[nodiscard]] const std::optional& last_bone_preview_geom() const { return m_last_bone_geom; } + void clear_tmp_node_idxs() { m_tmp_node_idxs.clear(); } [[nodiscard]] size_t tmp_node_count() const { return m_tmp_node_idxs.size(); } @@ -70,6 +76,10 @@ class Sketch_tools void move_slot_pt_(const ScreenCoords& screen_coords); void finalize_slot_(Sketch_op_recorder& rec); + void add_bone_pt_(const ScreenCoords& screen_coords); + void move_bone_pt_(const ScreenCoords& screen_coords); + void begin_bone_dialog_from_centers_(const gp_Pnt2d& c1, const gp_Pnt2d& c2); + void add_operation_axis_pt_(const ScreenCoords& screen_coords); void finalize_operation_axis_(Sketch_op_recorder& rec); @@ -86,4 +96,6 @@ class Sketch_tools std::vector m_tmp_node_idxs; std::vector m_tmp_edges; AIS_Shape_ptr m_tmp_shp; + std::optional> m_bone_centers; + std::optional m_last_bone_geom; }; diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 92f8229e..0f5f23b5 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -368,6 +369,215 @@ 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, const gp_Vec2d& n) +{ + return gp_Vec2d(c_bot, c_top).Dot(n) - 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, n); + + 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); +} + +TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g) +{ + auto circle_at = [&](const gp_Pnt2d& c, double r) -> TopoDS_Wire + { return make_circle_wire(pln, c, gp_Pnt2d(c.X() + r, c.Y())); }; + + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bb.Add(comp, circle_at(g.c1, g.r1)); + bb.Add(comp, circle_at(g.c2, g.r2)); + bb.Add(comp, circle_at(g.cut_plus, g.cut_radius)); + bb.Add(comp, circle_at(g.cut_minus, g.cut_radius)); + bb.Add(comp, BRepBuilderAPI_MakeEdge(to_3d(pln, g.tan_top_a), to_3d(pln, g.tan_top_b)).Edge()); + bb.Add(comp, BRepBuilderAPI_MakeEdge(to_3d(pln, g.tan_bot_a), to_3d(pln, g.tan_bot_b)).Edge()); + return comp; +} + // 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_geom.h b/src/utl_geom.h index a676fd57..4afad2f5 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include // For Pi #include @@ -76,6 +77,47 @@ 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; +}; + +/// 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); + +/// Four circle wires plus the two external tangent edges (preview / debug). +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/tests/skt_tests.cpp b/tests/skt_tests.cpp index 0fa4bef6..262b2c00 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -5,9 +5,12 @@ #include #include #include +#include #include #include +#include + #include "skt_edge.h" #include "skt_json.h" #include "skt_nodes.h" @@ -941,3 +944,86 @@ TEST_F(Sketch_test, ProjectUnit_displayConversionAndJsonRoundTrip) view().new_file(); EXPECT_EQ(view().get_project_unit(), Project_unit::Inch); } + +TEST_F(Sketch_test, BoneGeom_equalRadiiTangentCuttersAndWaistFromCutRadius) +{ + Bone_params p; + p.c1 = gp_Pnt2d(-2.0, 0.0); + p.c2 = gp_Pnt2d(2.0, 0.0); + p.r1 = 1.0; + p.r2 = 1.0; + p.cut_radius = 2.0; + p.drive = Bone_drive::Cut_radius; + + const std::optional g = compute_bone_geom(p); + ASSERT_TRUE(g.has_value()); + EXPECT_NEAR(g->cut_plus.X(), 0.0, 1e-9); + EXPECT_NEAR(g->cut_minus.X(), 0.0, 1e-9); + EXPECT_NEAR(g->cut_plus.Y(), std::sqrt(5.0), 1e-9); + EXPECT_NEAR(g->cut_plus.Y(), -g->cut_minus.Y(), 1e-9); + EXPECT_NEAR(g->tan_top_a.Y(), g->tan_top_b.Y(), 1e-9); + + const double d1 = std::hypot(g->cut_plus.X() - p.c1.X(), g->cut_plus.Y() - p.c1.Y()); + EXPECT_NEAR(d1, g->cut_radius + p.r1, 1e-9); + const double d2 = std::hypot(g->cut_plus.X() - p.c2.X(), g->cut_plus.Y() - p.c2.Y()); + EXPECT_NEAR(d2, g->cut_radius + p.r2, 1e-9); + EXPECT_NEAR(g->waist, 2.0 * std::sqrt(5.0) - 4.0, 1e-9); + + p.c1 = p.c2; + EXPECT_FALSE(compute_bone_geom(p).has_value()); +} + +TEST_F(Sketch_test, BoneGeom_waistDriveSolvesCutRadius) +{ + Bone_params p; + p.c1 = gp_Pnt2d(-2.0, 0.0); + p.c2 = gp_Pnt2d(2.0, 0.0); + p.r1 = 1.0; + p.r2 = 1.0; + p.waist = 0.4; + p.drive = Bone_drive::Waist; + + const std::optional g = compute_bone_geom(p); + ASSERT_TRUE(g.has_value()); + EXPECT_NEAR(g->waist, 0.4, 1e-6); + EXPECT_GT(g->cut_radius, 0.0); +} + +TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) +{ + Headless_guard guard(view()); + + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("BoneSketch", view(), default_plane); + sketch.add_bone(gp_Pnt2d(-2.0, 0.0), gp_Pnt2d(2.0, 0.0), 1.0, 0.5, 2.0, 0.4); + + EXPECT_GE(sketch.face_count(), 1u); + EXPECT_GE(Sketch_access::get_edge_count(sketch), 8u); + + bool found_a = false; + bool found_b = false; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (n.deleted || !n.permanent) + continue; + + if (n.name == "Bone A") + { + found_a = true; + EXPECT_TRUE(n.IsEqual(gp_Pnt2d(-2.0, 0.0), Precision::Confusion())); + } + if (n.name == "Bone B") + { + found_b = true; + EXPECT_TRUE(n.IsEqual(gp_Pnt2d(2.0, 0.0), Precision::Confusion())); + } + } + + EXPECT_TRUE(found_a); + EXPECT_TRUE(found_b); + + const std::vector labels = sketch.inspector_node_labels(); + EXPECT_NE(std::find(labels.begin(), labels.end(), "Bone A"), labels.end()); + EXPECT_NE(std::find(labels.begin(), labels.end(), "Bone B"), labels.end()); +} From 5501e0980ce159f53e12b2171bcf0c7a62e6be2e Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 2 Sep 2026 17:48:08 -0600 Subject: [PATCH 02/12] WIP --- CHANGELOG.md | 2 +- docs/usage-sketch.md | 22 ++- src/gui.cpp | 1 - src/gui.h | 10 -- src/gui_add.cpp | 85 ----------- src/gui_mode.cpp | 2 +- src/mode.h | 2 +- src/skt.cpp | 30 +--- src/skt.h | 6 +- src/skt_tools.cpp | 338 +++++++++++++++++++++++++++++++++---------- src/skt_tools.h | 17 ++- tests/skt_tests.cpp | 2 +- 12 files changed, 296 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2678f8..420650ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Add bone** sketch tool (Shift+U): click two circle centers, then a dialog for the two end radii, waist cut radius, and waist thickness. Waist cutters are tangent to both end circles; edit **Waist** or **Cut radius** and the other value updates. Commits end circles, waist cutters, capsule tangents, and permanent **Bone A** / **Bone B** center nodes. Remappable as **`mode.add_bone`**. +- **Add bone** sketch tool (Shift+U): click the two circle centers, then radius 1, radius 2, and waist width (like other sketch tools). Waist cutters are tangent to both end circles. Commits end circles, waist cutters, capsule tangents, and permanent **Bone A** / **Bone B** center nodes. 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/docs/usage-sketch.md b/docs/usage-sketch.md index bafa4a50..261f602b 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -720,13 +720,11 @@ The bone tool builds a connecting-rod profile: two end circles, two waist cut ci 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. In the **Add bone** dialog, enter: - - **Radius 1** / **Radius 2** — end circle radii - - **Cut radius** — both waist cutters (mirrored across the bone axis) - - **Waist** — remaining thickness at the midpoint, perpendicular to the center line -5. A live preview updates as you edit. **Add** commits one undo step; **Cancel** or Esc drops the preview. +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. +6. Click a point to set **waist width** (remaining thickness at the midpoint, perpendicular to the center line). The live dimension spans the full waist. Tab enters an exact waist. The tool commits after this click. -Values are in project units (same as other Add dialogs). **Add** stays disabled when a radius or the waist is not positive. Waist cutters are tangent to both end circles; edit **Waist** or **Cut radius** and the other value updates to match. Geometry also fails when the two centers coincide or one circle sits inside the other (no external tangents). +Waist cutters are tangent to both end circles (cut radius is solved from the waist). Geometry fails when the two centers coincide, one circle sits inside the other, or the waist cannot be realized. The sketch keeps the four full circles plus the two tangent segments. Extrude the leftover bone face with the normal **Extrude sketch face** tool. @@ -734,7 +732,7 @@ The sketch keeps the four full circles plus the two tangent segments. Extrude th - Snap the two centers to existing nodes, or to the sketch origin. - After Add, use **Add circle** snapped to **Bone A** / **Bone B** if you want a different hole radius than the end circles. -- **Waist** and **Cut radius** are linked: the last field you edit drives the solve; the other updates in the dialog. +- For waist, click on one side of the neck; the distance from the bone axis to that point is half the waist. ## Operation Axis Tool @@ -901,12 +899,12 @@ Edge dimension tool creates/removes **length dimensions between two sketch nodes **Technical Details:** -| | | -| -------------------: | ------------------------------------------------------------------------------ | -| **Dimension source** | Calculated from the two referenced nodes | +| | | +| -------------------: | ---------------------------------------------------------------------------------- | +| **Dimension source** | Calculated from the two referenced nodes | | **Unit system** | **File -> Project units** (Inches or Millimeters); dims and length entry follow it | -| **Auto-update** | Dimensions update automatically when geometry is modified | -| **View-only** | Does not affect the underlying geometry | +| **Auto-update** | Dimensions update automatically when geometry is modified | +| **View-only** | Does not affect the underlying geometry | ## Add Node Tool diff --git a/src/gui.cpp b/src/gui.cpp index dc478978..335a114b 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -165,7 +165,6 @@ void GUI::render_gui() add_cone_dialog_(); add_torus_dialog_(); add_sketch_dialog_(); - add_bone_dialog_(); log_window_(); lua_console_(); python_console_(); diff --git a/src/gui.h b/src/gui.h index 1a673509..4ac88ef0 100644 --- a/src/gui.h +++ b/src/gui.h @@ -336,8 +336,6 @@ class GUI void show_message(const std::string& message); /// Queue an ImGui error modal (logs title: message once; short title toast). Native and wasm. void show_error_dialog(const std::string& title, const std::string& message); - void open_add_bone_dialog(); - void dismiss_add_bone_dialog(); void log_message(const std::string& message); void set_show_options(bool v) { m_show_options = v; } void set_show_sketch_list(bool v) { m_show_sketch_list = v; } @@ -514,7 +512,6 @@ class GUI void add_cone_dialog_(); void add_torus_dialog_(); void add_sketch_dialog_(); - void add_bone_dialog_(); void add_menu_items_(); void about_dialog_(); @@ -767,13 +764,6 @@ class GUI glm::dvec3 m_add_torus_origin{0.0, 0.0, 0.0}; double m_add_torus_R1{1}, m_add_torus_R2{0.5}; bool m_open_add_sketch_popup{false}; - bool m_open_add_bone_popup{false}; - bool m_add_bone_dialog_open{false}; - double m_add_bone_r1{1.0}; - double m_add_bone_r2{0.5}; - double m_add_bone_cut_radius{2.0}; - double m_add_bone_waist{0.4}; - Bone_drive m_add_bone_drive{Bone_drive::Waist}; int m_new_sketch_plane{0}; // 0=XY, 1=XZ, 2=YZ double m_new_sketch_offset{}; bool m_hide_all_shapes{false}; diff --git a/src/gui_add.cpp b/src/gui_add.cpp index a925a062..a78947eb 100644 --- a/src/gui_add.cpp +++ b/src/gui_add.cpp @@ -408,91 +408,6 @@ void GUI::add_menu_items_() } } -void GUI::open_add_bone_dialog() -{ - m_open_add_bone_popup = true; - m_add_bone_dialog_open = true; -} - -void GUI::dismiss_add_bone_dialog() -{ - m_open_add_bone_popup = false; - m_add_bone_dialog_open = false; -} - -void GUI::add_bone_dialog_() -{ - if (m_open_add_bone_popup) - { - ImGui::OpenPopup("Add bone"); - m_open_add_bone_popup = false; - m_add_bone_dialog_open = true; - } - - if (!m_add_bone_dialog_open) - return; - - if (!ImGui::BeginPopupModal("Add bone", &m_add_bone_dialog_open, ImGuiWindowFlags_AlwaysAutoResize)) - { - if (!m_add_bone_dialog_open && m_view) - m_view->curr_sketch().cancel_elm(); - - return; - } - - ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); - ImGui::Spacing(); - if (ImGui::BeginTable("Add bone##table", 2, ImGuiTableFlags_SizingStretchProp)) - { - table_row_input_double_("Radius 1", "##bone_r1", &m_add_bone_r1); - table_row_input_double_("Radius 2", "##bone_r2", &m_add_bone_r2); - table_row_input_double_("Cut radius", "##bone_cut", &m_add_bone_cut_radius); - if (ImGui::IsItemDeactivatedAfterEdit()) - m_add_bone_drive = Bone_drive::Cut_radius; - table_row_input_double_("Waist", "##bone_waist", &m_add_bone_waist); - if (ImGui::IsItemDeactivatedAfterEdit()) - m_add_bone_drive = Bone_drive::Waist; - ImGui::EndTable(); - } - - const double scale = m_view->get_display_to_model_scale(); - const double r1 = m_add_bone_r1 * scale; - const double r2 = m_add_bone_r2 * scale; - const double cut = m_add_bone_cut_radius * scale; - const double waist = m_add_bone_waist * scale; - m_view->curr_sketch().update_bone_preview(r1, r2, cut, waist, m_add_bone_drive); - - if (const std::optional g = m_view->curr_sketch().last_bone_preview_geom()) - { - const double inv = 1.0 / scale; - if (m_add_bone_drive == Bone_drive::Waist) - m_add_bone_cut_radius = g->cut_radius * inv; - else - m_add_bone_waist = g->waist * inv; - } - - const bool can_add = m_add_bone_r1 > 0 && m_add_bone_r2 > 0 && m_add_bone_cut_radius > 0 && m_add_bone_waist > 0; - ImGui::Spacing(); - if (ImGui::Button("Add") && can_add) - { - if (m_view->curr_sketch().commit_pending_bone(r1, r2, cut, waist, m_add_bone_drive)) - { - ImGui::CloseCurrentPopup(); - m_add_bone_dialog_open = false; - } - } - - ImGui::SameLine(); - if (ImGui::Button("Cancel")) - { - m_view->curr_sketch().cancel_elm(); - ImGui::CloseCurrentPopup(); - m_add_bone_dialog_open = false; - } - - ImGui::EndPopup(); -} - namespace { void table_row_input_double_(const char* label, const char* id, double* value) diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 0e9aad03..465b96f5 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -1263,7 +1263,7 @@ void GUI::options_sketch_add_bone_mode_() options_sketch_common_(); options_sketch_len_angle_hotkeys_(); ImGui::Separator(); - ImGui::TextWrapped("Click the two circle centers, then enter radii and waist thickness."); + ImGui::TextWrapped("Click center A, center B, radius 1, radius 2, then the waist width."); } void GUI::options_orthographic_projection_() diff --git a/src/mode.h b/src/mode.h index 4e800247..4f416dcd 100644 --- a/src/mode.h +++ b/src/mode.h @@ -28,7 +28,7 @@ X(Sketch_add_circle) \ X(Sketch_add_circle_3_pts) \ X(Sketch_add_slot) \ - X(Sketch_add_bone) /* two centers, then radii / waist dialog */ \ + X(Sketch_add_bone) /* two centers, then r1, r2, waist clicks */ \ X(Sketch_dim_anno) \ X(Shape_cross_section) \ X(Shape_shaft_align) \ diff --git a/src/skt.cpp b/src/skt.cpp index 71c41589..9d7ed37c 100644 --- a/src/skt.cpp +++ b/src/skt.cpp @@ -152,16 +152,15 @@ void Sketch::add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const void Sketch::rebuild_faces() { update_faces_(); } -void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_radius, double waist) +void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist) { Bone_params params; - params.c1 = c1; - params.c2 = c2; - params.r1 = r1; - params.r2 = r2; - params.cut_radius = cut_radius; - params.waist = waist; - params.drive = Bone_drive::Cut_radius; + params.c1 = c1; + params.c2 = c2; + params.r1 = r1; + params.r2 = r2; + params.waist = waist; + params.drive = Bone_drive::Waist; const std::optional g = compute_bone_geom(params); if (!g) return; @@ -201,21 +200,6 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double update_faces_(); } -void Sketch::update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive) -{ - m_tools.update_bone_preview(r1, r2, cut_radius, waist, drive); -} - -bool Sketch::commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive) -{ - return m_tools.commit_pending_bone(r1, r2, cut_radius, waist, drive); -} - -const std::optional& Sketch::last_bone_preview_geom() const -{ - return m_tools.last_bone_preview_geom(); -} - void Sketch::add_edge_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, Sketch_op_recorder& rec) { m_edges.add_edge(pt_a, pt_b, rec); diff --git a/src/skt.h b/src/skt.h index a840dfa4..79ee3530 100644 --- a/src/skt.h +++ b/src/skt.h @@ -168,10 +168,8 @@ class Sketch /// Rebuild closed-face topology after bulk edge import. void rebuild_faces(); /// Add a bone profile (end circles, waist cutters, capsule tangents) and permanent center nodes. - void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double cut_radius, double waist); - void update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive); - [[nodiscard]] bool commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive); - [[nodiscard]] const std::optional& last_bone_preview_geom() const; + /// Waist cutters are tangent to both end circles; cut radius is solved from \\a waist. + void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist); private: friend class Sketch_json; diff --git a/src/skt_tools.cpp b/src/skt_tools.cpp index 765de40a..b39258d6 100644 --- a/src/skt_tools.cpp +++ b/src/skt_tools.cpp @@ -1,14 +1,17 @@ #include "skt_tools.h" #include +#include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -118,9 +121,7 @@ void Sketch_tools::on_enter() break; case Mode::Sketch_add_bone: - if (m_bone_centers) - break; - if (!m_tmp_edges.empty() && m_sketch.m_dims.entered_edge_len().has_value()) + if (!m_tmp_edges.empty() && !m_bone_centers && m_sketch.m_dims.entered_edge_len().has_value()) { Sketch_edge& edge = m_tmp_edges.back(); const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; @@ -131,10 +132,40 @@ void Sketch_tools::on_enter() m_sketch.m_dims.clear_typed_constraints(); } - if (!m_tmp_edges.empty() && m_tmp_edges.back().node_idx_b.has_value()) + 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(); - begin_bone_dialog_from_centers_(m_sketch.m_nodes[e.node_idx_a], m_sketch.m_nodes[*e.node_idx_b]); + bone_on_centers_ready_(m_sketch.m_nodes[e.node_idx_a], m_sketch.m_nodes[*e.node_idx_b]); + break; + } + if (!m_bone_centers || !m_sketch.m_dims.entered_edge_len().has_value()) + break; + + { + const double len = m_sketch.m_dims.entered_edge_len()->len; + if (len <= Precision::Confusion()) + break; + + Sketch_edge& edge = m_tmp_edges.back(); + const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; + m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * len); + if (unique(pt_a, *m_last_pt)) + m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); + + m_sketch.m_dims.clear_typed_constraints(); + + if (m_tmp_edges.size() == 2) + { + m_bone_r1 = len; + bone_begin_next_edge_from_(m_bone_centers->second); + } + else if (m_tmp_edges.size() == 3) + { + m_bone_r2 = 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_try_commit_(len); } break; @@ -170,7 +201,7 @@ void Sketch_tools::finalize() case Mode::Sketch_add_circle: finalize_circle_(rec); break; case Mode::Sketch_add_node: finalize_add_node_elm_cleanup_(); break; case Mode::Sketch_add_slot: finalize_slot_(rec); break; - case Mode::Sketch_add_bone: break; + case Mode::Sketch_add_bone: finalize_bone_(); break; case Mode::Sketch_operation_axis: finalize_operation_axis_(rec); break; // clang-format on default: @@ -891,117 +922,274 @@ void Sketch_tools::move_slot_pt_(const ScreenCoords& screen_coords) void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) { - if (m_bone_centers) - return; - if (m_tmp_edges.empty()) { add_line_string_pt_(screen_coords, Linestring_type::Multiple); return; } - auto on_second = [&](size_t node_idx) + if (!m_bone_centers) { - Sketch_edge& last = m_tmp_edges.back(); - if (node_idx == last.node_idx_a) + auto on_second = [&](size_t node_idx) + { + Sketch_edge& last = m_tmp_edges.back(); + if (node_idx == last.node_idx_a) + return; + + m_sketch.update_edge_end_pt_(last, node_idx); + bone_on_centers_ready_(m_sketch.m_nodes[last.node_idx_a], m_sketch.m_nodes[node_idx]); + }; + + if (m_sketch.m_dims.entered_edge_angle().has_value() && !m_tmp_edges.empty()) + { + std::optional pt_opt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); + if (!pt_opt) + return; + + const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_edges.back().node_idx_a]; + const double angle_rad = to_radians(*m_sketch.m_dims.entered_edge_angle()); + gp_Dir2d constrained_dir(std::cos(angle_rad), std::sin(angle_rad)); + gp_Vec2d to_click(pt_opt->X() - pt_a.X(), pt_opt->Y() - pt_a.Y()); + const double dist_along = to_click.Dot(gp_Vec2d(constrained_dir)); + gp_Pnt2d final_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(constrained_dir) * dist_along); + if (!unique(pt_a, final_pt)) + return; + + const size_t node_idx = m_sketch.m_nodes.get_node_exact(final_pt); + m_tmp_node_idxs.push_back(node_idx); + on_second(node_idx); return; + } - m_sketch.update_edge_end_pt_(last, node_idx); - begin_bone_dialog_from_centers_(m_sketch.m_nodes[last.node_idx_a], m_sketch.m_nodes[node_idx]); - }; + add_sketch_pt_(screen_coords, 1, on_second); + return; + } - if (m_sketch.m_dims.entered_edge_angle().has_value() && !m_tmp_edges.empty()) + auto on_dim = [&](size_t node_idx) { - std::optional pt_opt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); - if (!pt_opt) + Sketch_edge& last = m_tmp_edges.back(); + if (node_idx == last.node_idx_a) return; - const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_edges.back().node_idx_a]; - const double angle_rad = to_radians(*m_sketch.m_dims.entered_edge_angle()); - gp_Dir2d constrained_dir(std::cos(angle_rad), std::sin(angle_rad)); - gp_Vec2d to_click(pt_opt->X() - pt_a.X(), pt_opt->Y() - pt_a.Y()); - const double dist_along = to_click.Dot(gp_Vec2d(constrained_dir)); - gp_Pnt2d final_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(constrained_dir) * dist_along); - if (!unique(pt_a, final_pt)) + const gp_Pnt2d& pt_a = m_sketch.m_nodes[last.node_idx_a]; + const gp_Pnt2d& pt_b = m_sketch.m_nodes[node_idx]; + if (!unique(pt_a, pt_b)) return; - const size_t node_idx = m_sketch.m_nodes.get_node_exact(final_pt); - m_tmp_node_idxs.push_back(node_idx); - on_second(node_idx); - return; - } + m_sketch.update_edge_end_pt_(last, node_idx); + const double len = pt_a.Distance(pt_b); + + if (m_tmp_edges.size() == 2) + { + m_bone_r1 = len; + bone_begin_next_edge_from_(m_bone_centers->second); + } + else if (m_tmp_edges.size() == 3) + { + m_bone_r2 = len; + bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); + } + else if (m_tmp_edges.size() == 4) + { + const std::optional waist = bone_waist_from_pt_(pt_b); + if (waist) + (void)bone_try_commit_(*waist); + } + }; - add_sketch_pt_(screen_coords, 1, on_second); + add_sketch_pt_(screen_coords, 1, on_dim); } void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) { - if (m_bone_centers) + if (!m_bone_centers || m_tmp_edges.size() <= 1) + { + move_line_string_pt_(screen_coords); return; + } - move_line_string_pt_(screen_coords); + if (m_tmp_edges.size() < 4) + { + move_line_string_pt_(screen_coords); + bone_update_preview_(); + return; + } + + const std::optional n = bone_axis_perp_(); + if (!n) + return; + + const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); + + auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) + { + Sketch_edge& edge = m_tmp_edges.back(); + double half = std::abs(gp_Vec2d(mid, pt_b).Dot(*n)); + if (m_sketch.m_dims.entered_edge_len().has_value()) + half = m_sketch.m_dims.entered_edge_len()->len * 0.5; + + if (half <= Precision::Confusion()) + { + m_sketch.m_dims.clear_tmp_dim_anno(); + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + const gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); + const gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); + m_last_pt = span_b; + m_sketch.update_edge_shp_(edge, span_a, span_b); + + const double dist = (2.0 * half) / m_sketch.m_view.get_display_to_model_scale(); + m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); + m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); + bone_update_preview_(); + }; + + move_sketch_pt_(screen_coords, l); } -void Sketch_tools::begin_bone_dialog_from_centers_(const gp_Pnt2d& c1, const gp_Pnt2d& c2) +void Sketch_tools::bone_begin_next_edge_from_(const gp_Pnt2d& origin) +{ + m_sketch.m_dims.clear_typed_constraints(); + m_sketch.m_dims.set_show_angle_input(false); + m_sketch.m_view.gui().hide_angle_edit(); + m_sketch.m_view.gui().hide_dist_edit(false); + m_tmp_edges.push_back({m_sketch.m_nodes.get_node_exact(origin)}); +} + +void Sketch_tools::bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2) { if (!unique(c1, c2)) return; m_bone_centers = std::make_pair(c1, c2); - for (Sketch_edge& e : m_tmp_edges) - m_sketch.m_view.remove(e.shp); + bone_begin_next_edge_from_(c1); +} + +bool Sketch_tools::bone_try_commit_(double waist) +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2) + return false; + + Bone_params params; + params.c1 = m_bone_centers->first; + params.c2 = m_bone_centers->second; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = waist; + params.drive = Bone_drive::Waist; + if (!compute_bone_geom(params)) + return false; + + m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, waist); + clear_tmps(); + return true; +} + +void Sketch_tools::finalize_bone_() +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || m_tmp_edges.size() != 4 || !m_last_pt) + return; + + const std::optional waist = bone_waist_from_pt_(*m_last_pt); + if (waist) + (void)bone_try_commit_(*waist); +} + +std::optional Sketch_tools::bone_axis_perp_() const +{ + if (!m_bone_centers) + return std::nullopt; + + gp_Vec2d axis(m_bone_centers->first, m_bone_centers->second); + const double dist = axis.Magnitude(); + if (dist <= Precision::Confusion()) + return std::nullopt; + + return gp_Vec2d(axis / dist).Rotated(std::numbers::pi / 2.0); +} + +std::optional Sketch_tools::bone_waist_from_pt_(const gp_Pnt2d& pt) const +{ + const std::optional n = bone_axis_perp_(); + if (!n || !m_bone_centers) + return std::nullopt; + + const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); + const double waist = 2.0 * std::abs(gp_Vec2d(mid, pt).Dot(*n)); + if (waist <= Precision::Confusion()) + return std::nullopt; - m_sketch.m_view.gui().open_add_bone_dialog(); + return waist; } -void Sketch_tools::update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive) +void Sketch_tools::bone_update_preview_() { if (!m_bone_centers) return; - Bone_params params; - params.c1 = m_bone_centers->first; - params.c2 = m_bone_centers->second; - params.r1 = r1; - params.r2 = r2; - params.cut_radius = cut_radius; - params.waist = waist; - params.drive = drive; - - const std::optional g = compute_bone_geom(params); - if (!g) + const gp_Pnt2d& c1 = m_bone_centers->first; + const gp_Pnt2d& c2 = m_bone_centers->second; + + 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())); }; + + const double r1 = m_bone_r1.value_or( + (m_tmp_edges.size() == 2 && 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); + + if (m_tmp_edges.size() >= 4 && m_bone_r1 && m_bone_r2) { - m_last_bone_geom.reset(); + const std::optional waist = + m_sketch.m_dims.entered_edge_len().has_value() ? std::optional(m_sketch.m_dims.entered_edge_len()->len) + : (m_last_pt ? bone_waist_from_pt_(*m_last_pt) : std::nullopt); + if (waist) + { + Bone_params params; + params.c1 = c1; + params.c2 = c2; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = *waist; + params.drive = Bone_drive::Waist; + if (const std::optional g = compute_bone_geom(params)) + { + show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); + return; + } + } + m_sketch.m_view.remove(m_tmp_shp); m_tmp_shp = nullptr; return; } - m_last_bone_geom = g; - show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); -} - -bool Sketch_tools::commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive) -{ - if (!m_bone_centers) - return false; + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bool any = false; + if (r1 > Precision::Confusion()) + { + bb.Add(comp, circle_at(c1, r1)); + any = true; + } + if (r2 > Precision::Confusion()) + { + bb.Add(comp, circle_at(c2, r2)); + any = true; + } - Bone_params params; - params.c1 = m_bone_centers->first; - params.c2 = m_bone_centers->second; - params.r1 = r1; - params.r2 = r2; - params.cut_radius = cut_radius; - params.waist = waist; - params.drive = drive; - - const std::optional g = compute_bone_geom(params); - if (!g) - return false; + if (!any) + { + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } - m_sketch.add_bone(m_bone_centers->first, m_bone_centers->second, r1, r2, g->cut_radius, g->waist); - clear_tmps(); - return true; + show(m_sketch.m_ctx, m_tmp_shp, comp); } void Sketch_tools::finalize_slot_(Sketch_op_recorder& rec) @@ -1055,8 +1243,8 @@ bool Sketch_tools::clear_tmps() const bool operation_canceled = !m_tmp_edges.empty() || m_bone_centers.has_value(); m_bone_centers.reset(); - m_last_bone_geom.reset(); - m_sketch.m_view.gui().dismiss_add_bone_dialog(); + m_bone_r1.reset(); + m_bone_r2.reset(); clear_all(m_tmp_node_idxs, m_tmp_shp, m_tmp_edges); m_sketch.m_dims.on_clear_tmps(); diff --git a/src/skt_tools.h b/src/skt_tools.h index 040b6364..ea2e7338 100644 --- a/src/skt_tools.h +++ b/src/skt_tools.h @@ -1,12 +1,12 @@ #pragma once #include +#include #include #include #include #include "skt_edge.h" -#include "utl_geom.h" #include "utl_types.h" class Sketch; @@ -40,10 +40,6 @@ class Sketch_tools /// Clears rubber-band / tmp geometry. Returns true if tmp edges were present (used by `cancel()`). bool clear_tmps(); - void update_bone_preview(double r1, double r2, double cut_radius, double waist, Bone_drive drive); - [[nodiscard]] bool commit_pending_bone(double r1, double r2, double cut_radius, double waist, Bone_drive drive); - [[nodiscard]] const std::optional& last_bone_preview_geom() const { return m_last_bone_geom; } - void clear_tmp_node_idxs() { m_tmp_node_idxs.clear(); } [[nodiscard]] size_t tmp_node_count() const { return m_tmp_node_idxs.size(); } @@ -78,7 +74,13 @@ class Sketch_tools void add_bone_pt_(const ScreenCoords& screen_coords); void move_bone_pt_(const ScreenCoords& screen_coords); - void begin_bone_dialog_from_centers_(const gp_Pnt2d& c1, const gp_Pnt2d& c2); + void finalize_bone_(); + void bone_begin_next_edge_from_(const gp_Pnt2d& origin); + void bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2); + [[nodiscard]] bool bone_try_commit_(double waist); + void bone_update_preview_(); + [[nodiscard]] std::optional bone_axis_perp_() const; + [[nodiscard]] std::optional bone_waist_from_pt_(const gp_Pnt2d& pt) const; void add_operation_axis_pt_(const ScreenCoords& screen_coords); void finalize_operation_axis_(Sketch_op_recorder& rec); @@ -97,5 +99,6 @@ class Sketch_tools std::vector m_tmp_edges; AIS_Shape_ptr m_tmp_shp; std::optional> m_bone_centers; - std::optional m_last_bone_geom; + std::optional m_bone_r1; + std::optional m_bone_r2; }; diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 262b2c00..833a8804 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -995,7 +995,7 @@ TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) gp_Pln default_plane(gp::Origin(), gp::DZ()); Sketch sketch("BoneSketch", view(), default_plane); - sketch.add_bone(gp_Pnt2d(-2.0, 0.0), gp_Pnt2d(2.0, 0.0), 1.0, 0.5, 2.0, 0.4); + sketch.add_bone(gp_Pnt2d(-2.0, 0.0), gp_Pnt2d(2.0, 0.0), 1.0, 0.5, 0.4); EXPECT_GE(sketch.face_count(), 1u); EXPECT_GE(Sketch_access::get_edge_count(sketch), 8u); From 68317c95dc43a1e31ad6034cf0b5c6dcca28de9b Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 2 Sep 2026 17:56:00 -0600 Subject: [PATCH 03/12] Improve --- CHANGELOG.md | 2 +- docs/usage-sketch.md | 4 +-- src/doc/utility.md | 64 ++++++++++++++++++++++---------------------- src/skt.cpp | 20 ++++---------- src/skt.h | 2 +- src/utl_geom.cpp | 60 ++++++++++++++++++++++++++++++++--------- src/utl_geom.h | 20 +++++++++++++- tests/skt_tests.cpp | 10 +++++-- 8 files changed, 115 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 420650ff..a4d99853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,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 cutters are tangent to both end circles. Commits end circles, waist cutters, capsule tangents, and permanent **Bone A** / **Bone B** center nodes. Remappable as **`mode.add_bone`**. +- **Add bone** sketch tool (Shift+U): click the two circle centers, then radius 1, radius 2, and waist width (like other sketch tools). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs) and permanent **Bone A** / **Bone B** center nodes. 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/docs/usage-sketch.md b/docs/usage-sketch.md index 261f602b..bfcb70b0 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -713,7 +713,7 @@ The slot tool allows you to create an oblong or oval-shaped slot with rounded en ![Bone Tool](res/icons/Sketcher_CreateCircle.png) -The bone tool builds a connecting-rod profile: two end circles, two waist cut circles, and the capsule's external tangents. Topology splits those edges into a bone-shaped face you can extrude. Permanent nodes **Bone A** and **Bone B** stay at the two centers for later holes, dimensions, or an operation axis. +The bone tool builds a connecting-rod outline: the outer arcs of the two end circles and the inward waist arcs where the cutters touch those circles. The result is one closed face you can extrude. Permanent nodes **Bone A** and **Bone B** stay at the two centers for later holes, dimensions, or an operation axis. **How to use:** @@ -726,7 +726,7 @@ The bone tool builds a connecting-rod profile: two end circles, two waist cut ci Waist cutters are tangent to both end circles (cut radius is solved from the waist). Geometry fails when the two centers coincide, one circle sits inside the other, or the waist cannot be realized. -The sketch keeps the four full circles plus the two tangent segments. Extrude the leftover bone face with the normal **Extrude sketch face** tool. +The sketch keeps only that trimmed outline (four arcs). Extrude it with the normal **Extrude sketch face** tool. **Tips:** diff --git a/src/doc/utility.md b/src/doc/utility.md index 2904d21d..51147877 100644 --- a/src/doc/utility.md +++ b/src/doc/utility.md @@ -18,21 +18,21 @@ Typical uses: ## Module map -| File | Responsibility | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | -| [`utl.h`](../utl.h) / [`utl.cpp`](../utl.cpp) | `Status`, `Result`, `CHK_RET`, `clear_all`, textures, image decode, name uniquification | -| [`utl_types.h`](../utl_types.h) | OCCT/AIS handle typedefs, `ScreenCoords`, `Export_format`, `Export_unit`, `DECL_PTR`, `SafeType` | -| [`utl_geom.h`](../utl_geom.h) / [`.cpp`](../utl_geom.cpp) | 2D/3D geometry, wires, dimensions, Boost polygon tests, plane projection | -| [`utl_geom_boost.inl`](../utl_geom_boost.inl) | `ezy_geom` Boost.Geometry aliases | -| [`utl_occt.h`](../utl_occt.h) / [`.cpp`](../utl_occt.cpp) | `TopAbs` name table, `try_make_solid`, `append_cad_import_bodies`, `standard_failure_message` | -| [`utl_json.h`](../utl_json.h) / [`.cpp`](../utl_json.cpp) | JSON serializers for `gp_Pnt`, `gp_Pln`, etc. | -| [`utl_io.h`](../utl_io.h) / [`.cpp`](../utl_io.cpp) | `.ezy` zip v3 pack/unpack, format sniff, base64 | -| [`utl_asset_store.h`](../utl_asset_store.h) / [`.cpp`](../utl_asset_store.cpp) | Content-addressed RGBA blobs for sketch underlay assets | -| [`utl_settings.h`](../utl_settings.h) / [`.cpp`](../utl_settings.cpp) | User settings file paths, startup project blob I/O | -| [`utl_ply_io.h`](../utl_ply_io.h) / [`.cpp`](../utl_ply_io.cpp) | PLY import/export for mesh shapes | -| [`utl_cad_file_info.h`](../utl_cad_file_info.h) / [`.cpp`](../utl_cad_file_info.cpp) | Read-only STEP/IGES/STL/PLY metadata for **File -> Import** (no document mutation until Import) | -| [`utl_log.h`](../utl_log.h) / [`.cpp`](../utl_log.cpp) | `Log_strm` redirecting stdout/stderr to `GUI::log_message` | -| [`utl_dbg.h`](../utl_dbg.h) | `EZY_ASSERT`, `DBG_MSG`, debug break macros | +| File | Responsibility | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [`utl.h`](../utl.h) / [`utl.cpp`](../utl.cpp) | `Status`, `Result`, `CHK_RET`, `clear_all`, textures, image decode, name uniquification | +| [`utl_types.h`](../utl_types.h) | OCCT/AIS handle typedefs, `ScreenCoords`, `Export_format`, `Export_unit`, `DECL_PTR`, `SafeType` | +| [`utl_geom.h`](../utl_geom.h) / [`.cpp`](../utl_geom.cpp) | 2D/3D geometry, wires, dimensions, Boost polygon tests, plane projection | +| [`utl_geom_boost.inl`](../utl_geom_boost.inl) | `ezy_geom` Boost.Geometry aliases | +| [`utl_occt.h`](../utl_occt.h) / [`.cpp`](../utl_occt.cpp) | `TopAbs` name table, `try_make_solid`, `append_cad_import_bodies`, `standard_failure_message` | +| [`utl_json.h`](../utl_json.h) / [`.cpp`](../utl_json.cpp) | JSON serializers for `gp_Pnt`, `gp_Pln`, etc. | +| [`utl_io.h`](../utl_io.h) / [`.cpp`](../utl_io.cpp) | `.ezy` zip v3 pack/unpack, format sniff, base64 | +| [`utl_asset_store.h`](../utl_asset_store.h) / [`.cpp`](../utl_asset_store.cpp) | Content-addressed RGBA blobs for sketch underlay assets | +| [`utl_settings.h`](../utl_settings.h) / [`.cpp`](../utl_settings.cpp) | User settings file paths, startup project blob I/O | +| [`utl_ply_io.h`](../utl_ply_io.h) / [`.cpp`](../utl_ply_io.cpp) | PLY import/export for mesh shapes | +| [`utl_cad_file_info.h`](../utl_cad_file_info.h) / [`.cpp`](../utl_cad_file_info.cpp) | Read-only STEP/IGES/STL/PLY metadata for **File -> Import** (no document mutation until Import) | +| [`utl_log.h`](../utl_log.h) / [`.cpp`](../utl_log.cpp) | `Log_strm` redirecting stdout/stderr to `GUI::log_message` | +| [`utl_dbg.h`](../utl_dbg.h) | `EZY_ASSERT`, `DBG_MSG`, debug break macros | CMake IDE group: `src\utl` (pattern `^utl(_|\.)`). @@ -48,13 +48,13 @@ CMake IDE group: `src\utl` (pattern `^utl(_|\.)`). ### General helpers -| API | Purpose | -| ---------------------------------------- | ------------------------------------------------ | +| API | Purpose | +| ---------------------------------------- | ------------------------------------------------------------------------------------- | | `clear_all(...)` | Reset optional/containers/arithmetic/handles (`Nullify`)/enums/aggregates in one call | -| `unique_sequential_name(base, existing)` | `Name`, `Name.001`, ... for sketches/shapes | -| `load_texture(path)` | Toolbar icon loading | -| `decode_image_bytes(bytes)` | stb_image -> RGBA for underlay import | -| `safe_cstr_copy` | ImGui fixed-buffer copies (MSVC-safe) | +| `unique_sequential_name(base, existing)` | `Name`, `Name.001`, ... for sketches/shapes | +| `load_texture(path)` | Toolbar icon loading | +| `decode_image_bytes(bytes)` | stb_image -> RGBA for underlay import | +| `safe_cstr_copy` | ImGui fixed-buffer copies (MSVC-safe) | ## Geometry (`utl_geom`) @@ -63,7 +63,7 @@ Large module; grouped by concern: | Area | Examples | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Plane / 2D | `to_2d`, `to_3d`, `xy_plane`, `sketch_reference_plane`, `Plane_side` | -| Profile wires | `make_square_wire`, `make_circle_wire`, `make_slot_wire`, `create_wire_box` | +| Profile wires | `make_square_wire`, `make_circle_wire`, `make_slot_wire`, `make_bone_wire`, `create_wire_box` | | Sketch dimensions | `Length_dimension_style`, `create_distance_annotation`, `create_angle_annotation`, `apply_length_dimension_style`, `apply_angle_dimension_style` | | Analysis | `to_boost` (polygon), `to_boost_ls` (edge `linestring_2d`), `get_shape_bbox_center`, `plane_from_face`, `side_of_plane` | | Tests / debug | `to_wkt_string` (linestring / ring / polygon), `ezy_geom::area`, `is_valid`; Geometry Watch in `scripts/ezycad_graphical_debugging.xml` (`ring_2d` inherits vector as Ring; `linestring_2d` uses named `points` as Linestring). Re-select the XML path in Options after editing it. | @@ -74,10 +74,10 @@ Includes [`utl_geom_boost.inl`](../utl_geom_boost.inl) for `ezy_geom` polygon / ### `.ezy` v3 zip layout -| Path in archive | Content | -| ------------------ | ------------------------------------------------------------- | +| Path in archive | Content | +| ------------------ | ----------------------------------------------------------------------------------------- | | `manifest.json` | Document JSON (`ezyFormat`, `projectUnit`, sketches, shapes, view, mode, `ui.sketchList`) | -| `assets/.rgba` | Raw RGBA pixels for underlay `"asset"` references | +| `assets/.rgba` | Raw RGBA pixels for underlay `"asset"` references | | Function | Role | | ------------------------------ | --------------------------------------------------------------- | @@ -127,13 +127,13 @@ Raw PLY parse/write only (no unit conversion). `Occt_view::import_ply` / `export Used by **File -> Import**. Reads file bytes only until the user confirms import; does not add shapes by itself. -| API | Role | -| --------------------------- | ----------------------------------------------------------------- | -| `detect(path, bytes)` | Format from extension, then content sniff | -| `can_import(fmt)` | True for STEP and PLY | -| `collect(path, bytes)` | Label/value rows (size, roots/shapes, mesh header, bbox, etc.) | -| `read_step_named_bodies` | STEPCAF/XCAF bodies + product names (flat; falls back to plain reader) | -| `read_step_named_tree` | STEPCAF/XCAF assembly tree as group/leaf `Named_node`s (falls back flat) | +| API | Role | +| ------------------------ | ------------------------------------------------------------------------ | +| `detect(path, bytes)` | Format from extension, then content sniff | +| `can_import(fmt)` | True for STEP and PLY | +| `collect(path, bytes)` | Label/value rows (size, roots/shapes, mesh header, bbox, etc.) | +| `read_step_named_bodies` | STEPCAF/XCAF bodies + product names (flat; falls back to plain reader) | +| `read_step_named_tree` | STEPCAF/XCAF assembly tree as group/leaf `Named_node`s (falls back flat) | `Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. Heavy work splits into `prepare_step_import` (thread-safe geometry) + `commit_step_import` (UI thread). STEP Transfer accepts optional `Atomic_progress_indicator` / `Message_ProgressRange` (`utl_occt_progress.h`). `collect` remains available for tooling but is not shown in the Import dialog. diff --git a/src/skt.cpp b/src/skt.cpp index 9d7ed37c..bd07cd75 100644 --- a/src/skt.cpp +++ b/src/skt.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -167,14 +166,6 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double Sketch_op_recorder rec(m_view, *this); { - auto add_circle = [&](const gp_Pnt2d& center, double radius) - { - const gp_Pnt2d edge(center.X() + radius, center.Y()); - std::array pts = xy_stencil_pnts(center, edge); - add_arc_circle_(pts[0], pts[2], pts[1], rec); - add_arc_circle_(pts[0], pts[3], pts[1], rec); - }; - auto mark_center = [&](const gp_Pnt2d& c, const char* name) { const size_t idx = m_nodes.get_node_exact(c, true); @@ -184,14 +175,13 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double rec.note_curr_node(idx); }; + const Bone_profile p = get_bone_profile(*g); mark_center(g->c1, "Bone A"); mark_center(g->c2, "Bone B"); - add_circle(g->c1, g->r1); - add_circle(g->c2, g->r2); - add_circle(g->cut_plus, g->cut_radius); - add_circle(g->cut_minus, g->cut_radius); - add_edge_(g->tan_top_a, g->tan_top_b, rec); - add_edge_(g->tan_bot_a, g->tan_bot_b, rec); + add_arc_circle_(p.c1_minus, p.c1_outer, p.c1_plus, rec); + add_arc_circle_(p.c1_plus, p.waist_plus, p.c2_plus, rec); + add_arc_circle_(p.c2_plus, p.c2_outer, p.c2_minus, rec); + add_arc_circle_(p.c2_minus, p.waist_minus, p.c1_minus, rec); rec.commit(); } diff --git a/src/skt.h b/src/skt.h index 79ee3530..d320b215 100644 --- a/src/skt.h +++ b/src/skt.h @@ -167,7 +167,7 @@ class Sketch void add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const gp_Pnt2d& pt_c); /// Rebuild closed-face topology after bulk edge import. void rebuild_faces(); - /// Add a bone profile (end circles, waist cutters, capsule tangents) and permanent center nodes. + /// Add a bone outline (outer end arcs and inner waist arcs) and permanent center nodes. /// Waist cutters are tangent to both end circles; cut radius is solved from \\a waist. void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist); diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 0f5f23b5..3e75bf61 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -561,21 +561,55 @@ std::optional compute_bone_geom(const Bone_params& p) return bone_geom_with_cut_radius_(p.c1, p.c2, p.r1, p.r2, cut_r, axis, n, mid, vx, vy, a, h); } -TopoDS_Shape make_bone_preview_shape(const gp_Pln& pln, const Bone_geom& g) +Bone_profile get_bone_profile(const Bone_geom& g) { - auto circle_at = [&](const gp_Pnt2d& c, double r) -> TopoDS_Wire - { return make_circle_wire(pln, c, gp_Pnt2d(c.X() + r, c.Y())); }; + gp_Vec2d axis(g.c1, g.c2); + const double dist = axis.Magnitude(); + EZY_ASSERT(dist > Precision::Confusion()); + const gp_Vec2d ad(axis / dist); + const gp_Vec2d n = ad.Rotated(std::numbers::pi / 2.0); - TopoDS_Compound comp; - BRep_Builder bb; - bb.MakeCompound(comp); - bb.Add(comp, circle_at(g.c1, g.r1)); - bb.Add(comp, circle_at(g.c2, g.r2)); - bb.Add(comp, circle_at(g.cut_plus, g.cut_radius)); - bb.Add(comp, circle_at(g.cut_minus, g.cut_radius)); - bb.Add(comp, BRepBuilderAPI_MakeEdge(to_3d(pln, g.tan_top_a), to_3d(pln, g.tan_top_b)).Edge()); - bb.Add(comp, BRepBuilderAPI_MakeEdge(to_3d(pln, g.tan_bot_a), to_3d(pln, g.tan_bot_b)).Edge()); - return comp; + 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); + p.waist_plus = gp_Pnt2d(g.cut_plus).Translated(-n * g.cut_radius); + p.waist_minus = gp_Pnt2d(g.cut_minus).Translated(n * 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 diff --git a/src/utl_geom.h b/src/utl_geom.h index 4afad2f5..92d2bc37 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -112,10 +112,28 @@ struct Bone_geom 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); -/// Four circle wires plus the two external tangent edges (preview / debug). +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 diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 833a8804..57ff7271 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -969,6 +969,12 @@ TEST_F(Sketch_test, BoneGeom_equalRadiiTangentCuttersAndWaistFromCutRadius) EXPECT_NEAR(d2, g->cut_radius + p.r2, 1e-9); EXPECT_NEAR(g->waist, 2.0 * std::sqrt(5.0) - 4.0, 1e-9); + const Bone_profile pr = get_bone_profile(*g); + EXPECT_NEAR(pr.c1_plus.Distance(p.c1), p.r1, 1e-9); + EXPECT_NEAR(pr.c1_plus.Distance(g->cut_plus), g->cut_radius, 1e-9); + EXPECT_NEAR(pr.c2_plus.Distance(p.c2), p.r2, 1e-9); + EXPECT_NEAR(pr.c1_outer.X(), p.c1.X() - p.r1, 1e-9); + p.c1 = p.c2; EXPECT_FALSE(compute_bone_geom(p).has_value()); } @@ -997,8 +1003,8 @@ TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) Sketch sketch("BoneSketch", view(), default_plane); sketch.add_bone(gp_Pnt2d(-2.0, 0.0), gp_Pnt2d(2.0, 0.0), 1.0, 0.5, 0.4); - EXPECT_GE(sketch.face_count(), 1u); - EXPECT_GE(Sketch_access::get_edge_count(sketch), 8u); + EXPECT_EQ(sketch.face_count(), 1u); + EXPECT_EQ(Sketch_access::get_edge_count(sketch), 4u); bool found_a = false; bool found_b = false; From 86d43ef8e2102aeaf01306cb1d18dc109abf830a Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 2 Sep 2026 18:09:48 -0600 Subject: [PATCH 04/12] Improve --- src/doc/sketch.md | 5 +- src/skt_tools.cpp | 367 +---------------------------------------- src/skt_tools.h | 2 + src/skt_tools.inl | 45 +++++ src/skt_tools_bone.cpp | 340 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 393 insertions(+), 366 deletions(-) create mode 100644 src/skt_tools.inl create mode 100644 src/skt_tools_bone.cpp diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 13e51d64..3ccbeb6d 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -66,7 +66,7 @@ Sketch (coordinator: skt.cpp, skt.h) +-- Sketch_edges persistent edge list; add, split, remove, pick +-- Sketch_topo planar graph -> closed faces, edge splitting +-- Sketch_dims length dimensions, typed distance/angle input - +-- Sketch_tools interactive drawing session (tmp state) + +-- Sketch_tools interactive drawing session (tmp state; bone in skt_tools_bone.cpp) +-- Sketch_underlay raster image on the sketch plane Supporting (not owned sub-objects): @@ -203,7 +203,8 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::list -#include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -26,6 +23,8 @@ #include "utl_occt.h" #include "utl.h" +#include "skt_tools.inl" + using namespace glm; namespace @@ -121,52 +120,7 @@ void Sketch_tools::on_enter() break; case Mode::Sketch_add_bone: - if (!m_tmp_edges.empty() && !m_bone_centers && m_sketch.m_dims.entered_edge_len().has_value()) - { - Sketch_edge& edge = m_tmp_edges.back(); - const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; - m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * - m_sketch.m_dims.entered_edge_len()->len); - if (unique(pt_a, *m_last_pt)) - m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); - - 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]); - break; - } - if (!m_bone_centers || !m_sketch.m_dims.entered_edge_len().has_value()) - break; - - { - const double len = m_sketch.m_dims.entered_edge_len()->len; - if (len <= Precision::Confusion()) - break; - - Sketch_edge& edge = m_tmp_edges.back(); - const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; - m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * len); - if (unique(pt_a, *m_last_pt)) - m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); - - m_sketch.m_dims.clear_typed_constraints(); - - if (m_tmp_edges.size() == 2) - { - m_bone_r1 = len; - bone_begin_next_edge_from_(m_bone_centers->second); - } - else if (m_tmp_edges.size() == 3) - { - m_bone_r2 = 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_try_commit_(len); - } + bone_on_enter_(); break; case Mode::Sketch_add_multi_edges: @@ -920,278 +874,6 @@ void Sketch_tools::move_slot_pt_(const ScreenCoords& screen_coords) if_edge_pt_valid_(l); } -void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) -{ - if (m_tmp_edges.empty()) - { - add_line_string_pt_(screen_coords, Linestring_type::Multiple); - return; - } - - if (!m_bone_centers) - { - auto on_second = [&](size_t node_idx) - { - Sketch_edge& last = m_tmp_edges.back(); - if (node_idx == last.node_idx_a) - return; - - m_sketch.update_edge_end_pt_(last, node_idx); - bone_on_centers_ready_(m_sketch.m_nodes[last.node_idx_a], m_sketch.m_nodes[node_idx]); - }; - - if (m_sketch.m_dims.entered_edge_angle().has_value() && !m_tmp_edges.empty()) - { - std::optional pt_opt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); - if (!pt_opt) - return; - - const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_edges.back().node_idx_a]; - const double angle_rad = to_radians(*m_sketch.m_dims.entered_edge_angle()); - gp_Dir2d constrained_dir(std::cos(angle_rad), std::sin(angle_rad)); - gp_Vec2d to_click(pt_opt->X() - pt_a.X(), pt_opt->Y() - pt_a.Y()); - const double dist_along = to_click.Dot(gp_Vec2d(constrained_dir)); - gp_Pnt2d final_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(constrained_dir) * dist_along); - if (!unique(pt_a, final_pt)) - return; - - const size_t node_idx = m_sketch.m_nodes.get_node_exact(final_pt); - m_tmp_node_idxs.push_back(node_idx); - on_second(node_idx); - return; - } - - add_sketch_pt_(screen_coords, 1, on_second); - return; - } - - auto on_dim = [&](size_t node_idx) - { - Sketch_edge& last = m_tmp_edges.back(); - if (node_idx == last.node_idx_a) - return; - - const gp_Pnt2d& pt_a = m_sketch.m_nodes[last.node_idx_a]; - const gp_Pnt2d& pt_b = m_sketch.m_nodes[node_idx]; - if (!unique(pt_a, pt_b)) - return; - - m_sketch.update_edge_end_pt_(last, node_idx); - const double len = pt_a.Distance(pt_b); - - if (m_tmp_edges.size() == 2) - { - m_bone_r1 = len; - bone_begin_next_edge_from_(m_bone_centers->second); - } - else if (m_tmp_edges.size() == 3) - { - m_bone_r2 = len; - bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); - } - else if (m_tmp_edges.size() == 4) - { - const std::optional waist = bone_waist_from_pt_(pt_b); - if (waist) - (void)bone_try_commit_(*waist); - } - }; - - add_sketch_pt_(screen_coords, 1, on_dim); -} - -void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) -{ - if (!m_bone_centers || m_tmp_edges.size() <= 1) - { - move_line_string_pt_(screen_coords); - return; - } - - if (m_tmp_edges.size() < 4) - { - move_line_string_pt_(screen_coords); - bone_update_preview_(); - return; - } - - const std::optional n = bone_axis_perp_(); - if (!n) - return; - - const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); - - auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) - { - Sketch_edge& edge = m_tmp_edges.back(); - double half = std::abs(gp_Vec2d(mid, pt_b).Dot(*n)); - if (m_sketch.m_dims.entered_edge_len().has_value()) - half = m_sketch.m_dims.entered_edge_len()->len * 0.5; - - if (half <= Precision::Confusion()) - { - m_sketch.m_dims.clear_tmp_dim_anno(); - m_sketch.m_view.remove(m_tmp_shp); - m_tmp_shp = nullptr; - return; - } - - const gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); - const gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); - m_last_pt = span_b; - m_sketch.update_edge_shp_(edge, span_a, span_b); - - const double dist = (2.0 * half) / m_sketch.m_view.get_display_to_model_scale(); - m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); - m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); - bone_update_preview_(); - }; - - move_sketch_pt_(screen_coords, l); -} - -void Sketch_tools::bone_begin_next_edge_from_(const gp_Pnt2d& origin) -{ - m_sketch.m_dims.clear_typed_constraints(); - m_sketch.m_dims.set_show_angle_input(false); - m_sketch.m_view.gui().hide_angle_edit(); - m_sketch.m_view.gui().hide_dist_edit(false); - m_tmp_edges.push_back({m_sketch.m_nodes.get_node_exact(origin)}); -} - -void Sketch_tools::bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2) -{ - if (!unique(c1, c2)) - return; - - m_bone_centers = std::make_pair(c1, c2); - bone_begin_next_edge_from_(c1); -} - -bool Sketch_tools::bone_try_commit_(double waist) -{ - if (!m_bone_centers || !m_bone_r1 || !m_bone_r2) - return false; - - Bone_params params; - params.c1 = m_bone_centers->first; - params.c2 = m_bone_centers->second; - params.r1 = *m_bone_r1; - params.r2 = *m_bone_r2; - params.waist = waist; - params.drive = Bone_drive::Waist; - if (!compute_bone_geom(params)) - return false; - - m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, waist); - clear_tmps(); - return true; -} - -void Sketch_tools::finalize_bone_() -{ - if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || m_tmp_edges.size() != 4 || !m_last_pt) - return; - - const std::optional waist = bone_waist_from_pt_(*m_last_pt); - if (waist) - (void)bone_try_commit_(*waist); -} - -std::optional Sketch_tools::bone_axis_perp_() const -{ - if (!m_bone_centers) - return std::nullopt; - - gp_Vec2d axis(m_bone_centers->first, m_bone_centers->second); - const double dist = axis.Magnitude(); - if (dist <= Precision::Confusion()) - return std::nullopt; - - return gp_Vec2d(axis / dist).Rotated(std::numbers::pi / 2.0); -} - -std::optional Sketch_tools::bone_waist_from_pt_(const gp_Pnt2d& pt) const -{ - const std::optional n = bone_axis_perp_(); - if (!n || !m_bone_centers) - return std::nullopt; - - const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); - const double waist = 2.0 * std::abs(gp_Vec2d(mid, pt).Dot(*n)); - if (waist <= Precision::Confusion()) - return std::nullopt; - - return waist; -} - -void Sketch_tools::bone_update_preview_() -{ - if (!m_bone_centers) - return; - - const gp_Pnt2d& c1 = m_bone_centers->first; - const gp_Pnt2d& c2 = m_bone_centers->second; - - 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())); }; - - const double r1 = m_bone_r1.value_or( - (m_tmp_edges.size() == 2 && 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); - - if (m_tmp_edges.size() >= 4 && m_bone_r1 && m_bone_r2) - { - const std::optional waist = - m_sketch.m_dims.entered_edge_len().has_value() ? std::optional(m_sketch.m_dims.entered_edge_len()->len) - : (m_last_pt ? bone_waist_from_pt_(*m_last_pt) : std::nullopt); - if (waist) - { - Bone_params params; - params.c1 = c1; - params.c2 = c2; - params.r1 = *m_bone_r1; - params.r2 = *m_bone_r2; - params.waist = *waist; - params.drive = Bone_drive::Waist; - if (const std::optional g = compute_bone_geom(params)) - { - show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); - return; - } - } - - m_sketch.m_view.remove(m_tmp_shp); - m_tmp_shp = nullptr; - return; - } - - TopoDS_Compound comp; - BRep_Builder bb; - bb.MakeCompound(comp); - bool any = false; - if (r1 > Precision::Confusion()) - { - bb.Add(comp, circle_at(c1, r1)); - any = true; - } - if (r2 > Precision::Confusion()) - { - bb.Add(comp, circle_at(c2, r2)); - any = true; - } - - if (!any) - { - m_sketch.m_view.remove(m_tmp_shp); - m_tmp_shp = nullptr; - return; - } - - show(m_sketch.m_ctx, m_tmp_shp, comp); -} - void Sketch_tools::finalize_slot_(Sketch_op_recorder& rec) { EZY_ASSERT(m_tmp_edges.size() == 2); @@ -1251,49 +933,6 @@ bool Sketch_tools::clear_tmps() return operation_canceled; } -// General sketch point related -template -void Sketch_tools::add_sketch_pt_(const ScreenCoords& screen_coords, size_t required_num_pts, Callback&& callback) -{ - auto l = [&](const std::optional& node_idx, const gp_Pnt2d& pt) - { - if (node_idx) - m_tmp_node_idxs.push_back(*node_idx); - else - m_tmp_node_idxs.push_back(m_sketch.m_nodes.add_new_node(pt)); - - if (m_tmp_node_idxs.size() >= required_num_pts) - callback(m_tmp_node_idxs.back()); - }; - - move_sketch_pt_(screen_coords, l); -} - -template void Sketch_tools::move_sketch_pt_(const ScreenCoords& screen_coords, Callback&& callback) -{ - m_last_pt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); - if (!m_last_pt) - // View plane and sketch plane must be perpendicular. - return; - - std::optional node_idx = m_sketch.m_nodes.try_get_node_idx_snap(*m_last_pt); - - callback(node_idx, *m_last_pt); -} - -/// Invokes callback(e, pt_a, pt_b) with the last tmp edge only when it exists and is non-degenerate. -template void Sketch_tools::if_edge_pt_valid_(Callback&& callback) -{ - if (m_tmp_edges.empty()) - return; - - Sketch_edge& e = m_tmp_edges.back(); - const gp_Pnt2d& pt_a = m_sketch.m_nodes[e.node_idx_a]; - if (m_last_pt.has_value()) - if (unique(pt_a, *m_last_pt)) - callback(e, pt_a, *m_last_pt); -} - void Sketch_tools::finalize_add_node_elm_cleanup_() { if (!m_tmp_edges.empty() && !m_tmp_edges.back().node_idx_b.has_value()) diff --git a/src/skt_tools.h b/src/skt_tools.h index ea2e7338..7d9ca73e 100644 --- a/src/skt_tools.h +++ b/src/skt_tools.h @@ -72,6 +72,8 @@ class Sketch_tools void move_slot_pt_(const ScreenCoords& screen_coords); void finalize_slot_(Sketch_op_recorder& rec); + // Implementations in skt_tools_bone.cpp + void bone_on_enter_(); void add_bone_pt_(const ScreenCoords& screen_coords); void move_bone_pt_(const ScreenCoords& screen_coords); void finalize_bone_(); diff --git a/src/skt_tools.inl b/src/skt_tools.inl new file mode 100644 index 00000000..8a9a67c4 --- /dev/null +++ b/src/skt_tools.inl @@ -0,0 +1,45 @@ +#pragma once + +// Template helpers for Sketch_tools. Include from skt_tools*.cpp after "skt.h". + +template +void Sketch_tools::add_sketch_pt_(const ScreenCoords& screen_coords, size_t required_num_pts, Callback&& callback) +{ + auto l = [&](const std::optional& node_idx, const gp_Pnt2d& pt) + { + if (node_idx) + m_tmp_node_idxs.push_back(*node_idx); + else + m_tmp_node_idxs.push_back(m_sketch.m_nodes.add_new_node(pt)); + + if (m_tmp_node_idxs.size() >= required_num_pts) + callback(m_tmp_node_idxs.back()); + }; + + move_sketch_pt_(screen_coords, l); +} + +template void Sketch_tools::move_sketch_pt_(const ScreenCoords& screen_coords, Callback&& callback) +{ + m_last_pt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); + if (!m_last_pt) + // View plane and sketch plane must be perpendicular. + return; + + std::optional node_idx = m_sketch.m_nodes.try_get_node_idx_snap(*m_last_pt); + + callback(node_idx, *m_last_pt); +} + +/// Invokes callback(e, pt_a, pt_b) with the last tmp edge only when it exists and is non-degenerate. +template void Sketch_tools::if_edge_pt_valid_(Callback&& callback) +{ + if (m_tmp_edges.empty()) + return; + + Sketch_edge& e = m_tmp_edges.back(); + const gp_Pnt2d& pt_a = m_sketch.m_nodes[e.node_idx_a]; + if (m_last_pt.has_value()) + if (unique(pt_a, *m_last_pt)) + callback(e, pt_a, *m_last_pt); +} diff --git a/src/skt_tools_bone.cpp b/src/skt_tools_bone.cpp new file mode 100644 index 00000000..bbab22f8 --- /dev/null +++ b/src/skt_tools_bone.cpp @@ -0,0 +1,340 @@ +#include "skt_tools.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gui.h" +#include "gui_occt_view.h" +#include "skt.h" +#include "utl_geom.h" +#include "utl_occt.h" +#include "utl.h" + +#include "skt_tools.inl" + +void Sketch_tools::bone_on_enter_() +{ + if (!m_tmp_edges.empty() && !m_bone_centers && m_sketch.m_dims.entered_edge_len().has_value()) + { + Sketch_edge& edge = m_tmp_edges.back(); + const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; + m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * + m_sketch.m_dims.entered_edge_len()->len); + if (unique(pt_a, *m_last_pt)) + m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); + + 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; + + const double len = m_sketch.m_dims.entered_edge_len()->len; + if (len <= Precision::Confusion()) + return; + + Sketch_edge& edge = m_tmp_edges.back(); + const gp_Pnt2d& pt_a = m_sketch.m_nodes[edge.node_idx_a]; + m_last_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(m_sketch.m_dims.entered_edge_len()->dir) * len); + if (unique(pt_a, *m_last_pt)) + m_sketch.update_edge_end_pt_(edge, m_sketch.m_nodes.get_node_exact(*m_last_pt)); + + m_sketch.m_dims.clear_typed_constraints(); + + if (m_tmp_edges.size() == 2) + { + m_bone_r1 = len; + bone_begin_next_edge_from_(m_bone_centers->second); + } + else if (m_tmp_edges.size() == 3) + { + m_bone_r2 = 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_try_commit_(len); +} + +void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) +{ + if (m_tmp_edges.empty()) + { + add_line_string_pt_(screen_coords, Linestring_type::Multiple); + return; + } + + if (!m_bone_centers) + { + auto on_second = [&](size_t node_idx) + { + Sketch_edge& last = m_tmp_edges.back(); + if (node_idx == last.node_idx_a) + return; + + m_sketch.update_edge_end_pt_(last, node_idx); + bone_on_centers_ready_(m_sketch.m_nodes[last.node_idx_a], m_sketch.m_nodes[node_idx]); + }; + + if (m_sketch.m_dims.entered_edge_angle().has_value() && !m_tmp_edges.empty()) + { + std::optional pt_opt = m_sketch.m_view.pt_on_plane(screen_coords, m_sketch.m_pln); + if (!pt_opt) + return; + + const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_edges.back().node_idx_a]; + const double angle_rad = to_radians(*m_sketch.m_dims.entered_edge_angle()); + gp_Dir2d constrained_dir(std::cos(angle_rad), std::sin(angle_rad)); + gp_Vec2d to_click(pt_opt->X() - pt_a.X(), pt_opt->Y() - pt_a.Y()); + const double dist_along = to_click.Dot(gp_Vec2d(constrained_dir)); + gp_Pnt2d final_pt = gp_Pnt2d(pt_a).Translated(gp_Vec2d(constrained_dir) * dist_along); + if (!unique(pt_a, final_pt)) + return; + + const size_t node_idx = m_sketch.m_nodes.get_node_exact(final_pt); + m_tmp_node_idxs.push_back(node_idx); + on_second(node_idx); + return; + } + + add_sketch_pt_(screen_coords, 1, on_second); + return; + } + + auto on_dim = [&](size_t node_idx) + { + Sketch_edge& last = m_tmp_edges.back(); + if (node_idx == last.node_idx_a) + return; + + const gp_Pnt2d& pt_a = m_sketch.m_nodes[last.node_idx_a]; + const gp_Pnt2d& pt_b = m_sketch.m_nodes[node_idx]; + if (!unique(pt_a, pt_b)) + return; + + m_sketch.update_edge_end_pt_(last, node_idx); + const double len = pt_a.Distance(pt_b); + + if (m_tmp_edges.size() == 2) + { + m_bone_r1 = len; + bone_begin_next_edge_from_(m_bone_centers->second); + } + else if (m_tmp_edges.size() == 3) + { + m_bone_r2 = len; + bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); + } + else if (m_tmp_edges.size() == 4) + { + const std::optional waist = bone_waist_from_pt_(pt_b); + if (waist) + (void)bone_try_commit_(*waist); + } + }; + + add_sketch_pt_(screen_coords, 1, on_dim); +} + +void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) +{ + if (!m_bone_centers || m_tmp_edges.size() <= 1) + { + move_line_string_pt_(screen_coords); + return; + } + + if (m_tmp_edges.size() < 4) + { + move_line_string_pt_(screen_coords); + bone_update_preview_(); + return; + } + + const std::optional n = bone_axis_perp_(); + if (!n) + return; + + const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); + + auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) + { + Sketch_edge& edge = m_tmp_edges.back(); + double half = std::abs(gp_Vec2d(mid, pt_b).Dot(*n)); + if (m_sketch.m_dims.entered_edge_len().has_value()) + half = m_sketch.m_dims.entered_edge_len()->len * 0.5; + + if (half <= Precision::Confusion()) + { + m_sketch.m_dims.clear_tmp_dim_anno(); + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + const gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); + const gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); + m_last_pt = span_b; + m_sketch.update_edge_shp_(edge, span_a, span_b); + + const double dist = (2.0 * half) / m_sketch.m_view.get_display_to_model_scale(); + m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); + m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); + bone_update_preview_(); + }; + + move_sketch_pt_(screen_coords, l); +} + +void Sketch_tools::bone_begin_next_edge_from_(const gp_Pnt2d& origin) +{ + m_sketch.m_dims.clear_typed_constraints(); + m_sketch.m_dims.set_show_angle_input(false); + m_sketch.m_view.gui().hide_angle_edit(); + m_sketch.m_view.gui().hide_dist_edit(false); + m_tmp_edges.push_back({m_sketch.m_nodes.get_node_exact(origin)}); +} + +void Sketch_tools::bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2) +{ + if (!unique(c1, c2)) + return; + + m_bone_centers = std::make_pair(c1, c2); + bone_begin_next_edge_from_(c1); +} + +bool Sketch_tools::bone_try_commit_(double waist) +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2) + return false; + + Bone_params params; + params.c1 = m_bone_centers->first; + params.c2 = m_bone_centers->second; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = waist; + params.drive = Bone_drive::Waist; + if (!compute_bone_geom(params)) + return false; + + m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, waist); + clear_tmps(); + return true; +} + +void Sketch_tools::finalize_bone_() +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || m_tmp_edges.size() != 4 || !m_last_pt) + return; + + const std::optional waist = bone_waist_from_pt_(*m_last_pt); + if (waist) + (void)bone_try_commit_(*waist); +} + +std::optional Sketch_tools::bone_axis_perp_() const +{ + if (!m_bone_centers) + return std::nullopt; + + gp_Vec2d axis(m_bone_centers->first, m_bone_centers->second); + const double dist = axis.Magnitude(); + if (dist <= Precision::Confusion()) + return std::nullopt; + + return gp_Vec2d(axis / dist).Rotated(std::numbers::pi / 2.0); +} + +std::optional Sketch_tools::bone_waist_from_pt_(const gp_Pnt2d& pt) const +{ + const std::optional n = bone_axis_perp_(); + if (!n || !m_bone_centers) + return std::nullopt; + + const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); + const double waist = 2.0 * std::abs(gp_Vec2d(mid, pt).Dot(*n)); + if (waist <= Precision::Confusion()) + return std::nullopt; + + return waist; +} + +void Sketch_tools::bone_update_preview_() +{ + if (!m_bone_centers) + return; + + const gp_Pnt2d& c1 = m_bone_centers->first; + const gp_Pnt2d& c2 = m_bone_centers->second; + + 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())); }; + + const double r1 = m_bone_r1.value_or( + (m_tmp_edges.size() == 2 && 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); + + if (m_tmp_edges.size() >= 4 && m_bone_r1 && m_bone_r2) + { + const std::optional waist = + m_sketch.m_dims.entered_edge_len().has_value() ? std::optional(m_sketch.m_dims.entered_edge_len()->len) + : (m_last_pt ? bone_waist_from_pt_(*m_last_pt) : std::nullopt); + if (waist) + { + Bone_params params; + params.c1 = c1; + params.c2 = c2; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = *waist; + params.drive = Bone_drive::Waist; + if (const std::optional g = compute_bone_geom(params)) + { + show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); + return; + } + } + + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bool any = false; + if (r1 > Precision::Confusion()) + { + bb.Add(comp, circle_at(c1, r1)); + any = true; + } + if (r2 > Precision::Confusion()) + { + bb.Add(comp, circle_at(c2, r2)); + any = true; + } + + if (!any) + { + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + show(m_sketch.m_ctx, m_tmp_shp, comp); +} From b39d54915f1826ec89f91379d15f496ca87bd2d2 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 16:58:29 -0600 Subject: [PATCH 05/12] Fix --- src/skt_dims.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/skt_dims.cpp b/src/skt_dims.cpp index 55fe3553..a2059f4b 100644 --- a/src/skt_dims.cpp +++ b/src/skt_dims.cpp @@ -66,7 +66,13 @@ void Sketch_dims::on_finalize_elm_start() clear_tmp_dim_anno(); } -void Sketch_dims::on_clear_tmps() { clear_all(m_entered_edge_len, m_show_dim_input, m_entered_edge_angle, m_show_angle_input); } +void Sketch_dims::on_clear_tmps() +{ + clear_all(m_entered_edge_len, m_show_dim_input, m_entered_edge_angle, m_show_angle_input); + m_sketch.m_view.gui().hide_dist_edit(false); + m_sketch.m_view.gui().hide_angle_edit(); + clear_tmp_dim_anno(); +} void Sketch_dims::clear_typed_constraints() { From 810b2a93f75a6c9ebabedb161c39ba62fa814625 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 17:07:58 -0600 Subject: [PATCH 06/12] Bug fix. --- src/skt_op_recorder.cpp | 36 ++++++++++++++++++++++------- tests/skt_tests.cpp | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/skt_op_recorder.cpp b/src/skt_op_recorder.cpp index 94430599..84c1a3da 100644 --- a/src/skt_op_recorder.cpp +++ b/src/skt_op_recorder.cpp @@ -56,8 +56,9 @@ struct Sketch_op_data struct Curr_node_record { - gp_Pnt2d pt; - bool permanent{false}; + gp_Pnt2d pt; + bool permanent{false}; + std::string name; }; Sketch* m_sketch{nullptr}; @@ -129,7 +130,12 @@ class Sketch_op_recorder::Impl Sketch_op_recorder* m_owner{nullptr}; bool m_active{true}; bool m_committed{false}; - std::vector m_live_node_pts_at_start; + struct Live_node_at_start + { + gp_Pnt2d pt; + bool permanent{false}; + }; + std::vector m_live_nodes_at_start; std::vector m_linear_edges_at_start; Sketch_op_data m_data; @@ -222,7 +228,7 @@ Sketch_op_recorder::Impl::Impl(Occt_view& view, Sketch& sketch) { for (size_t i = 0, n = sketch.m_nodes.size(); i < n; ++i) if (!sketch.m_nodes[i].deleted) - m_live_node_pts_at_start.push_back(sketch.m_nodes[i]); + m_live_nodes_at_start.push_back({sketch.m_nodes[i], sketch.m_nodes[i].permanent}); Sketch_op_data::capture_linear_edges_at_start_(sketch, m_linear_edges_at_start); m_data.m_sketch = &sketch; @@ -307,17 +313,27 @@ void Sketch_op_recorder::Impl::note_curr_node(size_t node_idx) if (!m_active) return; - const gp_Pnt2d pt = m_sketch.m_nodes[node_idx]; + const gp_Pnt2d pt = m_sketch.m_nodes[node_idx]; + const bool now_permanent = m_sketch.m_nodes[node_idx].permanent; + + for (const Live_node_at_start& live : m_live_nodes_at_start) + { + if (!pts_equal_(live.pt, pt)) + continue; - for (const gp_Pnt2d& live : m_live_node_pts_at_start) - if (pts_equal_(live, pt)) + // Pre-existing permanent nodes are not owned by this op. Pre-existing non-permanent + // nodes stay owned by topology unless this op promotes them to permanent (e.g. bone centers). + if (live.permanent || !now_permanent) return; + break; + } + for (const Sketch_op_data::Curr_node_record& x : m_data.curr_nodes) if (pts_equal_(x.pt, pt)) return; - m_data.curr_nodes.push_back({pt, m_sketch.m_nodes[node_idx].permanent}); + m_data.curr_nodes.push_back({pt, now_permanent, m_sketch.m_nodes[node_idx].name}); } void Sketch_op_recorder::Impl::note_prev_length_dim(size_t lo, size_t hi, bool visible, std::optional flyout, @@ -463,6 +479,7 @@ void Sketch_op_data::apply_reverse_(Occt_view& view) const for (const Curr_node_record& node : curr_nodes) tombstone_node_at_pt_(*sketch, node.pt); + sketch->m_node_marks.sync(); sketch->m_nodes.hide_snap_annos(); sketch->update_faces_(); } @@ -615,6 +632,9 @@ void Sketch_op_data::restore_curr_node_at_pt_(Sketch& sketch, const Curr_node_re } const size_t node_idx = sketch.m_nodes.get_node_exact(rec.pt, rec.permanent); + if (!rec.name.empty()) + sketch.m_nodes[node_idx].name = rec.name; + if (is_arc_bulge) return; diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 57ff7271..fbedf4f6 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -1033,3 +1033,54 @@ TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) EXPECT_NE(std::find(labels.begin(), labels.end(), "Bone A"), labels.end()); EXPECT_NE(std::find(labels.begin(), labels.end(), "Bone B"), labels.end()); } + +TEST_F(Sketch_test, AddBone_undoRemovesPromotedCenters) +{ + Headless_guard guard(view()); + + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("BoneSketch", view(), default_plane); + + const gp_Pnt2d c1(-2.0, 0.0); + const gp_Pnt2d c2(2.0, 0.0); + // Interactive tool creates non-permanent center nodes before commit; undo must still remove them. + sketch.get_nodes().get_node_exact(c1); + sketch.get_nodes().get_node_exact(c2); + + sketch.add_bone(c1, c2, 1.0, 0.5, 0.4); + EXPECT_EQ(sketch.face_count(), 1u); + EXPECT_EQ(Sketch_access::get_edge_count(sketch), 4u); + EXPECT_EQ(Sketch_access::count_permanent_nodes(sketch), 3u) << "Origin plus Bone A and Bone B"; + + ASSERT_GT(view().undo_stack_size(), 0u); + EXPECT_TRUE(view().undo()); + EXPECT_EQ(Sketch_access::get_edge_count(sketch), 0u); + EXPECT_EQ(Sketch_access::count_permanent_nodes(sketch), 1u) << "Undo should remove Bone A/B centers"; + + EXPECT_TRUE(view().redo()); + EXPECT_EQ(Sketch_access::get_edge_count(sketch), 4u); + EXPECT_EQ(Sketch_access::count_permanent_nodes(sketch), 3u); + + bool found_a = false; + bool found_b = false; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (n.deleted || !n.permanent) + continue; + + if (n.name == "Bone A") + { + found_a = true; + EXPECT_TRUE(n.IsEqual(c1, Precision::Confusion())); + } + if (n.name == "Bone B") + { + found_b = true; + EXPECT_TRUE(n.IsEqual(c2, Precision::Confusion())); + } + } + + EXPECT_TRUE(found_a); + EXPECT_TRUE(found_b); +} From 8c9c2274e523a897851b461b90a2683aff7352ea Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 17:15:23 -0600 Subject: [PATCH 07/12] Waist point fix. --- CHANGELOG.md | 2 +- docs/usage-sketch.md | 4 ++-- src/gui_mode.cpp | 3 ++- src/skt_tools_bone.cpp | 38 +++++++++++++++++++++++++++++--------- src/utl_geom.cpp | 31 +++++++++++++++++++------------ tests/skt_tests.cpp | 22 ++++++++++++++++++++++ 6 files changed, 75 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d99853..4938c909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,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 cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs) and permanent **Bone A** / **Bone B** center nodes. Remappable as **`mode.add_bone`**. +- **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 (live dim sits on that section; offset from mid when end radii differ). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs) and permanent **Bone A** / **Bone B** center nodes. 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/docs/usage-sketch.md b/docs/usage-sketch.md index bfcb70b0..33d642b1 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -722,7 +722,7 @@ The bone tool builds a connecting-rod outline: the outer arcs of the two end cir 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. -6. Click a point to set **waist width** (remaining thickness at the midpoint, perpendicular to the center line). The live dimension spans the full waist. Tab enters an exact waist. The tool commits after this click. +6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). The live dimension spans that neck; when the end radii differ, the neck is offset toward the smaller end, not at the center midpoint. Tab enters an exact waist. The tool commits after this click. Waist cutters are tangent to both end circles (cut radius is solved from the waist). Geometry fails when the two centers coincide, one circle sits inside the other, or the waist cannot be realized. @@ -732,7 +732,7 @@ The sketch keeps only that trimmed outline (four arcs). Extrude it with the norm - Snap the two centers to existing nodes, or to the sketch origin. - After Add, use **Add circle** snapped to **Bone A** / **Bone B** if you want a different hole radius than the end circles. -- For waist, click on one side of the neck; the distance from the bone axis to that point is half the waist. +- For waist, click on either side of the bone axis; the distance from the axis to that point is half the waist (the dim then snaps to the true neck). ## Operation Axis Tool diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 465b96f5..d0accd08 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -1263,7 +1263,8 @@ void GUI::options_sketch_add_bone_mode_() options_sketch_common_(); options_sketch_len_angle_hotkeys_(); ImGui::Separator(); - ImGui::TextWrapped("Click center A, center B, radius 1, radius 2, then the waist width."); + ImGui::TextWrapped( + "Click center A, center B, radius 1, radius 2, then the waist (minimum neck width)."); } void GUI::options_orthographic_projection_() diff --git a/src/skt_tools_bone.cpp b/src/skt_tools_bone.cpp index bbab22f8..e47d6c88 100644 --- a/src/skt_tools_bone.cpp +++ b/src/skt_tools_bone.cpp @@ -163,15 +163,18 @@ void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) } const std::optional n = bone_axis_perp_(); - if (!n) + if (!n || !m_bone_r1 || !m_bone_r2) return; - const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); + const gp_Pnt2d& c1 = m_bone_centers->first; + const gp_Pnt2d& c2 = m_bone_centers->second; + const gp_Pnt2d mid = get_midpoint(c1, c2); auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) { Sketch_edge& edge = m_tmp_edges.back(); - double half = std::abs(gp_Vec2d(mid, pt_b).Dot(*n)); + // Waist value is twice the distance from the bone axis (independent of click along-axis). + double half = std::abs(gp_Vec2d(c1, pt_b).Dot(*n)); if (m_sketch.m_dims.entered_edge_len().has_value()) half = m_sketch.m_dims.entered_edge_len()->len * 0.5; @@ -183,12 +186,29 @@ void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) return; } - const gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); - const gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); - m_last_pt = span_b; + const double waist = 2.0 * half; + gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); + gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); + + Bone_params params; + params.c1 = c1; + params.c2 = c2; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = waist; + params.drive = Bone_drive::Waist; + if (const std::optional g = compute_bone_geom(params)) + { + // Place the live dim on the true neck (offset from mid when r1 != r2). + const Bone_profile pr = get_bone_profile(*g); + span_a = pr.waist_plus; + span_b = pr.waist_minus; + } + + m_last_pt = span_b; m_sketch.update_edge_shp_(edge, span_a, span_b); - const double dist = (2.0 * half) / m_sketch.m_view.get_display_to_model_scale(); + const double dist = waist / m_sketch.m_view.get_display_to_model_scale(); m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); bone_update_preview_(); @@ -264,8 +284,8 @@ std::optional Sketch_tools::bone_waist_from_pt_(const gp_Pnt2d& pt) cons if (!n || !m_bone_centers) return std::nullopt; - const gp_Pnt2d mid = get_midpoint(m_bone_centers->first, m_bone_centers->second); - const double waist = 2.0 * std::abs(gp_Vec2d(mid, pt).Dot(*n)); + // Twice the distance from the bone axis (c1->c2); matches min neck when cutters solve. + const double waist = 2.0 * std::abs(gp_Vec2d(m_bone_centers->first, pt).Dot(*n)); if (waist <= Precision::Confusion()) return std::nullopt; diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 3e75bf61..7fe67af8 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -416,9 +416,10 @@ std::optional circle_circle_intersect_pick_side_(const gp_Pnt2d& c1, d return d0 <= d1 ? i0 : i1; } -double bone_waist_gap_(const gp_Pnt2d& c_top, const gp_Pnt2d& c_bot, double cut_r, const gp_Vec2d& n) +double bone_waist_gap_(const gp_Pnt2d& c_top, const gp_Pnt2d& c_bot, double cut_r) { - return gp_Vec2d(c_bot, c_top).Dot(n) - 2.0 * 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, @@ -442,7 +443,7 @@ std::optional bone_geom_with_cut_radius_(const gp_Pnt2d& c1, const gp 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, n); + g.waist = bone_waist_gap_(g.cut_plus, g.cut_minus, cut_r); auto tangent_pair = [&](double sign, gp_Pnt2d& t1, gp_Pnt2d& t2) { @@ -567,7 +568,6 @@ Bone_profile get_bone_profile(const Bone_geom& g) const double dist = axis.Magnitude(); EZY_ASSERT(dist > Precision::Confusion()); const gp_Vec2d ad(axis / dist); - const gp_Vec2d n = ad.Rotated(std::numbers::pi / 2.0); auto contact = [](const gp_Pnt2d& c, double r, const gp_Pnt2d& cut) -> gp_Pnt2d { @@ -578,14 +578,21 @@ Bone_profile get_bone_profile(const Bone_geom& g) }; 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); - p.waist_plus = gp_Pnt2d(g.cut_plus).Translated(-n * g.cut_radius); - p.waist_minus = gp_Pnt2d(g.cut_minus).Translated(n * g.cut_radius); + 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; } diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index fbedf4f6..c0d9b74f 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -995,6 +995,28 @@ TEST_F(Sketch_test, BoneGeom_waistDriveSolvesCutRadius) EXPECT_GT(g->cut_radius, 0.0); } +TEST_F(Sketch_test, BoneGeom_unequalRadiiNeckOffsetFromMid) +{ + Bone_params p; + p.c1 = gp_Pnt2d(-2.0, 0.0); + p.c2 = gp_Pnt2d(2.0, 0.0); + p.r1 = 1.0; + p.r2 = 0.5; + p.waist = 0.4; + p.drive = Bone_drive::Waist; + + const std::optional g = compute_bone_geom(p); + ASSERT_TRUE(g.has_value()); + EXPECT_NEAR(g->waist, 0.4, 1e-6); + + const Bone_profile pr = get_bone_profile(*g); + EXPECT_NEAR(pr.waist_plus.Distance(pr.waist_minus), p.waist, 1e-6); + // Neck lies toward the smaller end (c2), not at the center midpoint. + EXPECT_GT(pr.waist_plus.X(), 0.0); + EXPECT_NEAR(pr.waist_plus.X(), pr.waist_minus.X(), 1e-9); + EXPECT_NEAR(pr.waist_plus.X(), g->cut_plus.X(), 1e-9); +} + TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) { Headless_guard guard(view()); From 23e3495d982396b9a77ea5ba8260f0c98fc4f531 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 17:32:36 -0600 Subject: [PATCH 08/12] Improve --- CHANGELOG.md | 2 +- agents.md | 1 + agents/README.md | 2 +- agents/conventions/token-lean.md | 2 +- docs/usage-settings.md | 131 +++++++++++------------ docs/usage-sketch.md | 13 ++- res/ezycad_settings.json | 2 + src/doc/gui.md | 13 ++- src/doc/sketch.md | 2 +- src/gui.h | 11 ++ src/gui_mode.cpp | 51 ++++++++- src/gui_settings.cpp | 12 +++ src/mode.h | 17 +++ src/skt.cpp | 32 +++++- src/skt.h | 8 +- src/skt_tools.cpp | 3 + src/skt_tools.h | 8 +- src/skt_tools_bone.cpp | 171 ++++++++++++++++++++++++++++--- tests/skt_tests.cpp | 21 ++++ 19 files changed, 408 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4938c909..afac3812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,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 (live dim sits on that section; offset from mid when end radii differ). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs) and permanent **Bone A** / **Bone B** center nodes. Remappable as **`mode.add_bone`**. +- **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 (live dim sits on that section; offset from mid when end radii differ). 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. 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.md b/agents.md index 3ea081a7..e6812116 100644 --- a/agents.md +++ b/agents.md @@ -18,6 +18,7 @@ Pointer for AI coding assistants. Details live in [agents/README.md](agents/READ - UI/settings/docs changes: [agents/conventions/user-docs-sync.md](agents/conventions/user-docs-sync.md) - **New `Mode` / toolbar tool / one-shot `Command`:** update remappable hotkeys — checklist in [src/doc/gui.md](src/doc/gui.md#new-mode-or-toolbar-command-hotkeys) +- **Sketch tool Options pane:** tool-specific **Options** above **Sketch options** — [src/doc/gui.md](src/doc/gui.md#options-panel-layout-sketch-tools) - Markdown tables (align for source + preview): [agents/conventions/markdown-tables.md](agents/conventions/markdown-tables.md) - Sketch subsystem: [src/doc/sketch.md](src/doc/sketch.md) (read; update when API or architecture changes) - Shape module: [src/doc/shape.md](src/doc/shape.md) (read; update when API or operation patterns change) diff --git a/agents/README.md b/agents/README.md index cea81aea..e217ff70 100644 --- a/agents/README.md +++ b/agents/README.md @@ -13,7 +13,7 @@ Root markers: [AGENTS.md](../AGENTS.md) / [agents.md](../agents.md). | 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 | -| GUI module (dev doc) | [src/doc/gui.md](../src/doc/gui.md) — read when editing `gui_*` / viewer shell; update if routing or settings change; **new Mode/Command → hotkeys checklist** | +| GUI module (dev doc) | [src/doc/gui.md](../src/doc/gui.md) — read when editing `gui_*` / viewer shell; update if routing or settings change; **new Mode/Command → hotkeys checklist**; **sketch Options: tool Options above Sketch options** | | Script module (dev doc) | [src/doc/script.md](../src/doc/script.md) — read when editing `scr_*`; update if bindings change | | Utility module (dev doc) | [src/doc/utility.md](../src/doc/utility.md) — read when editing `utl_*`; update if shared helpers or I/O change | | Build / test / wasm | [workflows/local-dev.md](workflows/local-dev.md) | diff --git a/agents/conventions/token-lean.md b/agents/conventions/token-lean.md index f157a2e7..228a9168 100644 --- a/agents/conventions/token-lean.md +++ b/agents/conventions/token-lean.md @@ -21,7 +21,7 @@ Goal: give assistants **only what they need** for the task at hand. Full style g | User-visible UI/settings | [user-docs-sync.md](user-docs-sync.md) + target `docs/usage-*.md` only | | Sketch subsystem (`src/skt*`, `tests/skt_*`, sketch behavior in `occt_view` / `gui`) | [src/doc/sketch.md](../../src/doc/sketch.md) — read before editing; update when API, invariants, module layout, or workflows change | | Shape module (`src/shp*`, shape ops in `occt_view` / `gui`) | [src/doc/shape.md](../../src/doc/shape.md) — read before editing; update when API, operation patterns, or registration/undo change | -| GUI / viewer shell (`src/gui*`, input routing, settings panes) | [src/doc/gui.md](../../src/doc/gui.md) — read before editing; update when modes, Options, hotkeys, or settings keys change | +| GUI / viewer shell (`src/gui*`, input routing, settings panes) | [src/doc/gui.md](../../src/doc/gui.md) — read before editing; update when modes, Options, hotkeys, or settings keys change; sketch tool Options: tool-specific block **above** **Sketch options** | | Script consoles (`src/scr*`, bindings) | [src/doc/script.md](../../src/doc/script.md) — read before editing; update when `ezy`/`view` API or console UI changes | | Utilities (`src/utl*`, results, I/O, geometry) | [src/doc/utility.md](../../src/doc/utility.md) — read before editing; update when shared helper contracts change | | Docs build | [workflows/docs-build.md](../workflows/docs-build.md) | diff --git a/docs/usage-settings.md b/docs/usage-settings.md index 8c548dd2..d528db04 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -118,6 +118,7 @@ Sketch-related preferences are edited in the **Options** panel while you use a s - **Extrude sketch face**: under **Extrude**, **Both sides**, **Twist**, and **Material** for the new solid (same document preset as **Normal** mode Options **Material**). With **Twist** on, the first drag sets height, then a second drag sets twist angle about the face center; Tab / Shift+Tab enter height / angle. Fast drag preview for dense faces is configured in **Settings -> Sketch -> Appearance** (**Extrude fast preview**); with **Twist**, face copies rotate as well as translate. 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. - **Add line edge**: under **Options**, **Add midpoint nodes** ([usage-sketch.md#line-edge-option-add-midpoint-nodes](usage-sketch.md#line-edge-option-add-midpoint-nodes)) and **Place from center** ([usage-sketch.md#line-edge-option-place-from-center](usage-sketch.md#line-edge-option-place-from-center)); each has a **?** link. See also [Line edge Options](usage-sketch.md#line-edge-options). The midpoint setting is also in **Settings -> Sketch -> Nodes** (persisted). +- **Add bone**: under **Options**, **Add center nodes** (default on; permanent **Bone A** / **Bone B**) and **Holes** (**None**, **One radius**, **Two radii**). Persisted as **`gui.bone_add_center_nodes`** and **`gui.bone_holes`**. See [Bone Creation Tool](usage-sketch.md#bone-creation-tool). - **Sketch operation** (mirror / revolve axis): mirror, revolve, angle, and clear-axis actions (see [usage-sketch.md](usage-sketch.md#operation-axis-tool)). Global length-dimension style (line width, arrows, color, text) is in **Settings -> Sketch -> Dimensions**. Per-dimension visibility, name, and offset remain in **Sketch List -> Dimensions** (saved in the project `.ezy` file). @@ -166,14 +167,14 @@ A **?** at the top of the section (when [UI verbosity](#settings-pane) is high e **Reserved (cannot remap)** -| Keys | Role | -| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| Esc, Enter, Tab / Shift+Tab | Cancel, confirm, distance / angle input | -| Delete, Backspace | Always delete selection (fixed aliases) | -| Selection filter digits (19) | Normal-mode shape filter | -| View zoom / orbit / roll | See [usage.md -> View navigation](usage.md#view-navigation) | -| Unmodified X / Y / Z | Move axis constraints; Rotate axis pick | -| Ctrl+Shift+Z | Fixed redo alias (alongside remappable Ctrl+Y) | +| Keys | Role | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| Esc, Enter, Tab / Shift+Tab | Cancel, confirm, distance / angle input | +| Delete, Backspace | Always delete selection (fixed aliases) | +| Selection filter digits (19) | Normal-mode shape filter | +| View zoom / orbit / roll | See [usage.md -> View navigation](usage.md#view-navigation) | +| Unmodified X / Y / Z | Move axis constraints; Rotate axis pick | +| Ctrl+Shift+Z | Fixed redo alias (alongside remappable Ctrl+Y) | Two actions cannot share the same chord. Reserved, unsupported, and duplicate chords show an inline message under the table and a status toast with the reason. @@ -211,62 +212,64 @@ If saved layout text has no `[Docking]` section (older installs), a default dock ### `gui` -| Key | Type | Meaning | -| ------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dark_mode` | boolean | Light/dark theme. | -| `show_options` | boolean | Options panel visible. | -| `show_sketch_list` | boolean | Sketch List pane visible. | -| `show_shape_list` | boolean | Shape List pane visible. | -| `log_window_visible` | boolean | Log window visible. | -| `show_settings_dialog` | boolean | Whether the Settings pane was open when last saved (usually false). | -| `show_lua_console` | boolean | Lua console pane visible (default **false**). | -| `show_python_console` | boolean | Python console pane visible (native builds with Python; default **false**). | -| `show_dbg` | boolean | Debug pane visible (debug builds only). | -| `inspection_orthographic` | boolean | Non-sketch modes Options: orthographic camera when true (default false). Sketch modes always use orthographic. | -| `edge_dim_label_h` | integer | Length dimension label placement: **0** near first point, **1** near second, **2** center, **3** automatic. | -| `edge_dim_line_width` | number | Sketch length dimension line width (**0.5** to **8.0**; desktop only; ineffective in WebAssembly). | -| `edge_dim_arrow_size` | number | Arrow head length (**1.0** to **24.0**). | -| `edge_dim_color` | array of 3 numbers | Dimension line, arrow, and text RGB (**0** to **1** per channel; default olive **0.54**, **0.54**, **0.21**). | -| `edge_dim_text_scale` | number | Label height multiplier (**0.5** to **3.0**; default **1.0**). | -| `edge_dim_text_render_mode` | integer | **0** opaque 2D, **1** SetCommonColor, **2** 2D screen, **3** 3D text, **4** Z Top, **5** Z Topmost (default). | -| `edge_dim_arrow_style` | integer | **0** standard, **1** sharp, **2** wide, **3** 3D shaded. | -| `edge_dim_arrow_orientation` | integer | **0** automatic, **1** internal, **2** external. | -| `show_sketch_dimensions` | boolean | When false, hides length dimensions on all sketches. | -| `permanent_node_anno_scale` | number | Scale for permanent **+** markers: the sketch **Origin** and user-placed Add node points ([Sketch origin](usage-sketch.md#sketch-origin); **0.25** to **3.0**; default **1.0**). | -| `origin_marker_color` | array of 3 numbers | RGB color for the **active** sketch's Origin marker (+ with circle; **0** to **1** per channel; default cyan **0.0**, **0.75**, **1.0**). | -| `sketch_edge_color` | array of 4 numbers | Current-sketch edge RGBA (**0** to **1**; alpha = opacity; default green **0**, **1**, **0**, **1**). | -| `sketch_edge_selection_color` | array of 4 numbers | Selected sketch-edge RGBA (default orange **1**, **0.545**, **0**, **1**). Falls back to `sketch_edge_highlight_color` when absent. | -| `sketch_edge_highlight_color` | array of 4 numbers | Mouse-over (dynamic) highlight RGBA for sketch edges (default yellow **1**, **1**, **0**, **1**). | -| `sketch_edge_line_width` | number | Current-sketch edge line width (**0.5** to **8.0**; default **1.0**; desktop only; ineffective in WebAssembly). | -| `sketch_face_color` | array of 4 numbers | Current-sketch face fill RGBA (default mauve **0.301**, **0.245**, **0.321**, **0.297**). | -| `sketch_face_selection_color` | array of 4 numbers | Selected sketch-face **fill** RGBA (default magenta **0.799**, **0.187**, **0.591**, **1**). Falls back to `sketch_face_highlight_color` when absent. | -| `sketch_face_highlight_color` | array of 4 numbers | Mouse-over (dynamic) sketch-face **fill** RGBA (default violet **0.823**, **0**, **1**, **1**). | -| `snap_guide_color_node` | array of 3 numbers | RGB for snap guides when both axes lock to the same node (float **0** to **1**; default lavender **0.82**, **0.55**, **0.95**). Legacy `snap_guide_color` loads here when `snap_guide_color_node` is absent. | -| `snap_guide_color_axis` | array of 3 numbers | RGB for snap guides when aligned on X or Y only (float **0** to **1**; default magenta **0.96**, **0.06**, **0.54**). Legacy `snap_guide_color` sets both node and axis colors. | -| `snap_guide_mode` | integer | **0** *Traditional* (local markers), **1** *Fullscreen* (view-spanning axis lines), **2** *Both* (default **2**), **3** *None* (disable snap-to-node and snap guides). | -| `snap_guide_line_width` | number | Open CASCADE line width for snap guides (axis lines, markers, co-axial overlay; **0.5** to **8.0**; default **1.0**; desktop only; ineffective in WebAssembly). | -| `annotate_all_coaxial_nodes` | boolean | When true (default), show axis guides and markers for *all* co-axial nodes (current sketch plus other visible sketches). When false, only the closest node per active axis is annotated. Also in sketch **Options**. | -| `imgui_style_dark` | object | ImGui layout for **dark mode** (see keys below). | -| `imgui_style_light` | object | ImGui layout for **light mode** (same keys). | -| `settings_headers` | object | Which Settings pane collapsing sections are expanded (booleans; see keys below). | -| `imgui_rounding_general` | number | **Legacy:** copied into both theme objects on load when `imgui_style_*` is absent. | -| `imgui_rounding_scroll` | number | **Legacy:** same. | -| `imgui_rounding_tabs` | number | **Legacy:** same. | -| `underlay_highlight_color` | array of 3 numbers | Default underlay tint (float RGB **0** to **1** per channel; default **0.64**, **0.56**, **0.31**). | -| `elm_list_hover_color` | array of 4 numbers | RGBA highlight for rows hovered in the **Shape List** or **Sketch List** dimensions table (float **0** to **1** per channel; default purple **0.40**, **0.10**, **0.47**, **1**). | -| `shape_selection_color` | array of 4 numbers | Selected 3D shape highlight RGBA (AIS SelectionStyle; float **0** to **1**; alpha = opacity; default purple **0.75**, **0.07**, **0.85**, **1**). | -| `sketch_shape_faint_style` | integer | How 3D shapes appear in sketch mode when enabled: **0** Off (hide), **1** Ghost (default), **2** Wire. See [usage-sketch.md](usage-sketch.md#sketching-2d). | -| `sketch_shape_faint_opacity` | number | Shape Faint Strength as opacity (**0.05** to **0.85**; default **0.14**; Settings shows **5%**–**85%**). Ghost and Wire. OCCT transparency is **1 - opacity**. | -| `sketch_shape_faint_enabled` | boolean | Master switch for faint solids in all sketch tools (default **true**). Also **Options -> Sketch options -> Faint shapes**. When false, solids are hidden in sketch modes. | -| `extrude_fast_preview` | boolean | When **true** (default), extrude uses a face-copy drag preview for faces with more edges than `extrude_fast_preview_edge_threshold`. Settings -> Sketch -> Appearance. See [Extrude](usage.md#extrude-sketch-face-tool-e). | -| `extrude_fast_preview_edge_threshold` | integer | Edge-count threshold for extrude fast preview (**4** to **256**; default **24**). | -| `view_roll_step_deg` | number | Degrees per **NumPad 8**/**2**/**4**/**6** orbit and **Shift+NumPad 4**/**6** roll (allowed range **0.1** to **180** in code; default **45**). | -| `view_zoom_scroll_scale` | number | Multiplier for `UpdateZoom` scroll delta from wheel and keyboard zoom (allowed range **0.25** to **64** in code; default **4**). With **Shift** held, the effective step is multiplied by **0.1** (Blender-style finer zoom). | -| `default_project_unit` | string | Default **File -> New** project unit: `"inch"` or `"millimeter"` (default **`inch`**). Edited under **Settings -> New project defaults**. | -| `default_2d_view_width` | number | Horizontal sketch-plane span for **File -> New** / projects with no saved camera, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | -| `default_2d_view_height` | number | Vertical sketch-plane span, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | -| `load_last_opened_on_startup` | boolean | Desktop: open the last `.ezy` on launch. **Legacy:** `load_last_saved_on_startup` is read as a fallback if the newer key is absent. | -| `last_opened_project_path` | string | Path of the last opened project for the option above. **Legacy:** `last_saved_project_path` is accepted if the newer key is missing. | +| Key | Type | Meaning | +| ------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dark_mode` | boolean | Light/dark theme. | +| `show_options` | boolean | Options panel visible. | +| `show_sketch_list` | boolean | Sketch List pane visible. | +| `show_shape_list` | boolean | Shape List pane visible. | +| `log_window_visible` | boolean | Log window visible. | +| `show_settings_dialog` | boolean | Whether the Settings pane was open when last saved (usually false). | +| `show_lua_console` | boolean | Lua console pane visible (default **false**). | +| `show_python_console` | boolean | Python console pane visible (native builds with Python; default **false**). | +| `show_dbg` | boolean | Debug pane visible (debug builds only). | +| `inspection_orthographic` | boolean | Non-sketch modes Options: orthographic camera when true (default false). Sketch modes always use orthographic. | +| `edge_dim_label_h` | integer | Length dimension label placement: **0** near first point, **1** near second, **2** center, **3** automatic. | +| `edge_dim_line_width` | number | Sketch length dimension line width (**0.5** to **8.0**; desktop only; ineffective in WebAssembly). | +| `edge_dim_arrow_size` | number | Arrow head length (**1.0** to **24.0**). | +| `edge_dim_color` | array of 3 numbers | Dimension line, arrow, and text RGB (**0** to **1** per channel; default olive **0.54**, **0.54**, **0.21**). | +| `edge_dim_text_scale` | number | Label height multiplier (**0.5** to **3.0**; default **1.0**). | +| `edge_dim_text_render_mode` | integer | **0** opaque 2D, **1** SetCommonColor, **2** 2D screen, **3** 3D text, **4** Z Top, **5** Z Topmost (default). | +| `edge_dim_arrow_style` | integer | **0** standard, **1** sharp, **2** wide, **3** 3D shaded. | +| `edge_dim_arrow_orientation` | integer | **0** automatic, **1** internal, **2** external. | +| `show_sketch_dimensions` | boolean | When false, hides length dimensions on all sketches. | +| `permanent_node_anno_scale` | number | Scale for permanent **+** markers: the sketch **Origin** and user-placed Add node points ([Sketch origin](usage-sketch.md#sketch-origin); **0.25** to **3.0**; default **1.0**). | +| `bone_add_center_nodes` | boolean | Add-bone Options: commit permanent **Bone A** / **Bone B** (default **true**). | +| `bone_holes` | integer | Add-bone Options holes mode: **0** None, **1** One radius, **2** Two radii (default **0**). | +| `origin_marker_color` | array of 3 numbers | RGB color for the **active** sketch's Origin marker (+ with circle; **0** to **1** per channel; default cyan **0.0**, **0.75**, **1.0**). | +| `sketch_edge_color` | array of 4 numbers | Current-sketch edge RGBA (**0** to **1**; alpha = opacity; default green **0**, **1**, **0**, **1**). | +| `sketch_edge_selection_color` | array of 4 numbers | Selected sketch-edge RGBA (default orange **1**, **0.545**, **0**, **1**). Falls back to `sketch_edge_highlight_color` when absent. | +| `sketch_edge_highlight_color` | array of 4 numbers | Mouse-over (dynamic) highlight RGBA for sketch edges (default yellow **1**, **1**, **0**, **1**). | +| `sketch_edge_line_width` | number | Current-sketch edge line width (**0.5** to **8.0**; default **1.0**; desktop only; ineffective in WebAssembly). | +| `sketch_face_color` | array of 4 numbers | Current-sketch face fill RGBA (default mauve **0.301**, **0.245**, **0.321**, **0.297**). | +| `sketch_face_selection_color` | array of 4 numbers | Selected sketch-face **fill** RGBA (default magenta **0.799**, **0.187**, **0.591**, **1**). Falls back to `sketch_face_highlight_color` when absent. | +| `sketch_face_highlight_color` | array of 4 numbers | Mouse-over (dynamic) sketch-face **fill** RGBA (default violet **0.823**, **0**, **1**, **1**). | +| `snap_guide_color_node` | array of 3 numbers | RGB for snap guides when both axes lock to the same node (float **0** to **1**; default lavender **0.82**, **0.55**, **0.95**). Legacy `snap_guide_color` loads here when `snap_guide_color_node` is absent. | +| `snap_guide_color_axis` | array of 3 numbers | RGB for snap guides when aligned on X or Y only (float **0** to **1**; default magenta **0.96**, **0.06**, **0.54**). Legacy `snap_guide_color` sets both node and axis colors. | +| `snap_guide_mode` | integer | **0** *Traditional* (local markers), **1** *Fullscreen* (view-spanning axis lines), **2** *Both* (default **2**), **3** *None* (disable snap-to-node and snap guides). | +| `snap_guide_line_width` | number | Open CASCADE line width for snap guides (axis lines, markers, co-axial overlay; **0.5** to **8.0**; default **1.0**; desktop only; ineffective in WebAssembly). | +| `annotate_all_coaxial_nodes` | boolean | When true (default), show axis guides and markers for *all* co-axial nodes (current sketch plus other visible sketches). When false, only the closest node per active axis is annotated. Also in sketch **Options**. | +| `imgui_style_dark` | object | ImGui layout for **dark mode** (see keys below). | +| `imgui_style_light` | object | ImGui layout for **light mode** (same keys). | +| `settings_headers` | object | Which Settings pane collapsing sections are expanded (booleans; see keys below). | +| `imgui_rounding_general` | number | **Legacy:** copied into both theme objects on load when `imgui_style_*` is absent. | +| `imgui_rounding_scroll` | number | **Legacy:** same. | +| `imgui_rounding_tabs` | number | **Legacy:** same. | +| `underlay_highlight_color` | array of 3 numbers | Default underlay tint (float RGB **0** to **1** per channel; default **0.64**, **0.56**, **0.31**). | +| `elm_list_hover_color` | array of 4 numbers | RGBA highlight for rows hovered in the **Shape List** or **Sketch List** dimensions table (float **0** to **1** per channel; default purple **0.40**, **0.10**, **0.47**, **1**). | +| `shape_selection_color` | array of 4 numbers | Selected 3D shape highlight RGBA (AIS SelectionStyle; float **0** to **1**; alpha = opacity; default purple **0.75**, **0.07**, **0.85**, **1**). | +| `sketch_shape_faint_style` | integer | How 3D shapes appear in sketch mode when enabled: **0** Off (hide), **1** Ghost (default), **2** Wire. See [usage-sketch.md](usage-sketch.md#sketching-2d). | +| `sketch_shape_faint_opacity` | number | Shape Faint Strength as opacity (**0.05** to **0.85**; default **0.14**; Settings shows **5%**–**85%**). Ghost and Wire. OCCT transparency is **1 - opacity**. | +| `sketch_shape_faint_enabled` | boolean | Master switch for faint solids in all sketch tools (default **true**). Also **Options -> Sketch options -> Faint shapes**. When false, solids are hidden in sketch modes. | +| `extrude_fast_preview` | boolean | When **true** (default), extrude uses a face-copy drag preview for faces with more edges than `extrude_fast_preview_edge_threshold`. Settings -> Sketch -> Appearance. See [Extrude](usage.md#extrude-sketch-face-tool-e). | +| `extrude_fast_preview_edge_threshold` | integer | Edge-count threshold for extrude fast preview (**4** to **256**; default **24**). | +| `view_roll_step_deg` | number | Degrees per **NumPad 8**/**2**/**4**/**6** orbit and **Shift+NumPad 4**/**6** roll (allowed range **0.1** to **180** in code; default **45**). | +| `view_zoom_scroll_scale` | number | Multiplier for `UpdateZoom` scroll delta from wheel and keyboard zoom (allowed range **0.25** to **64** in code; default **4**). With **Shift** held, the effective step is multiplied by **0.1** (Blender-style finer zoom). | +| `default_project_unit` | string | Default **File -> New** project unit: `"inch"` or `"millimeter"` (default **`inch`**). Edited under **Settings -> New project defaults**. | +| `default_2d_view_width` | number | Horizontal sketch-plane span for **File -> New** / projects with no saved camera, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | +| `default_2d_view_height` | number | Vertical sketch-plane span, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | +| `load_last_opened_on_startup` | boolean | Desktop: open the last `.ezy` on launch. **Legacy:** `load_last_saved_on_startup` is read as a fallback if the newer key is absent. | +| `last_opened_project_path` | string | Path of the last opened project for the option above. **Legacy:** `last_saved_project_path` is accepted if the newer key is missing. | | `hotkeys` | object | Remappable keyboard shortcuts: action id string keys to human-readable chord strings (for example `"mode.move": "G"`, `"mode.add_edge": "L"`, `"cmd.shape_cut": "Ctrl+Shift+C"`, `"edit.delete": "Shift+D"`, `"edit.copy": "Ctrl+C"`, `"edit.paste": "Ctrl+V"`). Missing keys merge to built-in defaults. See [Keyboard shortcuts](#keyboard-shortcuts) and [usage.md#hotkeys](usage.md#hotkeys). | Each **`imgui_style_dark`** / **`imgui_style_light`** object may contain: diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 33d642b1..4f590874 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -722,7 +722,15 @@ The bone tool builds a connecting-rod outline: the outer arcs of the two end cir 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. -6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). The live dimension spans that neck; when the end radii differ, the neck is offset toward the smaller end, not at the center midpoint. Tab enters an exact waist. The tool commits after this click. +6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). The live dimension spans that neck; 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. + +**Options** (while Add bone is active): + +| Option | Default | Meaning | +| -------------------- | ------- | ------------------------------------------------------------------------------------------- | +| **Add center nodes** | On | Commit permanent **Bone A** / **Bone B** nodes at the two centers | +| **Holes** | None | **None** (outline only), **One radius** (both holes match), or **Two radii** (set each end) | Waist cutters are tangent to both end circles (cut radius is solved from the waist). Geometry fails when the two centers coincide, one circle sits inside the other, or the waist cannot be realized. @@ -731,8 +739,9 @@ The sketch keeps only that trimmed outline (four arcs). Extrude it with the norm **Tips:** - Snap the two centers to existing nodes, or to the sketch origin. -- After Add, use **Add circle** snapped to **Bone A** / **Bone B** if you want a different hole radius than the end circles. +- After Add with no holes, use **Add circle** snapped to **Bone A** / **Bone B** if you want holes later. - For waist, click on either side of the bone axis; the distance from the axis to that point is half the waist (the dim then snaps to the true neck). +- Preferences **Add center nodes** and **Holes** are saved as `gui.bone_add_center_nodes` and `gui.bone_holes`. ## Operation Axis Tool diff --git a/res/ezycad_settings.json b/res/ezycad_settings.json index 16c5c880..804b799c 100644 --- a/res/ezycad_settings.json +++ b/res/ezycad_settings.json @@ -3,6 +3,8 @@ "add_mid_pt_edges": false, "add_mid_pt_rect_edges": true, "add_mid_pt_slot_edges": true, + "bone_add_center_nodes": true, + "bone_holes": 0, "annotate_all_coaxial_nodes": true, "dark_mode": true, "edge_dim_arrow_orientation": 0, diff --git a/src/doc/gui.md b/src/doc/gui.md index 4336ba61..1804dafc 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -224,7 +224,18 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio | `Sketch_operation_axis` | Mirror / Revolve / Clear axis | | `Sketch_face_extrude` | Both sides, Twist, material; help mentions Settings fast preview | -Shared sketch controls (snap, midpoint nodes, place-from-center) live in `options_sketch_common_` and helpers in `gui_mode.cpp`. +### Options panel layout (sketch tools) + +Vertical order for sketch tool Options panes: + +1. Mode title + doc **?** (`options_sketch_mode_header_`) +2. Tool-specific **Options** (checkboxes, combos, tool help) — **above** shared sketch controls +3. **Sketch options** (`options_sketch_shared_controls_`: snap guide mode, snap dist, faint shapes, …) +4. Optional **Shortcuts** / other footers (`options_sketch_len_angle_hotkeys_`, …) + +`options_sketch_common_()` is header + shared controls only (no tool-specific block). Prefer composing header → tool Options → shared when a tool has its own controls (see `options_sketch_add_bone_mode_`). Do not put tool-specific controls below **Sketch options**. + +Shared sketch controls (snap, faint shapes) live in `options_sketch_shared_controls_`. Midpoint / place-from-center helpers still append after `options_sketch_common_` in older tools — new work should use the order above. ## ImGui frame order (`render_gui`) diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 3ccbeb6d..27c912ff 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -204,7 +204,7 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::list(m_bone_holes); + if (ImGui::Combo("##bone_holes", &holes, c_bone_holes_strs.data(), static_cast(Bone_holes::_count))) + { + if (holes >= 0 && holes < static_cast(Bone_holes::_count)) + { + m_bone_holes = static_cast(holes); + 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.", + doc_urls::k_bone_creation_tool); + ImGui::TextWrapped( - "Click center A, center B, radius 1, radius 2, then the waist (minimum neck width)."); + "Click center A, center B, radius 1, radius 2, waist, then holes if enabled."); + + options_sketch_shared_controls_(); + options_sketch_len_angle_hotkeys_(); } void GUI::options_orthographic_projection_() @@ -1286,12 +1319,20 @@ void GUI::options_orthographic_projection_() } void GUI::options_sketch_common_() +{ + options_sketch_mode_header_(); + options_sketch_shared_controls_(); +} + +void GUI::options_sketch_mode_header_() { ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); - ImGui::Separator(); +} +void GUI::options_sketch_shared_controls_() +{ ImGui::TextUnformatted("Sketch options"); if (ImGui::BeginTable("options_sketch_sketch", 2, k_options_table_flags)) { diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index aabbb966..fba7aef2 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -64,6 +64,8 @@ std::string GUI::occt_view_settings_json() const {"add_mid_pt_edges", m_add_mid_pt_line_edges}, {"add_mid_pt_rect_edges", m_add_mid_pt_rect_edges}, {"add_mid_pt_slot_edges", m_add_mid_pt_slot_edges}, + {"bone_add_center_nodes", m_bone_add_center_nodes}, + {"bone_holes", static_cast(m_bone_holes)}, {"view_roll_step_deg", m_view_roll_step_deg}, {"view_zoom_scroll_scale", m_view_zoom_scroll_scale}, {"default_2d_view_width", m_default_2d_view_width}, @@ -156,6 +158,8 @@ void GUI::save_occt_view_settings() {"add_mid_pt_edges", m_add_mid_pt_line_edges}, {"add_mid_pt_rect_edges", m_add_mid_pt_rect_edges}, {"add_mid_pt_slot_edges", m_add_mid_pt_slot_edges}, + {"bone_add_center_nodes", m_bone_add_center_nodes}, + {"bone_holes", static_cast(m_bone_holes)}, {"load_last_opened_on_startup", m_load_last_opened_on_startup}, {"last_opened_project_path", m_last_opened_project_path}, {"imgui_style_dark", imgui_style_to_json_(m_imgui_style_dark)}, @@ -462,6 +466,14 @@ void GUI::parse_gui_panes_settings_(const std::string& content) m_add_mid_pt_rect_edges = b("add_mid_pt_rect_edges", true); m_add_mid_pt_slot_edges = b("add_mid_pt_slot_edges", false); + m_bone_add_center_nodes = b("bone_add_center_nodes", true); + m_bone_holes = Bone_holes::None; + if (g.contains("bone_holes") && g["bone_holes"].is_number_integer()) + { + const int holes = g["bone_holes"].get(); + if (holes >= 0 && holes < static_cast(Bone_holes::_count)) + m_bone_holes = static_cast(holes); + } m_load_last_opened_on_startup = b("load_last_opened_on_startup", b("load_last_saved_on_startup", false)); if (g.contains("last_opened_project_path") && g["last_opened_project_path"].is_string()) diff --git a/src/mode.h b/src/mode.h index 4f416dcd..0c4fba63 100644 --- a/src/mode.h +++ b/src/mode.h @@ -93,6 +93,23 @@ static_assert(c_fillet_mode_strs.size() == static_cast(Fillet_mode: #undef EZY_CHAMFER_FILLET_MODE_LIST +/// Add-bone Options: optional end holes after the waist click. +enum class Bone_holes +{ + None, // outline only + One_radius, // one click sets both hole radii + Two_radii, // hole at end A, then hole at end B + _count +}; + +constexpr std::array(Bone_holes::_count)> c_bone_holes_strs = { + "None", + "One radius", + "Two radii", +}; + +static_assert(c_bone_holes_strs.size() == static_cast(Bone_holes::_count)); + bool is_sketch_mode(const Mode mode); /// Return Mode for a name (e.g. "Normal", "Sketch_add_edge"). Returns Normal if not found. diff --git a/src/skt.cpp b/src/skt.cpp index bd07cd75..794fd65c 100644 --- a/src/skt.cpp +++ b/src/skt.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -151,7 +152,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::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist) +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) { Bone_params params; params.c1 = c1; @@ -164,6 +166,15 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double if (!g) return; + auto hole_ok = [](double outer_r, const std::optional& hole) -> bool + { + if (!hole) + return true; + return *hole > Precision::Confusion() && *hole + Precision::Confusion() < outer_r; + }; + if (!hole_ok(r1, hole_r1) || !hole_ok(r2, hole_r2)) + return; + Sketch_op_recorder rec(m_view, *this); { auto mark_center = [&](const gp_Pnt2d& c, const char* name) @@ -175,13 +186,28 @@ void Sketch::add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double rec.note_curr_node(idx); }; + auto add_hole_circle = [&](const gp_Pnt2d& center, double radius) + { + const gp_Pnt2d rim(center.X() + radius, center.Y()); + const std::array pts = xy_stencil_pnts(center, rim); + add_arc_circle_(pts[0], pts[2], pts[1], rec); + add_arc_circle_(pts[0], pts[3], pts[1], rec); + }; + const Bone_profile p = get_bone_profile(*g); - mark_center(g->c1, "Bone A"); - mark_center(g->c2, "Bone B"); + if (add_center_nodes) + { + mark_center(g->c1, "Bone A"); + mark_center(g->c2, "Bone B"); + } add_arc_circle_(p.c1_minus, p.c1_outer, p.c1_plus, rec); add_arc_circle_(p.c1_plus, p.waist_plus, p.c2_plus, rec); add_arc_circle_(p.c2_plus, p.c2_outer, p.c2_minus, rec); 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(); } diff --git a/src/skt.h b/src/skt.h index d320b215..98feff52 100644 --- a/src/skt.h +++ b/src/skt.h @@ -167,9 +167,13 @@ class Sketch void add_arc_circle(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_mid, const gp_Pnt2d& pt_c); /// Rebuild closed-face topology after bulk edge import. void rebuild_faces(); - /// Add a bone outline (outer end arcs and inner waist arcs) and permanent center nodes. + /// Add a bone outline (outer end arcs and inner waist arcs). /// Waist cutters are tangent to both end circles; cut radius is solved from \\a waist. - void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist); + /// Optional permanent **Bone A** / **Bone B** centers; optional hole circles (radii must be + /// positive and smaller than the matching end radii). + void add_bone(const gp_Pnt2d& c1, const gp_Pnt2d& c2, double r1, double r2, double waist, + bool add_center_nodes = true, std::optional hole_r1 = std::nullopt, + std::optional hole_r2 = std::nullopt); private: friend class Sketch_json; diff --git a/src/skt_tools.cpp b/src/skt_tools.cpp index cb45bfa4..b2340b3b 100644 --- a/src/skt_tools.cpp +++ b/src/skt_tools.cpp @@ -927,6 +927,9 @@ bool Sketch_tools::clear_tmps() m_bone_centers.reset(); m_bone_r1.reset(); m_bone_r2.reset(); + m_bone_waist.reset(); + m_bone_hole_r1.reset(); + m_bone_hole_r2.reset(); clear_all(m_tmp_node_idxs, m_tmp_shp, m_tmp_edges); m_sketch.m_dims.on_clear_tmps(); diff --git a/src/skt_tools.h b/src/skt_tools.h index 7d9ca73e..f95874ed 100644 --- a/src/skt_tools.h +++ b/src/skt_tools.h @@ -79,7 +79,10 @@ class Sketch_tools void finalize_bone_(); void bone_begin_next_edge_from_(const gp_Pnt2d& origin); void bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2); - [[nodiscard]] bool bone_try_commit_(double waist); + [[nodiscard]] bool bone_after_waist_(double waist); + [[nodiscard]] bool bone_after_hole_a_(double hole_r); + [[nodiscard]] bool bone_after_hole_b_(double hole_r); + [[nodiscard]] bool bone_try_commit_(); void bone_update_preview_(); [[nodiscard]] std::optional bone_axis_perp_() const; [[nodiscard]] std::optional bone_waist_from_pt_(const gp_Pnt2d& pt) const; @@ -103,4 +106,7 @@ class Sketch_tools std::optional> m_bone_centers; std::optional m_bone_r1; std::optional m_bone_r2; + std::optional m_bone_waist; + std::optional m_bone_hole_r1; + std::optional m_bone_hole_r2; }; diff --git a/src/skt_tools_bone.cpp b/src/skt_tools_bone.cpp index e47d6c88..5d546da1 100644 --- a/src/skt_tools_bone.cpp +++ b/src/skt_tools_bone.cpp @@ -12,6 +12,7 @@ #include "gui.h" #include "gui_occt_view.h" +#include "mode.h" #include "skt.h" #include "utl_geom.h" #include "utl_occt.h" @@ -19,6 +20,14 @@ #include "skt_tools.inl" +namespace +{ +bool bone_hole_radius_ok_(double outer_r, double hole_r) +{ + return hole_r > Precision::Confusion() && hole_r + Precision::Confusion() < outer_r; +} +} // namespace + void Sketch_tools::bone_on_enter_() { if (!m_tmp_edges.empty() && !m_bone_centers && m_sketch.m_dims.entered_edge_len().has_value()) @@ -64,7 +73,11 @@ void Sketch_tools::bone_on_enter_() bone_begin_next_edge_from_(get_midpoint(m_bone_centers->first, m_bone_centers->second)); } else if (m_tmp_edges.size() == 4) - (void)bone_try_commit_(len); + (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); } void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) @@ -140,8 +153,12 @@ void Sketch_tools::add_bone_pt_(const ScreenCoords& screen_coords) { const std::optional waist = bone_waist_from_pt_(pt_b); if (waist) - (void)bone_try_commit_(*waist); + (void)bone_after_waist_(*waist); } + 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); }; add_sketch_pt_(screen_coords, 1, on_dim); @@ -162,6 +179,13 @@ void Sketch_tools::move_bone_pt_(const ScreenCoords& screen_coords) return; } + if (m_tmp_edges.size() >= 5) + { + move_line_string_pt_(screen_coords); + bone_update_preview_(); + return; + } + const std::optional n = bone_axis_perp_(); if (!n || !m_bone_r1 || !m_bone_r2) return; @@ -235,7 +259,7 @@ void Sketch_tools::bone_on_centers_ready_(const gp_Pnt2d& c1, const gp_Pnt2d& c2 bone_begin_next_edge_from_(c1); } -bool Sketch_tools::bone_try_commit_(double waist) +bool Sketch_tools::bone_after_waist_(double waist) { if (!m_bone_centers || !m_bone_r1 || !m_bone_r2) return false; @@ -250,19 +274,100 @@ bool Sketch_tools::bone_try_commit_(double waist) if (!compute_bone_geom(params)) return false; - m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, waist); + m_bone_waist = waist; + const Bone_holes holes = m_sketch.m_view.gui().get_bone_holes(); + if (holes == Bone_holes::None) + return bone_try_commit_(); + + bone_begin_next_edge_from_(m_bone_centers->first); + return true; +} + +bool Sketch_tools::bone_after_hole_a_(double hole_r) +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || !m_bone_waist) + return false; + + const Bone_holes holes = m_sketch.m_view.gui().get_bone_holes(); + if (holes == Bone_holes::One_radius) + { + if (!bone_hole_radius_ok_(*m_bone_r1, hole_r) || !bone_hole_radius_ok_(*m_bone_r2, hole_r)) + return false; + m_bone_hole_r1 = hole_r; + m_bone_hole_r2 = hole_r; + return bone_try_commit_(); + } + + if (holes != Bone_holes::Two_radii) + return false; + if (!bone_hole_radius_ok_(*m_bone_r1, hole_r)) + return false; + + m_bone_hole_r1 = hole_r; + bone_begin_next_edge_from_(m_bone_centers->second); + return true; +} + +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)) + return false; + + m_bone_hole_r2 = hole_r; + return bone_try_commit_(); +} + +bool Sketch_tools::bone_try_commit_() +{ + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || !m_bone_waist) + return false; + + Bone_params params; + params.c1 = m_bone_centers->first; + params.c2 = m_bone_centers->second; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = *m_bone_waist; + params.drive = Bone_drive::Waist; + if (!compute_bone_geom(params)) + return false; + + const bool add_centers = m_sketch.m_view.gui().get_bone_add_center_nodes(); + m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, *m_bone_waist, add_centers, m_bone_hole_r1, + m_bone_hole_r2); clear_tmps(); return true; } void Sketch_tools::finalize_bone_() { - if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || m_tmp_edges.size() != 4 || !m_last_pt) + if (!m_bone_centers || !m_bone_r1 || !m_bone_r2 || !m_last_pt) + return; + + if (m_tmp_edges.size() == 4) + { + 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); + return; + } + + if (m_tmp_edges.size() == 5 && m_bone_waist) + { + const gp_Pnt2d& c1 = m_bone_centers->first; + (void)bone_after_hole_a_(c1.Distance(*m_last_pt)); return; + } - const std::optional waist = bone_waist_from_pt_(*m_last_pt); - if (waist) - (void)bone_try_commit_(*waist); + if (m_tmp_edges.size() == 6 && m_bone_waist && m_bone_hole_r1) + { + const gp_Pnt2d& c2 = m_bone_centers->second; + (void)bone_after_hole_b_(c2.Distance(*m_last_pt)); + } } std::optional Sketch_tools::bone_axis_perp_() const @@ -308,11 +413,17 @@ void Sketch_tools::bone_update_preview_() const double r2 = m_bone_r2.value_or( (m_tmp_edges.size() == 3 && 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); }; + if (m_tmp_edges.size() >= 4 && m_bone_r1 && m_bone_r2) { const std::optional waist = - m_sketch.m_dims.entered_edge_len().has_value() ? std::optional(m_sketch.m_dims.entered_edge_len()->len) - : (m_last_pt ? bone_waist_from_pt_(*m_last_pt) : std::nullopt); + m_bone_waist + ? m_bone_waist + : (m_sketch.m_dims.entered_edge_len().has_value() + ? std::optional(m_sketch.m_dims.entered_edge_len()->len) + : (m_last_pt ? bone_waist_from_pt_(*m_last_pt) : std::nullopt)); if (waist) { Bone_params params; @@ -324,7 +435,43 @@ void Sketch_tools::bone_update_preview_() params.drive = Bone_drive::Waist; if (const std::optional g = compute_bone_geom(params)) { - show(m_sketch.m_ctx, m_tmp_shp, make_bone_preview_shape(m_sketch.m_pln, *g)); + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bb.Add(comp, make_bone_preview_shape(m_sketch.m_pln, *g)); + + auto maybe_hole = [&](const gp_Pnt2d& c, double outer_r, const std::optional& fixed, + bool live_from_center) -> void + { + double hr = 0.0; + if (fixed) + hr = *fixed; + else if (live_from_center && m_last_pt) + hr = c.Distance(*m_last_pt); + if (bone_hole_radius_ok_(outer_r, hr)) + bb.Add(comp, circle_at(c, hr)); + }; + + if (m_tmp_edges.size() >= 5) + { + const Bone_holes holes = m_sketch.m_view.gui().get_bone_holes(); + if (holes == Bone_holes::One_radius) + { + const double hr = m_bone_hole_r1 ? *m_bone_hole_r1 + : (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)); + } + else if (holes == Bone_holes::Two_radii) + { + maybe_hole(c1, *m_bone_r1, m_bone_hole_r1, m_tmp_edges.size() == 5); + maybe_hole(c2, *m_bone_r2, m_bone_hole_r2, m_tmp_edges.size() == 6); + } + } + + show_compound(comp); return; } } @@ -356,5 +503,5 @@ void Sketch_tools::bone_update_preview_() return; } - show(m_sketch.m_ctx, m_tmp_shp, comp); + show_compound(comp); } diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index c0d9b74f..50d36ba1 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -1056,6 +1056,27 @@ TEST_F(Sketch_test, AddBone_createsFacesAndPermanentCenters) EXPECT_NE(std::find(labels.begin(), labels.end(), "Bone B"), labels.end()); } +TEST_F(Sketch_test, AddBone_optionalHolesAndNoCenterNodes) +{ + Headless_guard guard(view()); + + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("BoneHoles", view(), default_plane); + sketch.add_bone(gp_Pnt2d(-2.0, 0.0), gp_Pnt2d(2.0, 0.0), 1.0, 0.8, 0.4, false, 0.3, 0.25); + + // Outer face plus two hole face metas (holes assigned under the outer). + EXPECT_EQ(sketch.face_count(), 3u); + EXPECT_EQ(Sketch_access::get_edge_count(sketch), 8u); + + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (n.deleted) + continue; + EXPECT_FALSE(n.permanent && (n.name == "Bone A" || n.name == "Bone B")); + } +} + TEST_F(Sketch_test, AddBone_undoRemovesPromotedCenters) { Headless_guard guard(view()); From 9a6f6b65d7e8e0867b773a5d97ccb4754cb668cc Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 17:38:14 -0600 Subject: [PATCH 09/12] Improve --- agents/README.md | 36 ++++++++++++++++---------------- agents/conventions/token-lean.md | 32 ++++++++++++++-------------- src/doc/gui.md | 26 +++++++++++------------ src/gui_mode.cpp | 1 + 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/agents/README.md b/agents/README.md index e217ff70..692ca669 100644 --- a/agents/README.md +++ b/agents/README.md @@ -6,24 +6,24 @@ Root markers: [AGENTS.md](../AGENTS.md) / [agents.md](../agents.md). ## Quick index -| 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) | -| 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 | +| 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) | +| 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 | | GUI module (dev doc) | [src/doc/gui.md](../src/doc/gui.md) — read when editing `gui_*` / viewer shell; update if routing or settings change; **new Mode/Command → hotkeys checklist**; **sketch Options: tool Options above Sketch options** | -| Script module (dev doc) | [src/doc/script.md](../src/doc/script.md) — read when editing `scr_*`; update if bindings change | -| Utility module (dev doc) | [src/doc/utility.md](../src/doc/utility.md) — read when editing `utl_*`; update if shared helpers or I/O change | -| Build / test / wasm | [workflows/local-dev.md](workflows/local-dev.md) | -| OCCT desktop 8 vs wasm 7.9.3 | [conventions/occt-wasm-dual-version.md](conventions/occt-wasm-dual-version.md) — until wasm works on OCCT 8 | -| OCCT handles (`*_ptr`) | [conventions/occt-handles.md](conventions/occt-handles.md) — prefer aliases over `Handle()` for clang-format | -| Release | [workflows/release.md](workflows/release.md) | -| Issue/PR drafts | [drafts/](drafts/) — [github-drafts.md](conventions/github-drafts.md) | -| Feature plans (opt-in) | [plans/](plans/) — load **only** when the prompt matches that feature ([token-lean](conventions/token-lean.md)) | -| Token-saving rules | [conventions/token-lean.md](conventions/token-lean.md) | -| Markdown tables | [conventions/markdown-tables.md](conventions/markdown-tables.md) — align GFM pipes for source + preview | -| Outreach (optional) | [outreach/discoverability.md](outreach/discoverability.md) | +| Script module (dev doc) | [src/doc/script.md](../src/doc/script.md) — read when editing `scr_*`; update if bindings change | +| Utility module (dev doc) | [src/doc/utility.md](../src/doc/utility.md) — read when editing `utl_*`; update if shared helpers or I/O change | +| Build / test / wasm | [workflows/local-dev.md](workflows/local-dev.md) | +| OCCT desktop 8 vs wasm 7.9.3 | [conventions/occt-wasm-dual-version.md](conventions/occt-wasm-dual-version.md) — until wasm works on OCCT 8 | +| OCCT handles (`*_ptr`) | [conventions/occt-handles.md](conventions/occt-handles.md) — prefer aliases over `Handle()` for clang-format | +| Release | [workflows/release.md](workflows/release.md) | +| Issue/PR drafts | [drafts/](drafts/) — [github-drafts.md](conventions/github-drafts.md) | +| Feature plans (opt-in) | [plans/](plans/) — load **only** when the prompt matches that feature ([token-lean](conventions/token-lean.md)) | +| Token-saving rules | [conventions/token-lean.md](conventions/token-lean.md) | +| Markdown tables | [conventions/markdown-tables.md](conventions/markdown-tables.md) — align GFM pipes for source + preview | +| Outreach (optional) | [outreach/discoverability.md](outreach/discoverability.md) | Full user-doc style: [docs/ezycad_doc_style.md](../docs/ezycad_doc_style.md). OCCT build: [docs/building-occt.md](../docs/building-occt.md). diff --git a/agents/conventions/token-lean.md b/agents/conventions/token-lean.md index 228a9168..c6f2a777 100644 --- a/agents/conventions/token-lean.md +++ b/agents/conventions/token-lean.md @@ -13,23 +13,23 @@ Goal: give assistants **only what they need** for the task at hand. Full style g ## Load on demand -| Task | Read | -| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| Build / test | [workflows/local-dev.md](../workflows/local-dev.md) — or root [README.md](../../README.md#building-instructions) | -| OCCT / WASM (shared `src/` APIs) | [occt-wasm-dual-version.md](occt-wasm-dual-version.md) — desktop 8 vs wasm 7.9.3 until wasm works on OCCT 8 | -| OCCT handles | [occt-handles.md](occt-handles.md) — `*_ptr` / `opencascade::handle` over `Handle()` (clang-format) | -| User-visible UI/settings | [user-docs-sync.md](user-docs-sync.md) + target `docs/usage-*.md` only | -| Sketch subsystem (`src/skt*`, `tests/skt_*`, sketch behavior in `occt_view` / `gui`) | [src/doc/sketch.md](../../src/doc/sketch.md) — read before editing; update when API, invariants, module layout, or workflows change | -| Shape module (`src/shp*`, shape ops in `occt_view` / `gui`) | [src/doc/shape.md](../../src/doc/shape.md) — read before editing; update when API, operation patterns, or registration/undo change | +| Task | Read | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Build / test | [workflows/local-dev.md](../workflows/local-dev.md) — or root [README.md](../../README.md#building-instructions) | +| OCCT / WASM (shared `src/` APIs) | [occt-wasm-dual-version.md](occt-wasm-dual-version.md) — desktop 8 vs wasm 7.9.3 until wasm works on OCCT 8 | +| OCCT handles | [occt-handles.md](occt-handles.md) — `*_ptr` / `opencascade::handle` over `Handle()` (clang-format) | +| User-visible UI/settings | [user-docs-sync.md](user-docs-sync.md) + target `docs/usage-*.md` only | +| Sketch subsystem (`src/skt*`, `tests/skt_*`, sketch behavior in `occt_view` / `gui`) | [src/doc/sketch.md](../../src/doc/sketch.md) — read before editing; update when API, invariants, module layout, or workflows change | +| Shape module (`src/shp*`, shape ops in `occt_view` / `gui`) | [src/doc/shape.md](../../src/doc/shape.md) — read before editing; update when API, operation patterns, or registration/undo change | | GUI / viewer shell (`src/gui*`, input routing, settings panes) | [src/doc/gui.md](../../src/doc/gui.md) — read before editing; update when modes, Options, hotkeys, or settings keys change; sketch tool Options: tool-specific block **above** **Sketch options** | -| Script consoles (`src/scr*`, bindings) | [src/doc/script.md](../../src/doc/script.md) — read before editing; update when `ezy`/`view` API or console UI changes | -| Utilities (`src/utl*`, results, I/O, geometry) | [src/doc/utility.md](../../src/doc/utility.md) — read before editing; update when shared helper contracts change | -| Docs build | [workflows/docs-build.md](../workflows/docs-build.md) | -| Editing Markdown tables | [markdown-tables.md](markdown-tables.md) — align GFM pipes; after edits: `python scripts/agent_check.py ` | -| Release | [workflows/release.md](../workflows/release.md) | -| Specific issue/PR | One file under `drafts/issues/active/` or `drafts/prs/active/` | -| Feature plan under `plans/` | **Only** the matching file when the prompt is clearly about that feature (see [plans/README.md](../plans/README.md)); never bulk-load `plans/` | -| Forum posts | [outreach/discoverability.md](../outreach/discoverability.md) | +| Script consoles (`src/scr*`, bindings) | [src/doc/script.md](../../src/doc/script.md) — read before editing; update when `ezy`/`view` API or console UI changes | +| Utilities (`src/utl*`, results, I/O, geometry) | [src/doc/utility.md](../../src/doc/utility.md) — read before editing; update when shared helper contracts change | +| Docs build | [workflows/docs-build.md](../workflows/docs-build.md) | +| Editing Markdown tables | [markdown-tables.md](markdown-tables.md) — align GFM pipes; after edits: `python scripts/agent_check.py ` | +| Release | [workflows/release.md](../workflows/release.md) | +| Specific issue/PR | One file under `drafts/issues/active/` or `drafts/prs/active/` | +| Feature plan under `plans/` | **Only** the matching file when the prompt is clearly about that feature (see [plans/README.md](../plans/README.md)); never bulk-load `plans/` | +| Forum posts | [outreach/discoverability.md](../outreach/discoverability.md) | ## Draft hygiene (saves tokens long-term) diff --git a/src/doc/gui.md b/src/doc/gui.md index 1804dafc..84b4d5d9 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -166,7 +166,7 @@ Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GU | Remappable chord | `m_hotkeys` hit | `dispatch_hotkey_action_` (`Gui_action`: sketch/shape modes, booleans, delete, copy/paste, file, undo/redo) | | Move-mode keys | `Mode::Move` | `on_key_move_mode_` (axis constraints X/Y/Z); hardcoded | | Rotate-mode keys | `Mode::Rotate` | `on_key_rotate_mode_` (axis pick, Tab angle); hardcoded | -| Align-shafts keys | `Mode::Shape_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | +| Align-shafts keys | `Mode::Shape_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | Default remappable chords include G/R/S/J/E/C/F/D shape tools; sketch tools N/L/A/Q/B/O/U/I/P and Shift variants; Shift+P polar, Shift+X cross-section; Ctrl+Shift+C/F/M booleans; Shift+D delete; Ctrl+C / Ctrl+V copy/paste (in-app shape clipboard); Ctrl+N/O/S; Ctrl+Z / Ctrl+Y. Unmodified X/Y/Z are reserved for Move/Rotate axis toggles (`is_reserved_chord`); Shift+X remains free for cross-section. Remappable keys must pass `is_bindable_key` (letters, digits, Space, and named keys that round-trip in settings JSON); punctuation such as `,` / `.` and numpad keys are rejected. Settings **Keyboard shortcuts** has a `?` to `doc_urls::k_hotkeys` ([usage-settings.md#keyboard-shortcuts](../../docs/usage-settings.md#keyboard-shortcuts)). @@ -176,16 +176,16 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for ### Mouse move (`GUI::on_mouse_pos`) -| `Mode` | Delegate | -| --------------------------------------------------- | ------------------------------------------- | -| `Move` | `shp_move().move_selected` | -| `Rotate` | `shp_rotate().rotate_selected` | -| `Scale` | `shp_scale().scale_selected` | -| `Shape_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | -| `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | -| `Shape_polar_duplicate` | `shp_polar_dup().move_point` | -| Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | -| `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | +| `Mode` | Delegate | +| --------------------------------------------------------- | -------------------------------------------------------------------- | +| `Move` | `shp_move().move_selected` | +| `Rotate` | `shp_rotate().rotate_selected` | +| `Scale` | `shp_scale().scale_selected` | +| `Shape_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | +| `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | +| `Shape_polar_duplicate` | `shp_polar_dup().move_point` | +| Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | +| `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Always calls `m_view->on_mouse_move(screen_coords)` first. @@ -215,7 +215,7 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `Normal` | `options_normal_mode_` (selection filter, orthographic) | | `Move` / `Rotate` / `Scale` | `options_*_mode_` (constraints, axis, material) | -| `Shape_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | +| `Shape_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | | `Shape_chamfer` / `Shape_fillet` | mode + radius/distance | | `Shape_polar_duplicate` | angle, count, rotate/combine, **Dup** button | | `Shape_cross_section` | local XY/XZ/YZ, invert normal, hide back side, show section outline, bbox-ranged offset, Clip, Cross section sketch | @@ -230,7 +230,7 @@ Vertical order for sketch tool Options panes: 1. Mode title + doc **?** (`options_sketch_mode_header_`) 2. Tool-specific **Options** (checkboxes, combos, tool help) — **above** shared sketch controls -3. **Sketch options** (`options_sketch_shared_controls_`: snap guide mode, snap dist, faint shapes, …) +3. Separator, then **Sketch options** (`options_sketch_shared_controls_`: snap guide mode, snap dist, faint shapes, …) 4. Optional **Shortcuts** / other footers (`options_sketch_len_angle_hotkeys_`, …) `options_sketch_common_()` is header + shared controls only (no tool-specific block). Prefer composing header → tool Options → shared when a tool has its own controls (see `options_sketch_add_bone_mode_`). Do not put tool-specific controls below **Sketch options**. diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index c427016b..ed0b3c68 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -1296,6 +1296,7 @@ void GUI::options_sketch_add_bone_mode_() ImGui::TextWrapped( "Click center A, center B, radius 1, radius 2, waist, then holes if enabled."); + ImGui::Separator(); options_sketch_shared_controls_(); options_sketch_len_angle_hotkeys_(); } From 3eeaee56d0c5996d681324a8c9951d925bc33664 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 18:01:43 -0600 Subject: [PATCH 10/12] Bone icon --- docs/usage-sketch.md | 4 +- docs/usage.md | 2 +- res/icons/Sketcher_CreateBone.png | Bin 0 -> 508 bytes res/icons/Sketcher_CreateBone.svg | 34 ++++++ scripts/_gen_bone_icon.py | 170 ++++++++++++++++++++++++++++++ src/gui.cpp | 2 +- 6 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 res/icons/Sketcher_CreateBone.png create mode 100644 res/icons/Sketcher_CreateBone.svg create mode 100644 scripts/_gen_bone_icon.py diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 4f590874..ff7c3438 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -32,7 +32,7 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main - ![Circle Tool](res/icons/Sketcher_CreateCircle.png) [Create circles](#circle-creation-tools) - ![Rectangle Tool](res/icons/Sketcher_CreateRectangle.png) ![Square Tool](res/icons/Sketcher_CreateSquare.png) [Draw rectangles and squares](#rectangle-and-square-creation-tools) - ![Slot Tool](res/icons/Sketcher_CreateSlot.png) [Add slots](#slot-creation-tool) - - ![Bone Tool](res/icons/Sketcher_CreateCircle.png) [Add bone](#bone-creation-tool) + - ![Bone Tool](res/icons/Sketcher_CreateBone.png) [Add bone](#bone-creation-tool) - ![Dimension Tool](res/icons/TechDraw_LengthDimension.png) [Dimension tool](#dimension-tool) - ![Add Node Tool](res/icons/Sketcher_CreatePoint.png) [Add nodes](#add-node-tool) @@ -711,7 +711,7 @@ The slot tool allows you to create an oblong or oval-shaped slot with rounded en (bone-creation-tool)= ## Bone Creation Tool -![Bone Tool](res/icons/Sketcher_CreateCircle.png) +![Bone Tool](res/icons/Sketcher_CreateBone.png) The bone tool builds a connecting-rod outline: the outer arcs of the two end circles and the inward waist arcs where the cutters touch those circles. The result is one closed face you can extrude. Permanent nodes **Bone A** and **Bone B** stay at the two centers for later holes, dimensions, or an operation axis. diff --git a/docs/usage.md b/docs/usage.md index 5438842d..c373b4cf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -929,7 +929,7 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for - ![Sketcher_CreateCircle](res/icons/Sketcher_CreateCircle.png) - Add circle (center and radius) - ![Sketcher_Create3PointCircle](res/icons/Sketcher_Create3PointCircle.png) - Add circle from three points *(planned feature)* - ![Sketcher_CreateSlot](res/icons/Sketcher_CreateSlot.png) - Add slot -- ![Sketcher_CreateCircle](res/icons/Sketcher_CreateCircle.png) - Add bone (Shift+U) +- ![Sketcher_CreateBone](res/icons/Sketcher_CreateBone.png) - Add bone (Shift+U) - ![TechDraw_LengthDimension](res/icons/TechDraw_LengthDimension.png) - Dimension tool (D) ### 3D Operations diff --git a/res/icons/Sketcher_CreateBone.png b/res/icons/Sketcher_CreateBone.png new file mode 100644 index 0000000000000000000000000000000000000000..03c0a9e5082b53b371990c8c338e991672e755ef GIT binary patch literal 508 zcmVzEMuQ#>z-m<8=eUqVw8NkBRWo~VA zJRQ3IvZzQC&IU{=x24)gIK}aFAWc{e=)$SA>uJW=L@QBLJE{$3!a8~`Bd4cYXQ*%r zYzCV!8c^bWQ%>>n`qn$XRepNrKR@2dIAr@c5i(AMyk5V_Olw5(ggh5c|Z$FTvgtCe&#(_!k+OCesRVEWS&LL7GBa%mu%k{=XkIil9F! yb%On + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/_gen_bone_icon.py b/scripts/_gen_bone_icon.py new file mode 100644 index 00000000..c8109dea --- /dev/null +++ b/scripts/_gen_bone_icon.py @@ -0,0 +1,170 @@ +"""Create Sketcher_CreateBone.svg + .png matching FreeCAD sketcher icon style.""" +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "res" / "icons" + +# Geometry in 64x64 icon space (same as FreeCAD SVGs). +c1 = (18.0, 32.0) +c2 = (46.0, 32.0) +r1, r2 = 11.0, 15.5 +cut_r = 22.0 + +dist = c2[0] - c1[0] +R1, R2 = r1 + cut_r, r2 + cut_r +a = (R1 * R1 - R2 * R2 + dist * dist) / (2 * dist) +h = math.sqrt(max(0.0, R1 * R1 - a * a)) +mx = c1[0] + a +cut_plus = (mx, c1[1] - h) +cut_minus = (mx, c1[1] + h) + + +def ang(c: tuple[float, float], p: tuple[float, float]) -> float: + return math.atan2(p[1] - c[1], p[0] - c[0]) + + +def pt_on(c: tuple[float, float], r: float, a0: float) -> tuple[float, float]: + return (c[0] + r * math.cos(a0), c[1] + r * math.sin(a0)) + + +def contact(c: tuple[float, float], r: float, cut: tuple[float, float]) -> tuple[float, float]: + dx, dy = cut[0] - c[0], cut[1] - c[1] + d = math.hypot(dx, dy) + return (c[0] + dx * r / d, c[1] + dy * r / d) + + +c1p = contact(c1, r1, cut_plus) +c1m = contact(c1, r1, cut_minus) +c2p = contact(c2, r2, cut_plus) +c2m = contact(c2, r2, cut_minus) +c1o = (c1[0] - r1, c1[1]) +c2o = (c2[0] + r2, c2[1]) + + +def sample_arc( + cx: float, cy: float, r: float, a0: float, a1: float, sweep_ccw: bool, n: int = 48 +) -> list[tuple[float, float]]: + if sweep_ccw: + while a1 < a0: + a1 += 2 * math.pi + return [pt_on((cx, cy), r, a0 + (a1 - a0) * i / n) for i in range(n + 1)] + while a1 > a0: + a1 -= 2 * math.pi + return [pt_on((cx, cy), r, a0 + (a1 - a0) * i / n) for i in range(n + 1)] + + +def poly_outline() -> list[tuple[float, float]]: + pts: list[tuple[float, float]] = [] + # left outer c1o -> c1p CCW + pts += sample_arc(c1[0], c1[1], r1, ang(c1, c1o), ang(c1, c1p), True)[:-1] + # upper waist c1p -> c2p CW around cut_plus + pts += sample_arc(cut_plus[0], cut_plus[1], cut_r, ang(cut_plus, c1p), ang(cut_plus, c2p), False)[:-1] + # right outer c2p -> c2m CCW + pts += sample_arc(c2[0], c2[1], r2, ang(c2, c2p), ang(c2, c2m), True)[:-1] + # lower waist c2m -> c1m CW around cut_minus + pts += sample_arc(cut_minus[0], cut_minus[1], cut_r, ang(cut_minus, c2m), ang(cut_minus, c1m), False)[:-1] + # left bottom c1m -> c1o CCW + pts += sample_arc(c1[0], c1[1], r1, ang(c1, c1m), ang(c1, c1o), True) + return pts + + +def svg_arc(r: float, p_end: tuple[float, float], a0: float, a1: float, sweep_ccw: bool) -> str: + da = (a1 - a0) if sweep_ccw else (a0 - a1) + while da <= 0: + da += 2 * math.pi + large = 1 if da > math.pi else 0 + sweep = 1 if sweep_ccw else 0 + return f"A {r:.3f},{r:.3f} 0 {large},{sweep} {p_end[0]:.3f},{p_end[1]:.3f}" + + +def path_d() -> str: + parts = [f"M {c1o[0]:.3f},{c1o[1]:.3f}"] + parts.append(svg_arc(r1, c1p, ang(c1, c1o), ang(c1, c1p), True)) + parts.append(svg_arc(cut_r, c2p, ang(cut_plus, c1p), ang(cut_plus, c2p), False)) + parts.append(svg_arc(r2, c2m, ang(c2, c2p), ang(c2, c2m), True)) + parts.append(svg_arc(cut_r, c1m, ang(cut_minus, c2m), ang(cut_minus, c1m), False)) + parts.append(svg_arc(r1, c1o, ang(c1, c1m), ang(c1, c1o), True)) + parts.append("Z") + return " ".join(parts) + + +def write_svg(d: str) -> None: + svg = f""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + (OUT_DIR / "Sketcher_CreateBone.svg").write_text(svg, encoding="utf-8") + + +def draw_icon(size: int, pts: list[tuple[float, float]]) -> Image.Image: + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + s = size / 64.0 + poly = [(p[0] * s, p[1] * s) for p in pts] + + for width, color in [ + (8 * s, (21, 24, 25, 255)), + (4 * s, (211, 215, 207, 255)), + (2.2 * s, (255, 255, 255, 230)), + ]: + layer = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + dr = ImageDraw.Draw(layer) + dr.line(poly + [poly[0]], fill=color, width=max(1, int(round(width))), joint="curve") + img = Image.alpha_composite(img, layer) + + dr = ImageDraw.Draw(img) + for c in (c1, c2): + cx, cy = c[0] * s, c[1] * s + R = 5.5 * s + r = 3.8 * s + dr.ellipse([cx - R, cy - R, cx + R, cy + R], outline=(46, 0, 0, 255), width=max(1, int(1.5 * s))) + dr.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(239, 41, 41, 255), outline=(239, 41, 41, 255)) + return img + + +def main() -> None: + d = path_d() + write_svg(d) + pts = poly_outline() + draw_icon(48, pts).save(OUT_DIR / "Sketcher_CreateBone.png") + print("wrote", OUT_DIR / "Sketcher_CreateBone.svg") + print("wrote", OUT_DIR / "Sketcher_CreateBone.png") + + +if __name__ == "__main__": + main() diff --git a/src/gui.cpp b/src/gui.cpp index 335a114b..ffdc5deb 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -197,7 +197,7 @@ void GUI::initialize_toolbar_() {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add circle", Mode::Sketch_add_circle}, {load_texture("res/icons/Sketcher_Create3PointCircle.png"), false, "Add circle from three points", Mode::Sketch_add_circle_3_pts}, {load_texture("res/icons/Sketcher_CreateSlot.png"), false, "Add slot", Mode::Sketch_add_slot}, - {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add bone", Mode::Sketch_add_bone}, + {load_texture("res/icons/Sketcher_CreateBone.png"), false, "Add bone", Mode::Sketch_add_bone}, {load_texture("res/icons/TechDraw_LengthDimension.png"), false, "Length dimension", Mode::Sketch_dim_anno}, {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude}, {load_texture("res/icons/PartDesign_Chamfer.png"), false, "Chamfer", Mode::Shape_chamfer}, From 4f812bbaadeca8cf623d060a507275742ecde50e Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 18:10:57 -0600 Subject: [PATCH 11/12] Improve --- docs/usage-sketch.md | 1 + src/skt_tools_bone.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index ff7c3438..6999f9af 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -741,6 +741,7 @@ The sketch keeps only that trimmed outline (four arcs). Extrude it with the norm - Snap the two centers to existing nodes, or to the sketch origin. - After Add with no holes, use **Add circle** snapped to **Bone A** / **Bone B** if you want holes later. - For waist, click on either side of the bone axis; the distance from the axis to that point is half the waist (the dim then snaps to the true neck). +- If a hole radius is greater than or equal to that end's outer radius, a toast explains the hole must be smaller; the click is ignored. - Preferences **Add center nodes** and **Holes** are saved as `gui.bone_add_center_nodes` and `gui.bone_holes`. ## Operation Axis Tool diff --git a/src/skt_tools_bone.cpp b/src/skt_tools_bone.cpp index 5d546da1..ba9c0422 100644 --- a/src/skt_tools_bone.cpp +++ b/src/skt_tools_bone.cpp @@ -26,6 +26,11 @@ bool bone_hole_radius_ok_(double outer_r, double hole_r) { return hole_r > Precision::Confusion() && hole_r + Precision::Confusion() < outer_r; } + +bool bone_hole_radius_too_large_(double outer_r, double hole_r) +{ + return hole_r + Precision::Confusion() >= outer_r; +} } // namespace void Sketch_tools::bone_on_enter_() @@ -292,7 +297,11 @@ bool Sketch_tools::bone_after_hole_a_(double hole_r) if (holes == Bone_holes::One_radius) { if (!bone_hole_radius_ok_(*m_bone_r1, hole_r) || !bone_hole_radius_ok_(*m_bone_r2, hole_r)) + { + if (bone_hole_radius_too_large_(*m_bone_r1, hole_r) || bone_hole_radius_too_large_(*m_bone_r2, 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_(); @@ -301,7 +310,11 @@ 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)) + m_sketch.m_view.gui().show_message("Hole radius must be smaller than the end circle."); return false; + } m_bone_hole_r1 = hole_r; bone_begin_next_edge_from_(m_bone_centers->second); @@ -313,7 +326,11 @@ 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)) + m_sketch.m_view.gui().show_message("Hole radius must be smaller than the end circle."); return false; + } m_bone_hole_r2 = hole_r; return bone_try_commit_(); From 61b156b9c3f984192ef8504fd21988afbc137cc3 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 3 Sep 2026 19:06:53 -0600 Subject: [PATCH 12/12] Improve --- CHANGELOG.md | 10 +++- CMakeLists.txt | 32 ++++++++++-- README.md | 4 +- docs/building-occt.md | 20 ++++---- docs/usage-settings.md | 2 +- docs/usage-sketch.md | 8 ++- src/doc/sketch.md | 2 +- src/shp_extrude.h | 2 +- src/skt_tools_bone.cpp | 111 ++++++++++++++++++++--------------------- 9 files changed, 112 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afac3812..62add7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Extrude Both sides**: Options **Both sides** defaults to on (session sticky; not a Settings key). + +### Fixed + +- **Wasm configure/link**: Detect Emscripten via `EMSCRIPTEN` / `CMAKE_SYSTEM_NAME` (modern emsdk reports Clang). Link the FreeType package from the OCCT wasm install so `wasm-ld` finds `libfreetype.a` instead of bare `-lfreetype`. + ### 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 (live dim sits on that section; offset from mid when end radii differ). 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. Remappable as **`mode.add_bone`**. +- **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`**. - **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/CMakeLists.txt b/CMakeLists.txt index c741f5f7..7daae7fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,8 +14,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Add custom CMake modules path set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") -# Detect Emscripten early (for optional branching) -if(CMAKE_CXX_COMPILER MATCHES "em\\+\\+") +# Detect Emscripten early (for optional branching). +# Modern emsdk toolchains force CMAKE_CXX_COMPILER_ID to Clang and set EMSCRIPTEN=1 / +# CMAKE_SYSTEM_NAME=Emscripten; do not rely on matching "em++" in the compiler path. +if(EMSCRIPTEN OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR CMAKE_CXX_COMPILER MATCHES "em\\+\\+") set(EMSCRIPTEN_BUILD TRUE) endif() @@ -84,8 +86,9 @@ target_compile_features(lua PUBLIC c_std_99) # Combines CMAKE_BINARY_DIR and project-specific binary dir for flexibility set(BINARY_DIR ${${PROJECT_NAME}_BINARY_DIR}) -# Detect if we're using Emscripten compiler (for WebAssembly builds) -if(CMAKE_CXX_COMPILER MATCHES "/em\\+\\+(-[a-zA-Z0-9.])?(\.bat)?$") +# EzyCad branches on CMAKE_CXX_COMPILER_ID STREQUAL "Emscripten" (wasm link flags, +# TKOpenGles, no gtest/GLFW nuget). Override the Clang ID that emsdk forces. +if(EMSCRIPTEN_BUILD) message(" * C++ compiler: Emscripten") set(CMAKE_CXX_COMPILER_ID "Emscripten") else() @@ -200,6 +203,24 @@ else() set(OCCT_BIN_DIR) endif() +# Wasm OCCT (scripts/build-occt-*-wasm.ps1) installs FreeType under install/freetype. +# OCCT INTERFACE libs use $; without this package CMake emits bare -lfreetype. +if(EMSCRIPTEN_BUILD AND OpenCASCADE_FOUND) + get_filename_component(_ezy_occt_cmake "${OpenCASCADE_DIR}" ABSOLUTE) + get_filename_component(_ezy_occt_lib_cmake "${_ezy_occt_cmake}" DIRECTORY) + get_filename_component(_ezy_occt_lib "${_ezy_occt_lib_cmake}" DIRECTORY) + get_filename_component(_ezy_occt_prefix "${_ezy_occt_lib}" DIRECTORY) + set(freetype_DIR "${_ezy_occt_prefix}/freetype/lib/cmake/freetype" CACHE PATH + "FreeType CMake package from OCCT wasm install" FORCE) + if(NOT EXISTS "${freetype_DIR}/freetype-config.cmake") + message(FATAL_ERROR + "Wasm FreeType CMake package not found at ${freetype_DIR}. " + "Build OCCT with scripts/build-occt-793-wasm.ps1 (or build-occt-v8-wasm.ps1).") + endif() + find_package(freetype CONFIG REQUIRED) + message(STATUS "Using FreeType (wasm) from \"${freetype_DIR}\"") +endif() + # OCCT 3rd-party DLL path must not contain backticks — a common mistake is pasting # -DOCCT_3RD_PARTY_DIR=... from Markdown and including the closing ` of `...`. if(DEFINED OCCT_3RD_PARTY_DIR) @@ -390,7 +411,8 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Emscripten") target_link_libraries( ${PROJECT_NAME} lua - ${OpenCASCADE_LIBS}) + ${OpenCASCADE_LIBS} + freetype) else() diff --git a/README.md b/README.md index 0862aa24..1257c338 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,8 @@ Full guide: **[docs/building-occt.md](docs/building-occt.md)** (Windows prebuilt - **OCCT 7.9.3 for wasm (recommended):** `scripts\build-occt-793-wasm.ps1` (or `.cmd`) after `emsdk_env` — see [docs/building-occt.md](docs/building-occt.md#webassembly-emscripten). OCCT 8.x has a GLES shading regression on wasm (see [docs/bugs.md](docs/bugs.md)). - **OCCT 8.0.0.p1 for wasm:** `scripts\build-occt-v8-wasm.ps1` for regression testing against upstream. - Configure the EzyCad project with Emscripten (Ninja recommended): - - `emcmake cmake -S . -B build-em-7-9-3 -Wno-dev -G Ninja -DOpenCASCADE_DIR=C:/Users/you/occt-wasm-build/V7_9_3/install/lib/cmake/opencascade -DCMAKE_BUILD_TYPE=Release` - - Add **-Wno-dev** to suppress any remaining CMake developer warnings. + - `emcmake cmake -S . -B build-em-7-9-3 -Wno-dev -G Ninja "-DOpenCASCADE_DIR=%USERPROFILE%/occt-wasm-build/V7_9_3/install/lib/cmake/opencascade" -DCMAKE_BUILD_TYPE=Release` + - Replace `%USERPROFILE%` with your home if the shell does not expand it (path must contain `OpenCASCADEConfig.cmake`). Add **-Wno-dev** to suppress remaining CMake developer warnings. - If configure **freezes** after that warning, the hang is often in `find_package(OpenCASCADE)` or Emscripten compiler detection. Run with `--debug-output` to see where it stops. - Build: - `ninja -C build-em-7-9-3` (or `emmake cmake --build . --config Release`) diff --git a/docs/building-occt.md b/docs/building-occt.md index cc81c26a..04301478 100644 --- a/docs/building-occt.md +++ b/docs/building-occt.md @@ -136,15 +136,17 @@ FreeType wasm configure also disables optional zlib/png/harfbuzz finds to simpli ### Wasm troubleshooting -| Symptom | Fix | -| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `running scripts is disabled` | `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` **or** use `build-occt-793-wasm.cmd` / `build-occt-v8-wasm.cmd` **or** `powershell -ExecutionPolicy Bypass -File ...` | -| `Can't initialize filter; xz` | Script uses `.tar.gz` for FreeType; delete stale `*.tar.xz` under `src/` and re-run | -| `source directory .../build/freetype does not contain CMakeLists.txt` | Fixed: do not name a PowerShell function parameter `$Args` (shadows automatic `$Args`) | -| `emcc` not found | Run `emsdk_env.bat` / `emsdk_env.ps1` in the same shell | -| EzyCad configure hangs on `find_package(OpenCASCADE)` | `emcmake cmake ... --debug-output`; verify `OpenCASCADE_DIR` path | -| OCCT 8 + ghosted dimension labels | Retest `gui.edge_dim_text_render_mode` (Z-layer Topmost); grid compositing changed in 8.0 | -| Shaded faces missing / wireframe-only solids (wasm, OCCT 8.x) | Use **7.9.3** (`build-occt-793-wasm.ps1`); see [bugs.md](bugs.md) | +| Symptom | Fix | +| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `running scripts is disabled` | `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` **or** use `build-occt-793-wasm.cmd` / `build-occt-v8-wasm.cmd` **or** `powershell -ExecutionPolicy Bypass -File ...` | +| `Can't initialize filter; xz` | Script uses `.tar.gz` for FreeType; delete stale `*.tar.xz` under `src/` and re-run | +| `source directory .../build/freetype does not contain CMakeLists.txt` | Fixed: do not name a PowerShell function parameter `$Args` (shadows automatic `$Args`) | +| `emcc` not found | Run `emsdk_env.bat` / `emsdk_env.ps1` in the same shell | +| EzyCad configure hangs on `find_package(OpenCASCADE)` | `emcmake cmake ... --debug-output`; verify `OpenCASCADE_DIR` path | +| `GLFW3_LIBRARY-NOTFOUND` / `* C++ compiler: Clang` under `emcmake` | Stale/broken Emscripten detect: wipe `build-em-*` and reconfigure. CMake must print `* C++ compiler: Emscripten` (uses `EMSCRIPTEN` / `CMAKE_SYSTEM_NAME`, not path regex). | +| `unable to find library -lfreetype` at wasm link | FreeType must come from the same OCCT wasm install (`...\install\freetype\lib\cmake\freetype`). Reconfigure after `build-occt-793-wasm.ps1`; CMake should print `Using FreeType (wasm)`. | +| OCCT 8 + ghosted dimension labels | Retest `gui.edge_dim_text_render_mode` (Z-layer Topmost); grid compositing changed in 8.0 | +| Shaded faces missing / wireframe-only solids (wasm, OCCT 8.x) | Use **7.9.3** (`build-occt-793-wasm.ps1`); see [bugs.md](bugs.md) | --- diff --git a/docs/usage-settings.md b/docs/usage-settings.md index d528db04..0f903ea3 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -115,7 +115,7 @@ For other non-sketch Options content, see the matching tool section in the usage 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 guide mode** (*Traditional*, *Fullscreen*, *Both*, *None*), then **Snap dist** and **All co-axial nodes** (global co-axial grid vs closest-per-axis only; both hidden when mode is *None*), and **Faint shapes** (show document solids as ghost/wire while sketching; style and **Shape Faint Strength** in **Settings -> Sketch -> Appearance**). *None* disables snap-to-node and snap guides. See [How sketch snap works](usage-sketch.md#sketch-snapping). **Snap guide color (node)**, **Snap guide color (axis)**, **Snap guide line width**, **Snap guide mode**, and **All co-axial nodes** are also in **Settings -> Sketch -> Snap** (persisted in `gui.*` keys below). -- **Extrude sketch face**: under **Extrude**, **Both sides**, **Twist**, and **Material** for the new solid (same document preset as **Normal** mode Options **Material**). With **Twist** on, the first drag sets height, then a second drag sets twist angle about the face center; Tab / Shift+Tab enter height / angle. Fast drag preview for dense faces is configured in **Settings -> Sketch -> Appearance** (**Extrude fast preview**); with **Twist**, face copies rotate as well as translate. Other modes that still show **Material** in Options use that same preset when relevant (for example **Sketch from planar face**). +- **Extrude sketch face**: under **Extrude**, **Both sides** (default on), **Twist**, and **Material** for the new solid (same document preset as **Normal** mode Options **Material**). With **Twist** on, the first drag sets height, then a second drag sets twist angle about the face center; Tab / Shift+Tab enter height / angle. Fast drag preview for dense faces is configured in **Settings -> Sketch -> Appearance** (**Extrude fast preview**); with **Twist**, face copies rotate as well as translate. 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. - **Add line edge**: under **Options**, **Add midpoint nodes** ([usage-sketch.md#line-edge-option-add-midpoint-nodes](usage-sketch.md#line-edge-option-add-midpoint-nodes)) and **Place from center** ([usage-sketch.md#line-edge-option-place-from-center](usage-sketch.md#line-edge-option-place-from-center)); each has a **?** link. See also [Line edge Options](usage-sketch.md#line-edge-options). The midpoint setting is also in **Settings -> Sketch -> Nodes** (persisted). - **Add bone**: under **Options**, **Add center nodes** (default on; permanent **Bone A** / **Bone B**) and **Holes** (**None**, **One radius**, **Two radii**). Persisted as **`gui.bone_add_center_nodes`** and **`gui.bone_holes`**. See [Bone Creation Tool](usage-sketch.md#bone-creation-tool). diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 6999f9af..0cf4188e 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -722,9 +722,13 @@ The bone tool builds a connecting-rod outline: the outer arcs of the two end cir 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. -6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). The live dimension spans that neck; 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. +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. +After a successful commit, the mode switches to **Sketch inspection** (ready to select the face or Extrude). Unlike circle or slot, Add bone does not stay active for another bone. + +A live length dimension is shown only while placing the two centers. End radii, holes, and waist use the geometry preview (circle / bone outline); Tab still enters exact values. + **Options** (while Add bone is active): | Option | Default | Meaning | @@ -740,7 +744,7 @@ The sketch keeps only that trimmed outline (four arcs). Extrude it with the norm - Snap the two centers to existing nodes, or to the sketch origin. - After Add with no holes, use **Add circle** snapped to **Bone A** / **Bone B** if you want holes later. -- For waist, click on either side of the bone axis; the distance from the axis to that point is half the waist (the dim then snaps to the true neck). +- For waist, click on either side of the bone axis; the distance from the axis to that point is half the waist. - If a hole radius is greater than or equal to that end's outer radius, a toast explains the hole must be smaller; the click is ignored. - Preferences **Add center nodes** and **Holes** are saved as `gui.bone_add_center_nodes` and `gui.bone_holes`. diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 27c912ff..2945edab 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -204,7 +204,7 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::list n = bone_axis_perp_(); + if (!n || !m_bone_r1 || !m_bone_r2) + return; - if (m_tmp_edges.size() >= 5) - { - move_line_string_pt_(screen_coords); - bone_update_preview_(); - return; - } + const gp_Pnt2d& c1 = m_bone_centers->first; + const gp_Pnt2d& c2 = m_bone_centers->second; + const gp_Pnt2d mid = get_midpoint(c1, c2); - const std::optional n = bone_axis_perp_(); - if (!n || !m_bone_r1 || !m_bone_r2) - return; + auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) + { + Sketch_edge& edge = m_tmp_edges.back(); + // Waist value is twice the distance from the bone axis (independent of click along-axis). + double half = std::abs(gp_Vec2d(c1, pt_b).Dot(*n)); + if (m_sketch.m_dims.entered_edge_len().has_value()) + half = m_sketch.m_dims.entered_edge_len()->len * 0.5; - const gp_Pnt2d& c1 = m_bone_centers->first; - const gp_Pnt2d& c2 = m_bone_centers->second; - const gp_Pnt2d mid = get_midpoint(c1, c2); + if (half <= Precision::Confusion()) + { + m_sketch.m_dims.clear_tmp_dim_anno(); + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } - auto l = [&](const std::optional&, const gp_Pnt2d& pt_b) - { - Sketch_edge& edge = m_tmp_edges.back(); - // Waist value is twice the distance from the bone axis (independent of click along-axis). - double half = std::abs(gp_Vec2d(c1, pt_b).Dot(*n)); - if (m_sketch.m_dims.entered_edge_len().has_value()) - half = m_sketch.m_dims.entered_edge_len()->len * 0.5; + const double waist = 2.0 * half; + gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); + gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); - if (half <= Precision::Confusion()) - { - m_sketch.m_dims.clear_tmp_dim_anno(); - m_sketch.m_view.remove(m_tmp_shp); - m_tmp_shp = nullptr; - return; - } + Bone_params params; + params.c1 = c1; + params.c2 = c2; + params.r1 = *m_bone_r1; + params.r2 = *m_bone_r2; + params.waist = waist; + params.drive = Bone_drive::Waist; + if (const std::optional g = compute_bone_geom(params)) + { + // Place the live rubber-band on the true neck (offset from mid when r1 != r2). + const Bone_profile pr = get_bone_profile(*g); + span_a = pr.waist_plus; + span_b = pr.waist_minus; + } - const double waist = 2.0 * half; - gp_Pnt2d span_a = gp_Pnt2d(mid).Translated(-(*n) * half); - gp_Pnt2d span_b = gp_Pnt2d(mid).Translated((*n) * half); - - Bone_params params; - params.c1 = c1; - params.c2 = c2; - params.r1 = *m_bone_r1; - params.r2 = *m_bone_r2; - params.waist = waist; - params.drive = Bone_drive::Waist; - if (const std::optional g = compute_bone_geom(params)) - { - // Place the live dim on the true neck (offset from mid when r1 != r2). - const Bone_profile pr = get_bone_profile(*g); - span_a = pr.waist_plus; - span_b = pr.waist_minus; - } + m_last_pt = span_b; + m_sketch.update_edge_shp_(edge, span_a, span_b); - m_last_pt = span_b; - m_sketch.update_edge_shp_(edge, span_a, span_b); + const double dist = waist / m_sketch.m_view.get_display_to_model_scale(); + m_sketch.m_dims.clear_tmp_dim_anno(); + m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); + bone_update_preview_(); + }; - const double dist = waist / m_sketch.m_view.get_display_to_model_scale(); - m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); - m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); - bone_update_preview_(); - }; + move_sketch_pt_(screen_coords, l); + return; + } - move_sketch_pt_(screen_coords, l); + move_line_string_pt_(screen_coords); + m_sketch.m_dims.clear_tmp_dim_anno(); + bone_update_preview_(); } void Sketch_tools::bone_begin_next_edge_from_(const gp_Pnt2d& origin) @@ -355,6 +351,7 @@ bool Sketch_tools::bone_try_commit_() m_sketch.add_bone(params.c1, params.c2, params.r1, params.r2, *m_bone_waist, add_centers, m_bone_hole_r1, m_bone_hole_r2); clear_tmps(); + m_sketch.m_view.gui().set_parent_mode(); return true; }