From b9198f2ffde1742512204d2057cf1bc8044689dc Mon Sep 17 00:00:00 2001 From: Trailcode Date: Mon, 6 Jul 2026 19:01:49 -0600 Subject: [PATCH 1/5] Improve --- docs/usage-sketch.md | 5 +++- docs/usage.md | 23 +++++++++++++-- src/doc/sketch.md | 2 +- src/gui.cpp | 51 ++++++++++++++++++++++++++++++++++ src/gui.h | 5 ++++ src/gui_mode.cpp | 2 +- src/sketch.cpp | 63 ++++++++++++++++++++++++++++++++---------- src/sketch.h | 9 ++++++ tests/sketch_tests.cpp | 14 ++++++++++ 9 files changed, 154 insertions(+), 20 deletions(-) diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 3ea6d63..dd74c56 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -43,7 +43,7 @@ See [Sketch origin](#sketch-origin) — every sketch includes one permanent refe (sketch-origin)= ## Sketch origin -**Every sketch has exactly one Origin** — a fixed reference point on the sketch plane, shown as a red **+** marker whenever the sketch is visible in sketch mode (and in tools such as **polar duplicate** that snap to sketch nodes). +**Every sketch has exactly one Origin** — a fixed reference point on the sketch plane, shown as a red **+** marker (a **permanent node**, not placed with a tool) whenever the sketch is visible in sketch mode (and in tools such as **polar duplicate** that snap to sketch nodes). | | | | ---: | --- | @@ -54,11 +54,13 @@ See [Sketch origin](#sketch-origin) — every sketch includes one permanent refe | **Snapping** | Acts like any other sketch vertex: axis guides, vertex lock, and distance snap apply. Origins from **other visible sketches** are also snap targets on the current plane. | | **Cannot delete** | Unlike [Add node](#add-node-tool) points, the Origin cannot be removed from the selection. | | **Marker size** | **Settings -> Sketch -> Permanent node annotation size** (`permanent_node_anno_scale`; see [usage-settings.md](usage-settings.md#settings-file-reference)). | +| **Sketch properties** | **Sketch List -> [P]** — edit **Origin** **X** / **Y** (sketch plane coordinates) or **Reset to default**. Changes are undoable. | **Tips** - Use the Origin as a stable anchor when you need a known center (especially on [face-derived](#create-sketch-from-planar-face-tool) sketches). - If the **+** is hard to see, increase **Permanent node annotation size** in Settings. +- Open **Sketch properties** (**[P]** on the sketch row) to set **Origin** **X** / **Y** or **Reset to default** (plane `(0, 0)` or face bbox center). - While an **operational axis** is active in mirror/revolve phase, **+** markers (including Origin) are temporarily hidden so edge and face selection stays clear; they return when you **Clear axis** or leave that mode. ## Sketch snapping @@ -70,6 +72,7 @@ While you draw or place points in sketch mode, EzyCad helps you align to existin | **Snap distance** | Larger **Snap dist** values let snaps engage from farther away (screen pixels, converted to the sketch plane at the cursor). | | **Snap guides** | **Snap guide mode**: *Traditional* (local markers at guide intersections), *Fullscreen* (view-spanning axis lines), or *Both*. **Snap guide color (node)** is used when both X and Y align to the same vertex; **Snap guide color (axis)** when only one axis aligns. A separate checkbox **All co-axial nodes** (in the sketch Options panel and in Settings) enables *global* mode: when on, full horizontal and vertical guide lines + markers are shown for *all* nodes in the current sketch and all other visible sketches (the complete set of co-axial alignments). When off (default), only the closest node per active axis is annotated (classic closest-relative behavior). | | **Axis alignment** | Near a snap target, the pick can align to that point's **X** or **Y** on the sketch plane; guides show which axis is active. When **both** axes align to the **same** point, the cursor **locks to that vertex**. | +| **Sketch origin** | Each sketch's built-in **Origin** **+** (see [Sketch origin](#sketch-origin)) is always a snap target on that sketch. | | **Mid-point snap (Add node)** | A click near a **straight** edge (not at its ends) snaps onto the segment and **splits** it at commit time (see [Add node tool](#add-node-tool)). Separate from vertex lock. | | **Automatic splitting on edge intersections** | When you add a new straight (linear) edge using the Line Edge tool or Multi-Line Edge tool, if it crosses or touches the interior of any existing straight edge, the existing edge is automatically split at the intersection point. The new edge is also subdivided into atomic segments where needed. The same splitting occurs when an endpoint of the new edge snaps to the midpoint of an existing edge. When you add an **arc segment**, existing straight and arc edges are split at interior crossings, and the new arc is subdivided at intersection points on its interior. This produces correct T-junctions, crossings, and cleanly divided faces from a single sketch. | | **Other visible sketches** | Nodes from **other visible sketches** are projected onto the current sketch plane and act as snap targets (same distance rules). Each of those sketches contributes its **Origin** as well as any user-placed nodes. Useful for multi-sketch layouts and tools such as **polar duplicate** that pick sketch points. | diff --git a/docs/usage.md b/docs/usage.md index 569741e..1419563 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -56,6 +56,7 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d - [View and manage 2D sketches](#sketch-list) - [Select and edit sketch elements](#sketch-list) - [Toggle sketch visibility](#sketch-list) + - [Sketch origin (permanent + reference point per sketch)](usage-sketch.md#sketch-origin) 4. **Shape List** - [List 3D solids, materials, and display options](#shape-list) @@ -92,7 +93,7 @@ Each row is laid out left to right: - **Rename** - Click the name field and type a new name. - **Visibility** - Checkbox to show or hide the sketch in the 3D view. - **Underlay** - Checkbox to show or hide an [image underlay](usage-sketch.md#image-underlay) when one is imported (disabled until an underlay exists; tooltip *Display underlay*). -- **Sketch properties** - **`[P]`** opens **Sketch properties** (import/remove underlay, calibration, transform). See [Image underlay](usage-sketch.md#image-underlay). +- **Sketch properties** - **`[P]`** opens **Sketch properties** (sketch **Origin** X/Y, import/remove underlay, calibration, transform). See [Sketch origin](usage-sketch.md#sketch-origin) and [Image underlay](usage-sketch.md#image-underlay). - **Delete** - Right-click the name and choose **Delete**. When expanded, the row shows: @@ -213,7 +214,7 @@ EzyCad uses a workflow-based approach to 3D modeling: start with 2D sketches, th The typical modeling workflow in EzyCad follows these steps: -1. **Create a 2D Sketch**: Use the [2D Sketching tools](usage-sketch.md) to draw 2D geometry on a sketch plane. Sketches consist of edges (lines, arcs, circles) that form closed shapes called faces. +1. **Create a 2D Sketch**: Use the [2D Sketching tools](usage-sketch.md) to draw 2D geometry on a sketch plane. Sketches consist of edges (lines, arcs, circles) that form closed shapes called faces. Every new sketch also gets a built-in **[Origin](usage-sketch.md#sketch-origin)** — a permanent **+** reference point on the plane (at `(0, 0)` on a reference plane, or at the face center when created from a planar face). 2. **Extrude the Sketch**: Use the [Extrude tool](#extrude-sketch-face-tool-e) to convert 2D sketch faces into 3D solid shapes by extending them perpendicular to the sketch plane. @@ -226,6 +227,7 @@ The typical modeling workflow in EzyCad follows these steps: | | | | ---: | --- | | **Sketches** | 2D drawings on a plane that define the profile of your 3D shape | +| **Origin** | One permanent **+** reference node per sketch ([details](usage-sketch.md#sketch-origin)); snap target; listed as **Origin** in the Sketch List | | **Faces** | Closed regions within a sketch that can be [extruded](#extrude-sketch-face-tool-e) into 3D | | **Shapes** | 3D solid objects created from extruded sketch faces | | **Feature Operations** | Transform sketches into 3D geometry or modify existing 3D shapes | @@ -285,6 +287,20 @@ For detailed information on creating 2D geometry, see the [2D Sketching](usage-s ### 2D Sketching +#### Sketch origin (permanent reference point) + +Every sketch has exactly one **Origin**: a fixed point on the sketch plane, shown as a red **+** marker in sketch mode (same style as user-placed [Add node](usage-sketch.md#add-node-tool) points, but you cannot delete it). + +| | | +| ---: | --- | +| **Reference-plane sketch** | Origin at plane coordinates **(0, 0)**. | +| **Sketch from planar face** | Origin at the **bounding-box center** of the extracted face boundary. | +| **Sketch List** | Listed as **Origin** under **Nodes** when you expand a sketch row. | +| **Sketch properties** | **[P]** on the sketch row: **Origin** **X** / **Y** and **Reset to default**. | +| **Snapping** | Full vertex and axis snap; other visible sketches' origins are snap targets too. | + +Full details (marker size, operational-axis visibility, tips): **[Sketch origin](usage-sketch.md#sketch-origin)** in the 2D sketching guide. + See the **[2D Sketching guide](usage-sketch.md)** for full documentation of sketch tools: **[sketch origin](usage-sketch.md#sketch-origin)** (one permanent **+** reference point per sketch), **add node** (points and edge splits), line and multi-line edges, circles, arcs, rectangles, squares, slots, **operation axis** (with Mirror and Revolve actions in the Options panel), edge dimensions, and creating a sketch from a planar face. Revolve (via an operation axis) is one way to generate 3D solids directly from sketch geometry. **Sketch snap (overview):** While drawing or using **Add node**, picks can snap to existing geometry within **Snap dist** (Options panel). The main behaviors: @@ -292,6 +308,7 @@ See the **[2D Sketching guide](usage-sketch.md)** for full documentation of sket | | | | ---: | --- | | **Vertex snap** | Lock to an existing corner when horizontal and vertical axis guides both align to the same point. | +| **Sketch origin** | Every sketch's built-in **Origin** **+** is a snap target (see [Sketch origin](usage-sketch.md#sketch-origin)). | | **Mid-point snap** | With **Add node**, a click near a **straight** edge (but not at its ends) snaps onto the segment; EzyCad places a new vertex there and **splits** the edge into two. You do not need to hit the line exactly. | | **Edge midpoint** | Straight edges often expose a geometric **midpoint** as a snap target while drawing; that is separate from mid-point snap and from user-placed **+** nodes. | @@ -732,7 +749,7 @@ More context on the 3D viewer stack: **[3D viewer (Open CASCADE)](usage-occt-vie - [This usage guide](#ezycad-usage-guide) (source: [usage.md](usage.md)) - [Settings](usage-settings.md) (Settings pane, View menu, JSON settings file, startup project) - [3D viewer (Open CASCADE)](usage-occt-view.md) -- [2D Sketching](usage-sketch.md) (including [add node](usage-sketch.md#add-node-tool) and [image underlay](usage-sketch.md#image-underlay)) +- [2D Sketching](usage-sketch.md) (including [sketch origin](usage-sketch.md#sketch-origin), [add node](usage-sketch.md#add-node-tool), and [image underlay](usage-sketch.md#image-underlay)) - [Scripting (Lua / Python)](scripting.md) The markdown files in the repository remain the canonical source; Read the Docs publishes them on each update. Video tutorials are not published yet. diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 597527f..4ac0a45 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -24,7 +24,7 @@ Typical uses: - A `Sketch` is owned by `Occt_view` (`Sketch_list m_sketches`, current sketch `m_cur_sketch`). Use `Sketch_ptr` (`std::shared_ptr`) when sharing. - The constructor **`Occt_view& view` must outlive the sketch**. The sketch holds references to `m_view` and `m_ctx` (the OCCT interactive context). - Each sketch receives a stable **`m_id`** from `Occt_view::allocate_sketch_id()`. Display names (`m_name`) may duplicate; undo deltas and file I/O key off `get_id()`. -- Each sketch has an **origin node** (permanent "+" marker): plane origin `(0,0)` for new sketches, or the bounding-box center of the originating face wire when created from a face. +- `set_origin_pt(gp_Pnt2d)` / `reset_origin_pt()` / `origin_pt()` — user-editable origin marker (also **Sketch properties** UI). - Node indices are **never compacted**. Deleted nodes become tombstones (`Node::deleted`) so undo/redo and JSON round-trips stay stable. ### Plane and coordinates diff --git a/src/gui.cpp b/src/gui.cpp index a39d2e4..72bd2e2 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1288,11 +1288,62 @@ void GUI::sketch_properties_dialog_() ImGui::TextUnformatted(sk->get_name().c_str()); ImGui::Separator(); + sketch_origin_panel_settings_(sk); sketch_underlay_panel_settings_(sk); m_sketch_properties_open = open; ImGui::End(); } +void GUI::sketch_origin_panel_settings_(const Sketch::sptr& sk) +{ + EZY_ASSERT(sk); + + if (m_sketch_origin_panel_sketch != sk.get()) + { + m_sketch_origin_panel_sketch = sk.get(); + const gp_Pnt2d o = sk->origin_pt(); + m_sketch_origin_xy[0] = o.X(); + m_sketch_origin_xy[1] = o.Y(); + } + + ImGui::TextUnformatted("Origin"); + ImGui::Separator(); + + auto commit_origin_if_edited = [&]() + { + if (!ImGui::IsItemDeactivatedAfterEdit()) + return; + + m_view->push_undo_snapshot(); + sk->set_origin_pt(gp_Pnt2d(m_sketch_origin_xy[0], m_sketch_origin_xy[1])); + m_view->ctx().UpdateCurrentViewer(); + }; + + ImGui::InputDouble("X##sketch_origin", &m_sketch_origin_xy[0], 0.0, 0.0, "%.4f"); + commit_origin_if_edited(); + ImGui::InputDouble("Y##sketch_origin", &m_sketch_origin_xy[1], 0.0, 0.0, "%.4f"); + commit_origin_if_edited(); + + const gp_Pnt2d default_o = sk->default_origin_pt(); + ImGui::TextDisabled("Default: (%.4f, %.4f)", default_o.X(), default_o.Y()); + + if (ImGui::Button("Reset to default")) + { + m_view->push_undo_snapshot(); + sk->reset_origin_pt(); + const gp_Pnt2d o = sk->origin_pt(); + m_sketch_origin_xy[0] = o.X(); + m_sketch_origin_xy[1] = o.Y(); + m_view->ctx().UpdateCurrentViewer(); + } + + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Sketch plane coordinates for the permanent Origin + marker. Click ? for the user guide.", + doc_urls::k_sketch_origin); + + ImGui::Separator(); +} + void GUI::sketch_underlay_panel_settings_(const Sketch::sptr& sk) { EZY_ASSERT(sk); diff --git a/src/gui.h b/src/gui.h index f377fb2..dbf0425 100644 --- a/src/gui.h +++ b/src/gui.h @@ -265,6 +265,7 @@ class GUI void sketch_list_inspector_(const std::shared_ptr& sketch, int index, std::shared_ptr& hover_sketch, size_t& hover_dim_index); void sketch_properties_dialog_(); + void sketch_origin_panel_settings_(const std::shared_ptr& sk); void shape_list_(); void shape_info_dialog_(); void open_shape_info_(const Shp_ptr& shape); @@ -511,6 +512,10 @@ class GUI bool m_sketch_properties_open{false}; std::weak_ptr m_sketch_properties_sketch; + // Sketch origin (properties pane) + void* m_sketch_origin_panel_sketch{nullptr}; + double m_sketch_origin_xy[2]{0.0, 0.0}; + // Sketch underlay related void* m_underlay_panel_sketch{nullptr}; Underlay_calib_phase m_underlay_calib_phase{Underlay_calib_phase::None}; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index d42c060..4c8b019 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -44,7 +44,7 @@ std::string GUI::get_doc_url_for_mode(Mode mode) {Mode::Move, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-move-tool-g"}, {Mode::Rotate, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"}, {Mode::Scale, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-scale-tool-s"}, - {Mode::Sketch_inspection_mode, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketching-2d"}, + {Mode::Sketch_inspection_mode, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-origin"}, {Mode::Sketch_from_planar_face, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#create-sketch-from-planar-face-tool"}, {Mode::Sketch_face_extrude, "https://ezycad.readthedocs.io/en/latest/usage.html#extrude-sketch-face-tool-e"}, {Mode::Shape_chamfer, "https://ezycad.readthedocs.io/en/latest/usage.html#other-feature-operations"}, diff --git a/src/sketch.cpp b/src/sketch.cpp index 4bbc238..90e0fb8 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -363,33 +363,68 @@ void Sketch::get_originating_face_snp_pts_3d_(std::vector& out) } void Sketch::ensure_origin_node_() +{ + if (origin_node_idx_().has_value()) + { + // Commit baseline so cancel_elm() (e.g. from on_mode) does not remove the origin. + m_nodes.finalize(); + return; + } + + const gp_Pnt2d pt = default_origin_pt_(); + const size_t idx = m_nodes.add_new_node(pt, false, true); + Sketch_nodes::Node& n = m_nodes[idx]; + n.origin = true; + n.name = "Origin"; + m_nodes.finalize(); +} + +std::optional Sketch::origin_node_idx_() const { for (size_t i = 0; i < m_nodes.size(); ++i) if (!m_nodes[i].deleted && m_nodes[i].origin) - { - // Commit baseline so cancel_elm() (e.g. from on_mode) does not remove the origin. - m_nodes.finalize(); - return; - } + return i; + return std::nullopt; +} + +gp_Pnt2d Sketch::default_origin_pt_() const +{ gp_Pnt2d pt(0., 0.); if (m_originating_face) { const TopoDS_Shape& shape = m_originating_face->Shape(); if (shape.ShapeType() == TopAbs_WIRE) - { - const gp_Pnt center = get_shape_bbox_center(shape); - pt = to_2d(m_pln, center); - } + pt = to_2d(m_pln, get_shape_bbox_center(shape)); } - const size_t idx = m_nodes.add_new_node(pt, false, true); - Sketch_nodes::Node& n = m_nodes[idx]; - n.origin = true; - n.name = "Origin"; - m_nodes.finalize(); + return pt; +} + +gp_Pnt2d Sketch::default_origin_pt() const { return default_origin_pt_(); } + +gp_Pnt2d Sketch::origin_pt() const +{ + if (const std::optional idx = origin_node_idx_()) + return gp_Pnt2d(m_nodes[*idx].X(), m_nodes[*idx].Y()); + + return default_origin_pt_(); } +void Sketch::set_origin_pt(const gp_Pnt2d& pt) +{ + ensure_origin_node_(); + const std::optional idx = origin_node_idx_(); + EZY_ASSERT(idx.has_value()); + + Sketch_nodes::Node& n = m_nodes[*idx]; + n.SetX(pt.X()); + n.SetY(pt.Y()); + m_node_marks.sync(); +} + +void Sketch::reset_origin_pt() { set_origin_pt(default_origin_pt_()); } + gp_Pnt Sketch::to_3d_(size_t node_idx) const { return to_3d(m_pln, m_nodes[node_idx]); } gp_Pnt Sketch::to_3d_(const std::optional& node_idx) const diff --git a/src/sketch.h b/src/sketch.h index a306902..30cbb26 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -139,6 +139,13 @@ class Sketch Sketch_underlay& underlay() { return m_underlay; } [[nodiscard]] const Sketch_underlay& underlay() const { return m_underlay; } + /// Built-in origin marker position in sketch plane coordinates. + [[nodiscard]] gp_Pnt2d origin_pt() const; + /// Default origin for this sketch (plane `(0,0)` or face wire bbox center). + [[nodiscard]] gp_Pnt2d default_origin_pt() const; + void set_origin_pt(const gp_Pnt2d& pt); + void reset_origin_pt(); + static void set_add_mid_pt_edges(bool on); static bool get_add_mid_pt_edges(); static void set_edge_from_center(bool on); @@ -199,6 +206,8 @@ class Sketch bool operation_axis_suppresses_sketch_snap_() const; void update_originating_face_style(); void ensure_origin_node_(); + std::optional origin_node_idx_() const; + gp_Pnt2d default_origin_pt_() const; void json_add_length_dimension_(size_t node_a, size_t node_b, bool visible = true, std::optional flyout_offset = std::nullopt, const std::string& name = {}); diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index dc59725..e6a923b 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -1760,6 +1760,20 @@ TEST_F(Sketch_test, SketchOriginNodeSurvivesOnMode) EXPECT_NEAR(sketch.get_nodes()[*origin_idx].Y(), 3.0, Precision::Confusion()); } +TEST_F(Sketch_test, SetOriginPt) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("OriginEdit", view(), default_plane); + + sketch.set_origin_pt(gp_Pnt2d(12.0, -3.5)); + EXPECT_NEAR(sketch.origin_pt().X(), 12.0, Precision::Confusion()); + EXPECT_NEAR(sketch.origin_pt().Y(), -3.5, Precision::Confusion()); + + sketch.reset_origin_pt(); + EXPECT_NEAR(sketch.origin_pt().X(), 0.0, Precision::Confusion()); + EXPECT_NEAR(sketch.origin_pt().Y(), 0.0, Precision::Confusion()); +} + TEST_F(Sketch_test, OriginatingFaceSnapPointsCircle) { gp_Pln default_plane(gp::Origin(), gp::DZ()); From 52d6f444ad262cc46f163129642e450706744f1b Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 7 Jul 2026 10:32:52 -0600 Subject: [PATCH 2/5] WIP --- src/sketch_topo.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sketch_topo.cpp b/src/sketch_topo.cpp index 7aef2c4..a7fc66d 100644 --- a/src/sketch_topo.cpp +++ b/src/sketch_topo.cpp @@ -402,6 +402,7 @@ void Sketch_topo::update_faces() if (dfs(neighbor, start)) return true; } + return false; }; From 836da25d5cbaaa02897311d12d48097adf91ecde Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 7 Jul 2026 17:14:11 -0600 Subject: [PATCH 3/5] WIP --- docs/usage-settings.md | 1 + docs/usage-sketch.md | 13 ++-- docs/usage.md | 4 +- src/doc/sketch.md | 3 +- src/gui.cpp | 149 +++++++++++++++++++++++++++++++++----- src/gui.h | 23 +++++- src/gui_mode.cpp | 2 + src/gui_settings.cpp | 24 ++++++ src/sketch.cpp | 11 +++ src/sketch.h | 3 + src/sketch_display.cpp | 5 +- src/sketch_json.cpp | 6 ++ src/sketch_node_marks.cpp | 27 ++++--- src/sketch_node_marks.h | 2 +- src/utl_geom.cpp | 17 +++++ src/utl_geom.h | 3 + 16 files changed, 250 insertions(+), 43 deletions(-) diff --git a/docs/usage-settings.md b/docs/usage-settings.md index 57692fa..91681bc 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -176,6 +176,7 @@ String: ImGui `.ini` text for window positions and docking saved with **SaveIniS | `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**). | | `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**). | diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index dd74c56..747ab18 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -43,7 +43,7 @@ See [Sketch origin](#sketch-origin) — every sketch includes one permanent refe (sketch-origin)= ## Sketch origin -**Every sketch has exactly one Origin** — a fixed reference point on the sketch plane, shown as a red **+** marker (a **permanent node**, not placed with a tool) whenever the sketch is visible in sketch mode (and in tools such as **polar duplicate** that snap to sketch nodes). +**Every sketch has exactly one Origin** — a fixed reference point on the sketch plane, shown as a **+ inside a circle** on the **active sketch only** (a **permanent node**, not placed with a tool) whenever that sketch is visible in sketch mode (and in tools such as **polar duplicate** that snap to sketch nodes). User-placed [Add node](#add-node-tool) points use a red **+** without a circle on every visible sketch. Color is set in **Settings -> Sketch -> Origin marker color**. | | | | ---: | --- | @@ -54,13 +54,14 @@ See [Sketch origin](#sketch-origin) — every sketch includes one permanent refe | **Snapping** | Acts like any other sketch vertex: axis guides, vertex lock, and distance snap apply. Origins from **other visible sketches** are also snap targets on the current plane. | | **Cannot delete** | Unlike [Add node](#add-node-tool) points, the Origin cannot be removed from the selection. | | **Marker size** | **Settings -> Sketch -> Permanent node annotation size** (`permanent_node_anno_scale`; see [usage-settings.md](usage-settings.md#settings-file-reference)). | -| **Sketch properties** | **Sketch List -> [P]** — edit **Origin** **X** / **Y** (sketch plane coordinates) or **Reset to default**. Changes are undoable. | +| **Marker color** | **Settings -> Sketch -> Origin marker color** (`origin_marker_color`; active sketch only). | +| **Sketch properties** | **Sketch List -> [P]** — **Show origin marker**, **Position** sliders (range follows the current view) with **Set** for typed values. | **Tips** - Use the Origin as a stable anchor when you need a known center (especially on [face-derived](#create-sketch-from-planar-face-tool) sketches). - If the **+** is hard to see, increase **Permanent node annotation size** in Settings. -- Open **Sketch properties** (**[P]** on the sketch row) to set **Origin** **X** / **Y** or **Reset to default** (plane `(0, 0)` or face bbox center). +- Open **Sketch properties** (**[P]** on the sketch row) to drag **X** / **Y** sliders (view-relative range) or use **Set** beside each slider, type a value, and press **Enter**. - While an **operational axis** is active in mirror/revolve phase, **+** markers (including Origin) are temporarily hidden so edge and face selection stays clear; they return when you **Clear axis** or leave that mode. ## Sketch snapping @@ -885,9 +886,9 @@ In both cases, Add node never leaves a **permanent edge** between two clicks the ### Permanent “+” markers -Each sketch includes a built-in **Origin** (see [Sketch origin](#sketch-origin)) — always shown as a permanent **+** when the sketch is visible. +Each sketch includes a built-in **Origin** (see [Sketch origin](#sketch-origin)) — shown as a permanent **+ inside a circle** on the **active sketch** when enabled in **Sketch properties**. -Nodes you place with **Add node** are treated as **user-placed** points. When the sketch is visible, eligible points can show the same small **+** marker in the 3D view so you can see and pick them; you can delete user-placed markers from the selection. Geometry that exists only as automatic **edge midpoints** for snapping is separate (those nodes are not shown the same way; controlled by the **Add midpoints to new linear edges** setting in Settings > Sketch, default off). +Nodes you place with **Add node** are treated as **user-placed** points. When the sketch is visible, eligible points can show a red **+** marker in the 3D view so you can see and pick them; you can delete user-placed markers from the selection. Geometry that exists only as automatic **edge midpoints** for snapping is separate (those nodes are not shown the same way; controlled by the **Add midpoints to new linear edges** setting in Settings > Sketch, default off). ### How to use @@ -941,7 +942,7 @@ The create sketch from planar face tool allows you to extract the boundary of a 3. **Sketch Created**: A new sketch is automatically created with: - The face boundary as the initial wire - The sketch plane aligned with the face plane - - A permanent **Origin** **+** marker at the **center of the face boundary's bounding box** (a fixed reference on the sketch plane) + - A permanent **Origin** marker (**cyan + inside a circle**) at the **center of the face boundary's bounding box** (a fixed reference on the sketch plane) - The sketch name set to "Sketch from face" 4. **Edit Sketch**: You can now edit the sketch using standard sketch tools (add edges, modify geometry, etc.) diff --git a/docs/usage.md b/docs/usage.md index 1419563..b0aece7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -289,14 +289,14 @@ For detailed information on creating 2D geometry, see the [2D Sketching](usage-s #### Sketch origin (permanent reference point) -Every sketch has exactly one **Origin**: a fixed point on the sketch plane, shown as a red **+** marker in sketch mode (same style as user-placed [Add node](usage-sketch.md#add-node-tool) points, but you cannot delete it). +Every sketch has exactly one **Origin**: a fixed point on the sketch plane, shown as a **+ inside a circle** on the **active sketch only** in sketch mode (distinct from red **+** [Add node](usage-sketch.md#add-node-tool) markers on visible sketches; you cannot delete the Origin). | | | | ---: | --- | | **Reference-plane sketch** | Origin at plane coordinates **(0, 0)**. | | **Sketch from planar face** | Origin at the **bounding-box center** of the extracted face boundary. | | **Sketch List** | Listed as **Origin** under **Nodes** when you expand a sketch row. | -| **Sketch properties** | **[P]** on the sketch row: **Origin** **X** / **Y** and **Reset to default**. | +| **Sketch properties** | **[P]** on the sketch row: **Show origin marker**, **Position** sliders with **Set**, for typed values. | | **Snapping** | Full vertex and axis snap; other visible sketches' origins are snap targets too. | Full details (marker size, operational-axis visibility, tips): **[Sketch origin](usage-sketch.md#sketch-origin)** in the 2D sketching guide. diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 4ac0a45..4bebb07 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -25,6 +25,7 @@ Typical uses: - The constructor **`Occt_view& view` must outlive the sketch**. The sketch holds references to `m_view` and `m_ctx` (the OCCT interactive context). - Each sketch receives a stable **`m_id`** from `Occt_view::allocate_sketch_id()`. Display names (`m_name`) may duplicate; undo deltas and file I/O key off `get_id()`. - `set_origin_pt(gp_Pnt2d)` / `reset_origin_pt()` / `origin_pt()` — user-editable origin marker (also **Sketch properties** UI). +- `show_origin_marker()` / `set_show_origin_marker(bool)` — per-sketch visibility of the origin annotation (active sketch only). - Node indices are **never compacted**. Deleted nodes become tombstones (`Node::deleted`) so undo/redo and JSON round-trips stay stable. ### Plane and coordinates @@ -61,7 +62,7 @@ Snap distance, guide mode, and guide colors live on `Sketch_nodes` (static sette Sketch (coordinator: sketch.cpp, sketch.h) | +-- Sketch_nodes vertices, snapping, outside-sketch snap points - +-- Sketch_node_marks permanent "+" markers for user-placed nodes + +-- Sketch_node_marks permanent "+" markers for user-placed nodes; origin uses cyan + with circle +-- 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 diff --git a/src/gui.cpp b/src/gui.cpp index 72bd2e2..13e0ba3 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -135,6 +135,7 @@ void GUI::render_gui() toolbar_(); dist_edit_(); angle_edit_(); + sketch_origin_set_edit_(); sketch_list_(); sketch_properties_dialog_(); shape_list_(); @@ -817,6 +818,80 @@ void GUI::hide_angle_edit() bool GUI::is_dist_or_angle_edit_active() const { return m_dist_callback != nullptr || m_angle_callback != nullptr; } +bool GUI::is_sketch_origin_set_edit_active() const { return m_sketch_origin_set_plane_idx >= 0; } + +void GUI::open_sketch_origin_set_edit_(const Sketch::sptr& sk, int plane_idx, double v_min, double v_max) +{ + EZY_ASSERT(sk); + EZY_ASSERT(plane_idx == 0 || plane_idx == 1); + + m_sketch_origin_set_sketch = sk; + m_sketch_origin_set_plane_idx = plane_idx; + m_sketch_origin_set_v_min = v_min; + m_sketch_origin_set_v_max = v_max; + m_sketch_origin_set_loc = ImGui::GetItemRectMin(); + m_sketch_origin_set_focus_pending = true; + std::snprintf(m_sketch_origin_set_text_buf.data(), m_sketch_origin_set_text_buf.size(), "%.9g", + m_sketch_origin_xy[plane_idx]); +} + +void GUI::hide_sketch_origin_set_edit(bool apply) +{ + if (m_sketch_origin_set_plane_idx < 0) + return; + + const Sketch::sptr sk = m_sketch_origin_set_sketch.lock(); + const int plane_idx = m_sketch_origin_set_plane_idx; + const double v_min = m_sketch_origin_set_v_min; + const double v_max = m_sketch_origin_set_v_max; + m_sketch_origin_set_plane_idx = -1; + m_sketch_origin_set_sketch.reset(); + + if (!sk || !apply) + return; + + float parsed{}; + if (!parse_dist_text_to_float_(m_sketch_origin_set_text_buf.data(), parsed)) + return; + + m_sketch_origin_xy[plane_idx] = std::clamp(static_cast(parsed), v_min, v_max); + sk->set_origin_pt(gp_Pnt2d(m_sketch_origin_xy[0], m_sketch_origin_xy[1])); + m_view->ctx().UpdateCurrentViewer(); +} + +void GUI::sketch_origin_set_edit_() +{ + if (m_sketch_origin_set_plane_idx < 0) + return; + + if (!m_sketch_origin_set_sketch.lock()) + { + m_sketch_origin_set_plane_idx = -1; + return; + } + + ImGui::SetNextWindowPos(m_sketch_origin_set_loc, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(120.0f, 25.0f), ImGuiCond_Once); + ImGui::Begin("OriginSetEdit##unique_id", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::SetNextItemWidth(100.0f); + if (m_sketch_origin_set_focus_pending) + { + ImGui::SetKeyboardFocusHere(0); + m_sketch_origin_set_focus_pending = false; + } + + ImGui::InputText("##origin_set_text", m_sketch_origin_set_text_buf.data(), m_sketch_origin_set_text_buf.size(), + ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific); + + if (ImGui::IsItemDeactivatedAfterEdit()) + hide_sketch_origin_set_edit(true); + + ImGui::End(); +} + void GUI::angle_edit_() { if (!m_angle_callback) @@ -1268,12 +1343,16 @@ void GUI::sketch_underlay_import_dialog_() void GUI::sketch_properties_dialog_() { if (!m_sketch_properties_open) + { + hide_sketch_origin_set_edit(false); return; + } const auto sk = m_sketch_properties_sketch.lock(); if (!sk) { m_sketch_properties_open = false; + hide_sketch_origin_set_edit(false); return; } @@ -1282,6 +1361,8 @@ void GUI::sketch_properties_dialog_() if (!ImGui::Begin("Sketch properties", &open, ImGuiWindowFlags_None)) { m_sketch_properties_open = open; + if (!open) + hide_sketch_origin_set_edit(false); ImGui::End(); return; } @@ -1300,6 +1381,7 @@ void GUI::sketch_origin_panel_settings_(const Sketch::sptr& sk) if (m_sketch_origin_panel_sketch != sk.get()) { + hide_sketch_origin_set_edit(false); m_sketch_origin_panel_sketch = sk.get(); const gp_Pnt2d o = sk->origin_pt(); m_sketch_origin_xy[0] = o.X(); @@ -1309,38 +1391,65 @@ void GUI::sketch_origin_panel_settings_(const Sketch::sptr& sk) ImGui::TextUnformatted("Origin"); ImGui::Separator(); - auto commit_origin_if_edited = [&]() + bool show_origin = sk->show_origin_marker(); + if (ImGui::Checkbox("Show origin marker", &show_origin)) { - if (!ImGui::IsItemDeactivatedAfterEdit()) - return; + sk->set_show_origin_marker(show_origin); + m_view->ctx().UpdateCurrentViewer(); + } - m_view->push_undo_snapshot(); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("When off, hides the Origin +/circle for this sketch (active sketch only). The Origin remains a snap target.", + doc_urls::k_sketch_origin); + + const ImGuiIO& io = ImGui::GetIO(); + double min_u = 0., min_v = 0., max_u = 1., max_v = 1.; + const bool have_view = + m_view->sketch_plane_view_aabb_2d(sk->get_plane(), static_cast(io.DisplaySize.x), + static_cast(io.DisplaySize.y), min_u, min_v, max_u, max_v); + if (!have_view) + { + constexpr double k_fallback = 250.0; + min_u = m_sketch_origin_xy[0] - k_fallback; + max_u = m_sketch_origin_xy[0] + k_fallback; + min_v = m_sketch_origin_xy[1] - k_fallback; + max_v = m_sketch_origin_xy[1] + k_fallback; + } + + auto apply_origin = [&]() + { sk->set_origin_pt(gp_Pnt2d(m_sketch_origin_xy[0], m_sketch_origin_xy[1])); m_view->ctx().UpdateCurrentViewer(); }; - ImGui::InputDouble("X##sketch_origin", &m_sketch_origin_xy[0], 0.0, 0.0, "%.4f"); - commit_origin_if_edited(); - ImGui::InputDouble("Y##sketch_origin", &m_sketch_origin_xy[1], 0.0, 0.0, "%.4f"); - commit_origin_if_edited(); + auto origin_axis_row = [&](const char* axis_label, const char* popup_id, int plane_idx, double v_min, double v_max) + { + ImGui::PushID(popup_id); + double* const p_value = &m_sketch_origin_xy[plane_idx]; + const bool changed = + ImGui::SliderScalar(axis_label, ImGuiDataType_Double, p_value, &v_min, &v_max, "%.4f", ImGuiSliderFlags_ClampOnInput); - const gp_Pnt2d default_o = sk->default_origin_pt(); - ImGui::TextDisabled("Default: (%.4f, %.4f)", default_o.X(), default_o.Y()); + if (changed) + apply_origin(); - if (ImGui::Button("Reset to default")) - { - m_view->push_undo_snapshot(); - sk->reset_origin_pt(); - const gp_Pnt2d o = sk->origin_pt(); - m_sketch_origin_xy[0] = o.X(); - m_sketch_origin_xy[1] = o.Y(); - m_view->ctx().UpdateCurrentViewer(); - } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + if (ImGui::SmallButton("Set")) + open_sketch_origin_set_edit_(sk, plane_idx, v_min, v_max); + ImGui::PopID(); + }; + + ImGui::TextUnformatted("Position (sketch plane, vs. current view)"); ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("Sketch plane coordinates for the permanent Origin + marker. Click ? for the user guide.", + GUI_DOC_HELP_("Sketch plane coordinates for the permanent Origin marker (+ with circle). Click ? for the user guide.", doc_urls::k_sketch_origin); + ImGui::PushID(sk.get()); + // Labels match typical sketch axes on screen: "X" drives plane Y, "Y" plane X (same as underlay center). + origin_axis_row("X", "sketch_origin_set_x", 1, min_v, max_v); + origin_axis_row("Y", "sketch_origin_set_y", 0, min_u, max_u); + ImGui::PopID(); + ImGui::Separator(); } diff --git a/src/gui.h b/src/gui.h index dbf0425..7888132 100644 --- a/src/gui.h +++ b/src/gui.h @@ -84,6 +84,8 @@ inline constexpr int k_gui_edge_dim_text_render_mode_max = k_gui_edge_dim_te inline constexpr float k_gui_permanent_node_anno_scale_min = 0.25f; inline constexpr float k_gui_permanent_node_anno_scale_max = 3.0f; inline constexpr float k_gui_permanent_node_anno_scale_default = 1.0f; + +inline constexpr float k_gui_origin_marker_color_default[3] = {0.0f, 0.75f, 1.0f}; /// Allowed range and default for `gui.view_roll_step_deg` (view roll and numpad orbit steps; must match Settings slider). inline constexpr double k_gui_view_roll_step_deg_min = 0.1; inline constexpr double k_gui_view_roll_step_deg_max = 180.0; @@ -169,6 +171,8 @@ class GUI void apply_sketch_dimensions_visibility(); /// Scale factor for permanent sketch-node '+' annotations. float permanent_node_anno_scale() const { return m_permanent_node_anno_scale; } + /// RGB color for the active sketch's origin marker (0-1 per channel). + const float* origin_marker_color_rgb() const { return m_origin_marker_color; } bool get_add_mid_pt_line_edges() const { return m_add_mid_pt_line_edges; } bool get_add_mid_pt_rect_edges() const { return m_add_mid_pt_rect_edges; } bool get_add_mid_pt_slot_edges() const { return m_add_mid_pt_slot_edges; } @@ -189,8 +193,10 @@ class GUI void set_angle_edit(float angle, std::function&& callback, const std::optional screen_coords = std::nullopt); void hide_angle_edit(); + void hide_sketch_origin_set_edit(bool apply = true); /// True when dist or angle edit is visible; Tab should be routed to on_key() instead of ImGui. bool is_dist_or_angle_edit_active() const; + bool is_sketch_origin_set_edit_active() const; void show_message(const std::string& message); void log_message(const std::string& message); void set_show_options(bool v) { m_show_options = v; } @@ -260,6 +266,8 @@ class GUI }; void dist_edit_(); void angle_edit_(); + void sketch_origin_set_edit_(); + void open_sketch_origin_set_edit_(const std::shared_ptr& sk, int plane_idx, double v_min, double v_max); void on_left_click_(const ScreenCoords& screen_coords); void sketch_list_(); void sketch_list_inspector_(const std::shared_ptr& sketch, int index, std::shared_ptr& hover_sketch, @@ -428,6 +436,8 @@ class GUI int m_edge_dim_text_render_mode = k_gui_edge_dim_text_render_mode_default; bool m_show_sketch_dimensions = true; float m_permanent_node_anno_scale = k_gui_permanent_node_anno_scale_default; + float m_origin_marker_color[3] = {k_gui_origin_marker_color_default[0], k_gui_origin_marker_color_default[1], + k_gui_origin_marker_color_default[2]}; bool m_add_mid_pt_line_edges = false; bool m_add_mid_pt_rect_edges = true; bool m_add_mid_pt_slot_edges = false; @@ -512,9 +522,16 @@ class GUI bool m_sketch_properties_open{false}; std::weak_ptr m_sketch_properties_sketch; - // Sketch origin (properties pane) - void* m_sketch_origin_panel_sketch{nullptr}; - double m_sketch_origin_xy[2]{0.0, 0.0}; + // Sketch origin value input (properties pane Set button) + std::weak_ptr m_sketch_origin_set_sketch; + int m_sketch_origin_set_plane_idx{-1}; + double m_sketch_origin_set_v_min{0.0}; + double m_sketch_origin_set_v_max{0.0}; + std::array m_sketch_origin_set_text_buf{}; + bool m_sketch_origin_set_focus_pending{false}; + ImVec2 m_sketch_origin_set_loc{}; + void* m_sketch_origin_panel_sketch{nullptr}; + double m_sketch_origin_xy[2]{0.0, 0.0}; // Sketch underlay related void* m_underlay_panel_sketch{nullptr}; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 4c8b019..beb0ed3 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -306,6 +306,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) case GLFW_KEY_ESCAPE: cancel_underlay_calib_(); m_view->cancel(Set_parent_mode::Yes); + hide_sketch_origin_set_edit(false); hide_dist_edit(); hide_angle_edit(); break; @@ -322,6 +323,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) } case GLFW_KEY_ENTER: + hide_sketch_origin_set_edit(true); hide_dist_edit(); hide_angle_edit(); m_view->on_enter(screen_coords); diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index 5fdeaa0..1be8356 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -40,6 +40,7 @@ std::string GUI::occt_view_settings_json() const {"edge_dim_arrow_orientation", m_edge_dim_arrow_orientation}, {"show_sketch_dimensions", m_show_sketch_dimensions}, {k_gui_key_permanent_node_anno_scale, m_permanent_node_anno_scale}, + {"origin_marker_color", {m_origin_marker_color[0], m_origin_marker_color[1], m_origin_marker_color[2]}}, {"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}, @@ -105,6 +106,7 @@ void GUI::save_occt_view_settings() {"edge_dim_arrow_orientation", m_edge_dim_arrow_orientation}, {"show_sketch_dimensions", m_show_sketch_dimensions}, {k_gui_key_permanent_node_anno_scale, m_permanent_node_anno_scale}, + {"origin_marker_color", {m_origin_marker_color[0], m_origin_marker_color[1], m_origin_marker_color[2]}}, {"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}, @@ -314,6 +316,14 @@ void GUI::parse_gui_panes_settings_(const std::string& content) m_edge_dim_color[i] = std::clamp(a[i].get(), 0.f, 1.f); } + if (g.contains("origin_marker_color") && g["origin_marker_color"].is_array() && g["origin_marker_color"].size() >= 3) + { + const json& a = g["origin_marker_color"]; + for (int i = 0; i < 3; ++i) + if (a[i].is_number()) + m_origin_marker_color[i] = std::clamp(a[i].get(), 0.f, 1.f); + } + if (g.contains("edge_dim_arrow_style") && g["edge_dim_arrow_style"].is_number_integer()) { const int v = g["edge_dim_arrow_style"].get(); @@ -1132,6 +1142,20 @@ void GUI::settings_() doc_urls::k_sketch_origin); } + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Origin marker color"); + ImGui::TableSetColumnIndex(1); + { + if (ImGui::ColorEdit3("##origin_marker_color", m_origin_marker_color, ImGuiColorEditFlags_Float)) + node_anno_changed = true; + + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Color for the active sketch's Origin marker (+ with circle). Click ? to open the user guide.", + doc_urls::k_sketch_origin); + } + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); diff --git a/src/sketch.cpp b/src/sketch.cpp index 90e0fb8..1635322 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -425,6 +425,17 @@ void Sketch::set_origin_pt(const gp_Pnt2d& pt) void Sketch::reset_origin_pt() { set_origin_pt(default_origin_pt_()); } +bool Sketch::show_origin_marker() const { return m_show_origin_marker; } + +void Sketch::set_show_origin_marker(bool show) +{ + if (m_show_origin_marker == show) + return; + + m_show_origin_marker = show; + m_node_marks.sync(); +} + gp_Pnt Sketch::to_3d_(size_t node_idx) const { return to_3d(m_pln, m_nodes[node_idx]); } gp_Pnt Sketch::to_3d_(const std::optional& node_idx) const diff --git a/src/sketch.h b/src/sketch.h index 30cbb26..1e310b5 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -145,6 +145,8 @@ class Sketch [[nodiscard]] gp_Pnt2d default_origin_pt() const; void set_origin_pt(const gp_Pnt2d& pt); void reset_origin_pt(); + [[nodiscard]] bool show_origin_marker() const; + void set_show_origin_marker(bool show); static void set_add_mid_pt_edges(bool on); static bool get_add_mid_pt_edges(); @@ -252,4 +254,5 @@ class Sketch Sketch_tools m_tools; Sketch_underlay m_underlay; bool m_show_faces{true}; + bool m_show_origin_marker{true}; }; diff --git a/src/sketch_display.cpp b/src/sketch_display.cpp index 3da342a..08e2b81 100644 --- a/src/sketch_display.cpp +++ b/src/sketch_display.cpp @@ -186,7 +186,10 @@ void Sketch::set_current() } sync_operation_axis_display_(); - m_node_marks.sync(); + + for (Sketch_ptr& sketch : m_view.get_sketches()) + if (sketch->is_visible()) + sketch->m_node_marks.sync(); } void Sketch::set_edge_style(Edge_style style) diff --git a/src/sketch_json.cpp b/src/sketch_json.cpp index 597ab5f..998fbc1 100644 --- a/src/sketch_json.cpp +++ b/src/sketch_json.cpp @@ -153,6 +153,9 @@ nlohmann::json Sketch_json::to_json(const Sketch& sketch, const Ezy_asset_store& j["name"] = sketch.get_name(); j["plane"] = ::to_json(sketch.m_pln); + if (!sketch.m_show_origin_marker) + j["show_origin_marker"] = false; + if (sketch.m_originating_face) { const TopoDS_Shape& shape = sketch.m_originating_face->Shape(); @@ -322,6 +325,9 @@ Sketch::sptr Sketch_json::from_json(Occt_view& view, const nlohmann::json& j) view.adopt_sketch_id(ret->m_id); } + if (j.contains("show_origin_marker") && j["show_origin_marker"].is_boolean()) + ret->m_show_origin_marker = j["show_origin_marker"].get(); + if (j["isCurrent"]) ret->set_current(); diff --git a/src/sketch_node_marks.cpp b/src/sketch_node_marks.cpp index b75b2c8..eb404a8 100644 --- a/src/sketch_node_marks.cpp +++ b/src/sketch_node_marks.cpp @@ -16,11 +16,17 @@ Sketch_node_marks::Sketch_node_marks(Sketch& sketch) { } -void Sketch_node_marks::apply_style_(AIS_Shape_ptr& shp) const +void Sketch_node_marks::apply_style_(AIS_Shape_ptr& shp, bool origin) const { // Keep permanent node markers visually stable across sketch switching. shp->SetWidth(1.25); - shp->SetColor(Quantity_NOC_RED); + if (origin) + { + const float* c = m_sketch.m_view.gui().origin_marker_color_rgb(); + shp->SetColor(Quantity_Color(c[0], c[1], c[2], Quantity_TOC_RGB)); + } + else + shp->SetColor(Quantity_NOC_RED); shp->SetTransparency(0.0); } @@ -43,7 +49,9 @@ void Sketch_node_marks::sync() for (size_t i = 0, n = m_sketch.m_nodes.size(); i < n; ++i) { const Sketch_nodes::Node& node = m_sketch.m_nodes[i]; - const bool show = m_sketch.m_visible && node.permanent && !node.deleted && show_permanent_marks && !hide_for_operation_axis; + bool show = m_sketch.m_visible && node.permanent && !node.deleted && show_permanent_marks && !hide_for_operation_axis; + if (node.origin) + show = show && m_sketch.is_current() && m_sketch.m_show_origin_marker; if (!show) { @@ -56,19 +64,20 @@ void Sketch_node_marks::sync() } const gp_Pnt2d p2(node.X(), node.Y()); - const gp_Pnt c3 = to_3d(m_sketch.m_pln, p2); - const TopoDS_Shape cross = create_plus_cross_shape(m_sketch.m_pln, c3, half_arm); + const gp_Pnt c3 = to_3d(m_sketch.m_pln, p2); + const TopoDS_Shape marker = + node.origin ? create_origin_marker_shape(m_sketch.m_pln, c3, half_arm) : create_plus_cross_shape(m_sketch.m_pln, c3, half_arm); if (m_marks[i]) { - m_marks[i]->Set(cross); - apply_style_(m_marks[i]); + m_marks[i]->Set(marker); + apply_style_(m_marks[i], node.origin); m_sketch.m_ctx.Redisplay(m_marks[i], true); } else { - Sketch_AIS_node_mark_ptr mk = new Sketch_AIS_node_mark(m_sketch, i, cross); - apply_style_(mk); + Sketch_AIS_node_mark_ptr mk = new Sketch_AIS_node_mark(m_sketch, i, marker); + apply_style_(mk, node.origin); m_marks[i] = mk; m_sketch.m_ctx.Display(mk, AIS_WireFrame, 0, false); } diff --git a/src/sketch_node_marks.h b/src/sketch_node_marks.h index 102478f..8eff4b0 100644 --- a/src/sketch_node_marks.h +++ b/src/sketch_node_marks.h @@ -22,7 +22,7 @@ class Sketch_node_marks private: friend class Sketch_delta; - void apply_style_(AIS_Shape_ptr& shp) const; + void apply_style_(AIS_Shape_ptr& shp, bool origin) const; Sketch& m_sketch; std::vector m_marks; diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index b045969..cee79fe 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -277,6 +277,23 @@ TopoDS_Wire make_circle_wire(const gp_Pln& pln, const gp_Pnt2d& center, const gp return wire_maker.Wire(); } +TopoDS_Shape create_origin_marker_shape(const gp_Pln& plane, const gp_Pnt& center_3d, double half_arm) +{ + const double circle_radius = std::max(half_arm * 1.35, Precision::Confusion() * 50.0); + const TopoDS_Shape cross = create_plus_cross_shape(plane, center_3d, half_arm); + + const gp_Pnt2d center_2d = to_2d(plane, center_3d); + const gp_Pnt2d edge_2d(center_2d.X() + circle_radius, center_2d.Y()); + const TopoDS_Wire circle_wire = make_circle_wire(plane, center_2d, edge_2d); + + TopoDS_Compound comp; + BRep_Builder bb; + bb.MakeCompound(comp); + bb.Add(comp, cross); + bb.Add(comp, circle_wire); + return comp; +} + Slot_pnts get_slot_points(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const gp_Pnt2d& pt_c) { Slot_pnts ret; diff --git a/src/utl_geom.h b/src/utl_geom.h index 4454e27..344a855 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -45,6 +45,9 @@ TopoDS_Wire create_wire_box(const gp_Pln& plane, const gp_Pnt& position, double /// Two perpendicular segments forming a + on \a plane, centered at \a center_3d, half-length \a half_arm (model units). TopoDS_Shape create_plus_cross_shape(const gp_Pln& plane, const gp_Pnt& center_3d, double half_arm); +/// Sketch origin marker: + cross with a surrounding circle on \a plane (model units). +TopoDS_Shape create_origin_marker_shape(const gp_Pln& plane, const gp_Pnt& center_3d, double half_arm); + TopoDS_Wire make_square_wire(const gp_Pln& pln, const gp_Pnt2d& center, const gp_Pnt2d& edge_midpoint); std::array square_corners(const gp_Pnt2d& center, const gp_Pnt2d& edge_midpoint); From a12ead96ef131182a2b0a0cafac276a856b549bc Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 7 Jul 2026 17:20:53 -0600 Subject: [PATCH 4/5] Consistency --- src/gui.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui.cpp b/src/gui.cpp index 13e0ba3..9d962d0 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1269,6 +1269,9 @@ void GUI::sketch_list_() { if (ImGui::SmallButton("[P]")) { + m_view->set_curr_sketch(sketch); + if (!is_sketch_mode(get_mode())) + set_mode(Mode::Sketch_inspection_mode); m_sketch_properties_sketch = sketch; m_sketch_properties_open = true; } @@ -1348,7 +1351,7 @@ void GUI::sketch_properties_dialog_() return; } - const auto sk = m_sketch_properties_sketch.lock(); + const Sketch::sptr sk = m_view->curr_sketch_shared(); if (!sk) { m_sketch_properties_open = false; @@ -1356,6 +1359,8 @@ void GUI::sketch_properties_dialog_() return; } + m_sketch_properties_sketch = sk; + ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_FirstUseEver); bool open = m_sketch_properties_open; if (!ImGui::Begin("Sketch properties", &open, ImGuiWindowFlags_None)) From 20ed7d0cf170bc8f66f34e9098a0c6c4da555e8a Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 7 Jul 2026 17:43:24 -0600 Subject: [PATCH 5/5] Sketch origin cannot be deleted. --- docs/usage-sketch.md | 8 +++---- docs/usage.md | 4 ++-- src/doc/sketch.md | 2 +- src/gui.cpp | 2 +- src/gui_occt_view.cpp | 5 ++++ src/sketch.cpp | 3 +++ src/sketch_ais.cpp | 16 +++++++++++++ src/sketch_ais.h | 3 +++ src/sketch_json.cpp | 1 + src/sketch_node_marks.cpp | 4 ++++ src/sketch_nodes.cpp | 27 +++++++++++++++++---- src/sketch_nodes.h | 4 ++++ tests/sketch_tests.cpp | 49 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 747ab18..df7f9d0 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -51,8 +51,8 @@ See [Sketch origin](#sketch-origin) — every sketch includes one permanent refe | **Location (reference plane)** | Sketch plane coordinates **(0, 0)** — the plane's built-in reference origin (XY, XZ, YZ, or offset reference planes). | | **Location (from face)** | **Center of the bounding box** of the face boundary wire when you use [Create sketch from planar face](#create-sketch-from-planar-face-tool). | | **Sketch List** | Listed as **Origin** under **Nodes** when you expand a sketch row (see [Sketch List](usage.md#sketch-list)). | -| **Snapping** | Acts like any other sketch vertex: axis guides, vertex lock, and distance snap apply. Origins from **other visible sketches** are also snap targets on the current plane. | -| **Cannot delete** | Unlike [Add node](#add-node-tool) points, the Origin cannot be removed from the selection. | +| **Snapping** | Acts like any other sketch vertex when the marker is shown: axis guides, vertex lock, and distance snap apply. Origins from **other visible sketches** (with marker shown) are also snap targets on the current plane. When **Show origin marker** is off, the Origin is not a snap target. | +| **Cannot delete** | The Origin cannot be selected or deleted. Hide it with **Show origin marker** in **Sketch properties** instead. | | **Marker size** | **Settings -> Sketch -> Permanent node annotation size** (`permanent_node_anno_scale`; see [usage-settings.md](usage-settings.md#settings-file-reference)). | | **Marker color** | **Settings -> Sketch -> Origin marker color** (`origin_marker_color`; active sketch only). | | **Sketch properties** | **Sketch List -> [P]** — **Show origin marker**, **Position** sliders (range follows the current view) with **Set** for typed values. | @@ -73,10 +73,10 @@ While you draw or place points in sketch mode, EzyCad helps you align to existin | **Snap distance** | Larger **Snap dist** values let snaps engage from farther away (screen pixels, converted to the sketch plane at the cursor). | | **Snap guides** | **Snap guide mode**: *Traditional* (local markers at guide intersections), *Fullscreen* (view-spanning axis lines), or *Both*. **Snap guide color (node)** is used when both X and Y align to the same vertex; **Snap guide color (axis)** when only one axis aligns. A separate checkbox **All co-axial nodes** (in the sketch Options panel and in Settings) enables *global* mode: when on, full horizontal and vertical guide lines + markers are shown for *all* nodes in the current sketch and all other visible sketches (the complete set of co-axial alignments). When off (default), only the closest node per active axis is annotated (classic closest-relative behavior). | | **Axis alignment** | Near a snap target, the pick can align to that point's **X** or **Y** on the sketch plane; guides show which axis is active. When **both** axes align to the **same** point, the cursor **locks to that vertex**. | -| **Sketch origin** | Each sketch's built-in **Origin** **+** (see [Sketch origin](#sketch-origin)) is always a snap target on that sketch. | +| **Sketch origin** | Each sketch's built-in **Origin** (see [Sketch origin](#sketch-origin)) is a snap target when its marker is shown. | | **Mid-point snap (Add node)** | A click near a **straight** edge (not at its ends) snaps onto the segment and **splits** it at commit time (see [Add node tool](#add-node-tool)). Separate from vertex lock. | | **Automatic splitting on edge intersections** | When you add a new straight (linear) edge using the Line Edge tool or Multi-Line Edge tool, if it crosses or touches the interior of any existing straight edge, the existing edge is automatically split at the intersection point. The new edge is also subdivided into atomic segments where needed. The same splitting occurs when an endpoint of the new edge snaps to the midpoint of an existing edge. When you add an **arc segment**, existing straight and arc edges are split at interior crossings, and the new arc is subdivided at intersection points on its interior. This produces correct T-junctions, crossings, and cleanly divided faces from a single sketch. | -| **Other visible sketches** | Nodes from **other visible sketches** are projected onto the current sketch plane and act as snap targets (same distance rules). Each of those sketches contributes its **Origin** as well as any user-placed nodes. Useful for multi-sketch layouts and tools such as **polar duplicate** that pick sketch points. | +| **Other visible sketches** | Nodes from **other visible sketches** are projected onto the current sketch plane and act as snap targets (same distance rules). Each visible sketch contributes its **Origin** when that sketch's marker is shown, plus any user-placed nodes. Useful for multi-sketch layouts and tools such as **polar duplicate** that pick sketch points. | | **Operational axis mode** | While an operational axis is **defined** and **Operational axis** mode is active (mirror/revolve phase), sketch snap and permanent **+** node markers are **suppressed** so edge and face selection stays clear. Normal snapping applies again after **Clear axis** or when you leave the tool. Axis placement (before the axis exists) still uses snap. | **Angle constraint:** When a line or add-node rubber band has an active angle constraint, vertex and axis snap may be disabled or relaxed so the typed angle stays exact (see each tool's section). diff --git a/docs/usage.md b/docs/usage.md index b0aece7..d7a6574 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -297,7 +297,7 @@ Every sketch has exactly one **Origin**: a fixed point on the sketch plane, show | **Sketch from planar face** | Origin at the **bounding-box center** of the extracted face boundary. | | **Sketch List** | Listed as **Origin** under **Nodes** when you expand a sketch row. | | **Sketch properties** | **[P]** on the sketch row: **Show origin marker**, **Position** sliders with **Set**, for typed values. | -| **Snapping** | Full vertex and axis snap; other visible sketches' origins are snap targets too. | +| **Snapping** | Full vertex and axis snap when the marker is shown; other visible sketches' origins snap when their marker is shown. | Full details (marker size, operational-axis visibility, tips): **[Sketch origin](usage-sketch.md#sketch-origin)** in the 2D sketching guide. @@ -308,7 +308,7 @@ See the **[2D Sketching guide](usage-sketch.md)** for full documentation of sket | | | | ---: | --- | | **Vertex snap** | Lock to an existing corner when horizontal and vertical axis guides both align to the same point. | -| **Sketch origin** | Every sketch's built-in **Origin** **+** is a snap target (see [Sketch origin](usage-sketch.md#sketch-origin)). | +| **Sketch origin** | Every sketch's built-in **Origin** is a snap target when its marker is shown (see [Sketch origin](usage-sketch.md#sketch-origin)). | | **Mid-point snap** | With **Add node**, a click near a **straight** edge (but not at its ends) snaps onto the segment; EzyCad places a new vertex there and **splits** the edge into two. You do not need to hit the line exactly. | | **Edge midpoint** | Straight edges often expose a geometric **midpoint** as a snap target while drawing; that is separate from mid-point snap and from user-placed **+** nodes. | diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 4bebb07..0a411bf 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -25,7 +25,7 @@ Typical uses: - The constructor **`Occt_view& view` must outlive the sketch**. The sketch holds references to `m_view` and `m_ctx` (the OCCT interactive context). - Each sketch receives a stable **`m_id`** from `Occt_view::allocate_sketch_id()`. Display names (`m_name`) may duplicate; undo deltas and file I/O key off `get_id()`. - `set_origin_pt(gp_Pnt2d)` / `reset_origin_pt()` / `origin_pt()` — user-editable origin marker (also **Sketch properties** UI). -- `show_origin_marker()` / `set_show_origin_marker(bool)` — per-sketch visibility of the origin annotation (active sketch only). +- `show_origin_marker()` / `set_show_origin_marker(bool)` — per-sketch visibility of the origin annotation (active sketch only); when off, origin is excluded from snap. - Node indices are **never compacted**. Deleted nodes become tombstones (`Node::deleted`) so undo/redo and JSON round-trips stay stable. ### Plane and coordinates diff --git a/src/gui.cpp b/src/gui.cpp index 9d962d0..b60c14f 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1404,7 +1404,7 @@ void GUI::sketch_origin_panel_settings_(const Sketch::sptr& sk) } ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("When off, hides the Origin +/circle for this sketch (active sketch only). The Origin remains a snap target.", + GUI_DOC_HELP_("When off, hides the Origin +/circle for this sketch (active sketch only) and removes it from snap targets.", doc_urls::k_sketch_origin); const ImGuiIO& io = ImGui::GetIO(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 8e96c17..21af197 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -50,6 +50,7 @@ #include "shp.h" #include "shp_create.h" #include "sketch.h" +#include "sketch_ais.h" #include "sketch_json.h" #include "utl_types.h" #include "utl.h" @@ -1150,6 +1151,10 @@ void Occt_view::delete_shapes(std::vector to_delete) if (to_delete.empty()) return; + std::erase_if(to_delete, [](const AIS_Shape_ptr& shp) { return is_sketch_origin_node_mark(shp.get()); }); + if (to_delete.empty()) + return; + push_undo_snapshot(); remove_selected_length_dimensions_from_sketches_(); delete_(to_delete); diff --git a/src/sketch.cpp b/src/sketch.cpp index 1635322..074f443 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -433,7 +433,10 @@ void Sketch::set_show_origin_marker(bool show) return; m_show_origin_marker = show; + m_nodes.set_origin_snap_enabled(show); m_node_marks.sync(); + if (Sketch* cur = m_view.current_sketch_if_any()) + cur->set_current(); } gp_Pnt Sketch::to_3d_(size_t node_idx) const { return to_3d(m_pln, m_nodes[node_idx]); } diff --git a/src/sketch_ais.cpp b/src/sketch_ais.cpp index 059c788..6bf901e 100644 --- a/src/sketch_ais.cpp +++ b/src/sketch_ais.cpp @@ -34,3 +34,19 @@ bool try_remove_sketch_permanent_node_mark(AIS_Shape* shp) return false; } + +bool is_sketch_origin_node_mark(const AIS_Shape* shp) +{ + if (!shp) + return false; + + const auto* mark = dynamic_cast(shp); + if (!mark) + return false; + + const size_t i = mark->node_idx; + if (i >= mark->owner_sketch.get_nodes().size()) + return false; + + return mark->owner_sketch.get_nodes()[i].origin; +} diff --git a/src/sketch_ais.h b/src/sketch_ais.h index ed07fce..e6dbc5c 100644 --- a/src/sketch_ais.h +++ b/src/sketch_ais.h @@ -36,3 +36,6 @@ struct Sketch_face_shp : public AIS_Shape /// If `shp` is a permanent sketch node mark, removes it from its owner sketch. bool try_remove_sketch_permanent_node_mark(AIS_Shape* shp); + +/// True when `shp` is the non-deletable sketch origin node mark. +[[nodiscard]] bool is_sketch_origin_node_mark(const AIS_Shape* shp); diff --git a/src/sketch_json.cpp b/src/sketch_json.cpp index 998fbc1..d62e01a 100644 --- a/src/sketch_json.cpp +++ b/src/sketch_json.cpp @@ -327,6 +327,7 @@ Sketch::sptr Sketch_json::from_json(Occt_view& view, const nlohmann::json& j) if (j.contains("show_origin_marker") && j["show_origin_marker"].is_boolean()) ret->m_show_origin_marker = j["show_origin_marker"].get(); + ret->m_nodes.set_origin_snap_enabled(ret->m_show_origin_marker); if (j["isCurrent"]) ret->set_current(); diff --git a/src/sketch_node_marks.cpp b/src/sketch_node_marks.cpp index eb404a8..22020e8 100644 --- a/src/sketch_node_marks.cpp +++ b/src/sketch_node_marks.cpp @@ -73,6 +73,8 @@ void Sketch_node_marks::sync() m_marks[i]->Set(marker); apply_style_(m_marks[i], node.origin); m_sketch.m_ctx.Redisplay(m_marks[i], true); + if (node.origin) + m_sketch.m_ctx.Deactivate(m_marks[i]); } else { @@ -80,6 +82,8 @@ void Sketch_node_marks::sync() apply_style_(mk, node.origin); m_marks[i] = mk; m_sketch.m_ctx.Display(mk, AIS_WireFrame, 0, false); + if (node.origin) + m_sketch.m_ctx.Deactivate(mk); } } } diff --git a/src/sketch_nodes.cpp b/src/sketch_nodes.cpp index 46b663d..e5dddb6 100644 --- a/src/sketch_nodes.cpp +++ b/src/sketch_nodes.cpp @@ -123,6 +123,9 @@ class Sketch_nodes::Impl void clear_outside_snap_pnts(); void add_outside_snap_pnt(const gp_Pnt& pt3d); + void set_origin_snap_enabled(bool enabled) { m_origin_snap_enabled = enabled; } + [[nodiscard]] bool origin_snap_enabled() const { return m_origin_snap_enabled; } + private: Sketch_nodes* m_owner; std::vector m_nodes; @@ -141,6 +144,16 @@ class Sketch_nodes::Impl Occt_view& m_view; AIS_InteractiveContext& m_ctx; const gp_Pln m_pln; + bool m_origin_snap_enabled{true}; + + [[nodiscard]] bool node_snap_eligible_(const Node& n) const + { + if (n.deleted) + return false; + if (n.origin && !m_origin_snap_enabled) + return false; + return true; + } /// World-space snap radius at `pt` (same convention as `try_get_node_idx_snap` / `try_pick_existing_node`). double snap_radius_world_(const gp_Pnt2d& pt) const; @@ -204,7 +217,7 @@ std::optional Sketch_nodes::Impl::try_pick_existing_node(const ScreenCoo double best_sq = std::numeric_limits::max(); for (size_t idx = 0, num = m_nodes.size(); idx < num; ++idx) { - if (m_nodes[idx].deleted) + if (!node_snap_eligible_(m_nodes[idx]) || m_nodes[idx].origin) continue; const double sq = m_nodes[idx].SquareDistance(pt); @@ -299,7 +312,7 @@ std::optional Sketch_nodes::Impl::get_node(const ScreenCoords& screen_co void Sketch_nodes::Impl::get_snap_pts_3d(std::vector& out) { for (const Node& n : m_nodes) - if (!n.deleted) + if (node_snap_eligible_(n)) out.push_back(to_3d(m_pln, n)); } @@ -588,7 +601,7 @@ std::optional Sketch_nodes::Impl::try_get_node_idx_snap( for (size_t nd_idx = 0, num = m_nodes.size(); nd_idx < num; ++nd_idx) { - if (m_nodes[nd_idx].deleted) + if (!node_snap_eligible_(m_nodes[nd_idx])) continue; if (std::find(to_exclude.begin(), to_exclude.end(), nd_idx) != to_exclude.end()) @@ -646,7 +659,7 @@ std::optional Sketch_nodes::Impl::try_get_node_idx_snap( std::vector matches; for (const auto& n : m_nodes) { - if (n.deleted) + if (!node_snap_eligible_(n)) continue; double axis_diff = std::fabs(guide_val - n.XY().Coord(axis_idx + 1)); @@ -889,4 +902,8 @@ float Sketch_nodes::get_snap_guide_line_width() { return s_snap_guide_line_width void Sketch_nodes::set_annotate_all_coaxial_nodes(bool enable) { s_annotate_all_coaxial_nodes = enable; } -bool Sketch_nodes::get_annotate_all_coaxial_nodes() { return s_annotate_all_coaxial_nodes; } \ No newline at end of file +bool Sketch_nodes::get_annotate_all_coaxial_nodes() { return s_annotate_all_coaxial_nodes; } + +void Sketch_nodes::set_origin_snap_enabled(bool enabled) { m_impl->set_origin_snap_enabled(enabled); } + +bool Sketch_nodes::origin_snap_enabled() const { return m_impl->origin_snap_enabled(); } \ No newline at end of file diff --git a/src/sketch_nodes.h b/src/sketch_nodes.h index a4a0171..4088671 100644 --- a/src/sketch_nodes.h +++ b/src/sketch_nodes.h @@ -56,6 +56,10 @@ class Sketch_nodes void get_snap_pts_3d(std::vector& out); void hide_snap_annos(); + /// When false, the sketch origin node is omitted from snap targets (see `Sketch::show_origin_marker`). + void set_origin_snap_enabled(bool enabled); + [[nodiscard]] bool origin_snap_enabled() const; + Node& operator[](size_t idx); const Node& operator[](size_t idx) const; Node& operator[](const std::optional idx); diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index e6a923b..94036cb 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -15,6 +15,7 @@ #include "gui.h" #include "gui_occt_view.h" #include "sketch.h" +#include "sketch_ais.h" #include "sketch_delta.h" #include "sketch_json.h" #include "sketch_edge.h" @@ -116,6 +117,7 @@ class Sketch_access /// Used for regression tests of specific .ezy cases (e.g. bridge + hole topologies). static void sketch_json_add_linear_edge_(Sketch& sketch, size_t idx_a, size_t idx_b, std::optional idx_mid = std::nullopt); static void set_operation_axis_(Sketch& sketch, const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b); + static void remove_permanent_node_mark_(Sketch& sketch, size_t node_idx); }; void Sketch_access::add_edge_(Sketch& sketch, const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b) @@ -241,6 +243,12 @@ void Sketch_access::set_operation_axis_(Sketch& sketch, const gp_Pnt2d& pt_a, co sketch.sketch_json_set_operation_axis_(pt_a, pt_b); } +void Sketch_access::remove_permanent_node_mark_(Sketch& sketch, size_t node_idx) +{ + Sketch_AIS_node_mark mark(sketch, node_idx, TopoDS_Shape()); + sketch.remove_permanent_node_mark(mark); +} + class View_access { public: @@ -1774,6 +1782,47 @@ TEST_F(Sketch_test, SetOriginPt) EXPECT_NEAR(sketch.origin_pt().Y(), 0.0, Precision::Confusion()); } +TEST_F(Sketch_test, OriginNodeNotDeletable) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("OriginNoDelete", view(), default_plane); + + std::optional origin_idx; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + if (!sketch.get_nodes()[i].deleted && sketch.get_nodes()[i].origin) + { + origin_idx = i; + break; + } + } + + ASSERT_TRUE(origin_idx.has_value()); + Sketch_access::remove_permanent_node_mark_(sketch, *origin_idx); + EXPECT_FALSE(sketch.get_nodes()[*origin_idx].deleted); +} + +TEST_F(Sketch_test, HiddenOriginExcludedFromSnap) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("HiddenOriginSnap", view(), default_plane); + + std::vector pts; + sketch.get_nodes().get_snap_pts_3d(pts); + ASSERT_EQ(pts.size(), 1u); + + sketch.set_show_origin_marker(false); + EXPECT_FALSE(sketch.get_nodes().origin_snap_enabled()); + + pts.clear(); + sketch.get_nodes().get_snap_pts_3d(pts); + EXPECT_TRUE(pts.empty()); + + gp_Pnt2d near_origin(0.01, 0.01); + std::optional snap_idx = sketch.get_nodes().try_get_node_idx_snap(near_origin); + EXPECT_FALSE(snap_idx.has_value()); +} + TEST_F(Sketch_test, OriginatingFaceSnapPointsCircle) { gp_Pln default_plane(gp::Origin(), gp::DZ());