diff --git a/CHANGELOG.md b/CHANGELOG.md index eead0e4..4493651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Sketch / 2D Topology +- **Sketch origin:** Every sketch has a permanent **Origin** **+** marker at plane `(0, 0)` (reference-plane sketches) or at the bounding-box center of the face wire when created from a planar face. The origin is a snap target, is saved in project JSON (`origin` on the node), and cannot be deleted. User guide: dedicated [Sketch origin](docs/usage-sketch.md#sketch-origin) section (also linked from [Sketch List](docs/usage.md#sketch-list) and settings docs). - **Arc segments:** Each arc is a single sketch edge (not two chained edges). New arcs split existing straight and arc edges at interior crossings; arcs are subdivided when crossed. Face walking uses arc tangents at endpoints. The Arc Segment tool shows a line rubber-band after the first click and an arc preview after the second; optional **Add midpoint nodes** in the Arc tool Options adds a geometric-mid snap node (same setting as line edges). Documented in [usage-sketch.md](docs/usage-sketch.md#arc-segment-creation-tool). **Known open bug:** slot + circle intersection splitting — see [bugs.md](docs/bugs.md). - For the **Add line edge** and **Add multi-line edge** tools, a new setting **"Add midpoints to new linear edges"** (Settings > Sketch; also exposed in the tool Options pane) controls whether automatic midpoint nodes are created on new straight edges. Default is **off** (no midpoints), so new edges do not get mid snap targets unless the option is enabled. Existing edges with mids are unaffected; JSON load/save supports edges without mids for compatibility. - **Add line edge** Options: **Place from center** — first click sets the edge midpoint; second click or Tab length uses the **full** edge length (symmetric placement). Documented in the user guide with **?** help links in the Options panel. diff --git a/docs/usage-settings.md b/docs/usage-settings.md index e312a22..57692fa 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -59,7 +59,7 @@ Between those, the pane has **six** collapsible sections. Expand a section to se - **Arrow style** — *Standard*, *Sharp*, *Wide*, or *3D shaded* - **Arrow orientation** — *Automatic*, *Internal*, or *External* - **Show sketch dimensions** — global on/off for length dimensions on all sketches (tool mode may still limit which sketch shows dims when on) - - **Permanent node annotation size**, **Add midpoints to new linear edges** (default off; only affects Line Edge and Multi-Line Edge tools), **Underlay highlight color**, **Snap guide color (node)**, **Snap guide color (axis)**, **Snap guide line width** (slider **0.5** to **8.0**; default **1.0**), **Snap guide mode**, **All co-axial nodes** (directly under **Sketch**, not inside **Dimensions**) + - **Permanent node annotation size** (scale for **+** markers on the sketch **Origin** and [Add node](usage-sketch.md#add-node-tool) points; see [Sketch origin](usage-sketch.md#sketch-origin)), **Add midpoints to new linear edges** (default off; only affects Line Edge and Multi-Line Edge tools), **Underlay highlight color**, **Snap guide color (node)**, **Snap guide color (axis)**, **Snap guide line width** (slider **0.5** to **8.0**; default **1.0**), **Snap guide mode**, **All co-axial nodes** (directly under **Sketch**, not inside **Dimensions**) 6. **Startup project** — **Desktop only:** **Load last opened on startup** (checkbox, with **?**), then **Last opened path:** … or **(No path saved yet.)** Then **Save current as startup project**, **Clear saved startup** (with **?**). **WebAssembly:** no load-last row; only the two buttons and **?**. See [Startup project](#startup-project). @@ -175,7 +175,7 @@ String: ImGui `.ini` text for window positions and docking saved with **SaveIniS | `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 **+** node markers in sketch mode (**0.25** to **3.0**; default **1.0**). | +| `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**). | | `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 b4ee513..3ea6d63 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -4,20 +4,21 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main ## Table of Contents 1. [Sketching (2D)](#sketching-2d) -2. [Sketch snapping](#sketch-snapping) -3. [Hotkeys](#hotkeys) -4. [Line Edge Creation Tools](#line-edge-creation-tools) -5. [Multi-Line Edge Tool](#multi-line-edge-tool) -6. [Circle Creation Tools](#circle-creation-tools) -7. [Circle Creation Workflow](#circle-creation-workflow) -8. [Arc Segment Creation Tool](#arc-segment-creation-tool) -9. [Rectangle and Square Creation Tools](#rectangle-and-square-creation-tools) -10. [Slot Creation Tool](#slot-creation-tool) -11. [Operation Axis Tool](#operation-axis-tool) -12. [Dimension Tool](#dimension-tool) -13. [Add Node Tool](#add-node-tool) -14. [Create Sketch from Planar Face Tool](#create-sketch-from-planar-face-tool) -15. [Image underlay](#image-underlay) +2. [Sketch origin](#sketch-origin) +3. [Sketch snapping](#sketch-snapping) +4. [Hotkeys](#hotkeys) +5. [Line Edge Creation Tools](#line-edge-creation-tools) +6. [Multi-Line Edge Tool](#multi-line-edge-tool) +7. [Circle Creation Tools](#circle-creation-tools) +8. [Circle Creation Workflow](#circle-creation-workflow) +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) --- @@ -37,6 +38,29 @@ This guide covers all 2D sketching tools and operations in EzyCad. For the main - ![Operation Axis Tool](res/icons/Sketcher_MirrorSketch.png) [Operational axis](#operation-axis-tool) - Define a reference axis, then use the **Mirror** and **Revolve** controls (in the Options panel) to mirror selected sketch edges or revolve edges/faces into 3D solids. - ![Create Sketch from Planar Face Tool](res/icons/Macro_FaceToSketch_48.png) [Create sketch from planar face](#create-sketch-from-planar-face-tool) +See [Sketch origin](#sketch-origin) — every sketch includes one permanent reference point. + +(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). + +| | | +| ---: | --- | +| **When created** | Added automatically when the sketch is created; you do not place it with a tool. | +| **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. | +| **Marker size** | **Settings -> Sketch -> Permanent node annotation size** (`permanent_node_anno_scale`; see [usage-settings.md](usage-settings.md#settings-file-reference)). | + +**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. +- 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 While you draw or place points in sketch mode, EzyCad helps you align to existing geometry. **Snap dist**, **Snap guide mode**, and **All co-axial nodes** are in the Options panel; **Snap guide color**, **Snap guide line width**, and the same mode/co-axial toggles are in **Settings -> Sketch** (see [usage-settings.md](usage-settings.md#sketch-tools)). @@ -48,14 +72,14 @@ While you draw or place points in sketch mode, EzyCad helps you align to existin | **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**. | | **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). 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 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. | | **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). **Tips:** - For precise corners, approach a vertex until both horizontal and vertical guides appear, then click. -- **Edge midpoints** (for straight edges) are optional snap targets. By default (see Settings > Sketch), no midpoint nodes are created for new linear edges from the Line or Multi-Line tools. When enabled, they are snap targets but do not show **+** markers and are not listed under **Nodes** in the [Sketch List](usage.md#sketch-list) (that list is user-placed points only). +- **Edge midpoints** (for straight edges) are optional snap targets. By default (see Settings > Sketch), no midpoint nodes are created for new linear edges from the Line or Multi-Line tools. When enabled, they are snap targets but do not show **+** markers and are not listed under **Nodes** in the [Sketch List](usage.md#sketch-list). That list shows the sketch **Origin** plus **user-placed** [Add node](#add-node-tool) points only — not every internal topology vertex or automatic edge midpoint. - Adding a new straight edge that intersects existing straight edges (or snaps to their midpoints) automatically splits the intersected edges. This is the main way to divide a closed shape (rectangle, square, slot, or freehand closed profile) into multiple separate faces for individual extrusion or other operations. No manual "split" command is needed. The resulting faces remain valid even if you add the splitting edge before or after the outer shape. ## Hotkeys @@ -858,7 +882,9 @@ In both cases, Add node never leaves a **permanent edge** between two clicks the ### Permanent “+” markers -Nodes you place with **Add node** are treated as **user-placed** points. When the sketch is visible, eligible points can show a small **+** marker in the 3D view so you can see and pick them. 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). +Each sketch includes a built-in **Origin** (see [Sketch origin](#sketch-origin)) — always shown as a permanent **+** when the sketch is visible. + +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). ### How to use @@ -912,6 +938,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) - 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 9f2bf29..569741e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -98,7 +98,7 @@ Each row is laid out left to right: When expanded, the row shows: - **Dimensions** - Table of length dimensions: visibility, editable name, and **offset** (label distance from the edge; **0** = automatic). Hovering a row highlights that dimension in the 3D view (color from **Settings -> View presentation -> Element hover color**). -- **Nodes**, **Edges**, **Faces** - Collapsible lists of element labels for inspection. **Nodes** lists **user-placed** points only (the ones with **+** markers in sketch mode), not every internal topology vertex or automatic edge midpoint. **Edges** and **Faces** use default labels (`E0`, `F0`, ...) or saved names where set. Dimension names are editable in the table above; node/edge/face names in these lists are read-only labels for reference. +- **Nodes**, **Edges**, **Faces** - Collapsible lists of element labels for inspection. **Nodes** lists the sketch **Origin** (always present; see [Sketch origin](usage-sketch.md#sketch-origin)) plus **user-placed** points from the Add node tool (the ones with **+** markers in sketch mode). It does not list every internal topology vertex or automatic edge midpoint. **Edges** and **Faces** use default labels (`E0`, `F0`, ...) or saved names where set. Dimension names are editable in the table above; node/edge/face names in these lists are read-only labels for reference. The window can be closed with its close button; use **View -> Sketch List** again to show it. @@ -285,7 +285,7 @@ For detailed information on creating 2D geometry, see the [2D Sketching](usage-s ### 2D Sketching -See the **[2D Sketching guide](usage-sketch.md)** for full documentation of sketch tools: **add node** (points and edge splits), line and multi-line edges, circles, arcs, rectangles, squares, slots, **operation axis** (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. +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: diff --git a/src/doc/sketch.md b/src/doc/sketch.md index 7a643b8..597527f 100644 --- a/src/doc/sketch.md +++ b/src/doc/sketch.md @@ -24,6 +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. - Node indices are **never compacted**. Deleted nodes become tombstones (`Node::deleted`) so undo/redo and JSON round-trips stay stable. ### Plane and coordinates @@ -87,7 +88,7 @@ Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln); Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln, const TopoDS_Wire& outer_wire); ``` -The wire overload creates a sketch **from a planar face**; `m_originating_face` is displayed and its vertices contribute to snap targets. +The wire overload creates a sketch **from a planar face**; `m_originating_face` is displayed and its vertices contribute to snap targets. `ensure_origin_node_()` places the origin at the wire bounding-box center (or plane `(0,0)` for plane-only sketches); call `m_nodes.finalize()` so `cancel_elm()` / `on_mode()` does not roll it back. ### Input routing (from UI / `Occt_view`) diff --git a/src/gui.h b/src/gui.h index 8bde731..f377fb2 100644 --- a/src/gui.h +++ b/src/gui.h @@ -105,6 +105,7 @@ namespace doc_urls inline constexpr const char* k_view_roll = "https://ezycad.readthedocs.io/en/latest/usage.html#view-roll"; inline constexpr const char* k_view_navigation = "https://ezycad.readthedocs.io/en/latest/usage.html#view-navigation"; inline constexpr const char* k_sketch_snapping = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-snapping"; +inline constexpr const char* k_sketch_origin = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-origin"; inline constexpr const char* k_line_edge_midpoint_nodes = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#line-edge-option-add-midpoint-nodes"; inline constexpr const char* k_line_edge_place_from_center = diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index a1129c4..5fdeaa0 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -1128,8 +1128,8 @@ void GUI::settings_() } ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("Scale for permanent '+' node markers in sketch mode. Click ? to open the user guide.", - doc_urls::k_add_node_tool); + GUI_DOC_HELP_("Scale for '+' markers on the sketch Origin and Add node points. Click ? to open the user guide.", + doc_urls::k_sketch_origin); } ImGui::TableNextRow(); diff --git a/src/sketch.cpp b/src/sketch.cpp index d841d75..4bbc238 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -33,6 +33,7 @@ Sketch::Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln) , m_tools(*this) , m_underlay(view.ctx()) { + ensure_origin_node_(); } Sketch::Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln, const TopoDS_Wire& outer_wire) @@ -52,6 +53,7 @@ Sketch::Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln, cons m_originating_face = new AIS_Shape(outer_wire); m_ctx.Display(m_originating_face, true); update_originating_face_style(); + ensure_origin_node_(); } Sketch::~Sketch() @@ -233,7 +235,12 @@ std::vector Sketch::inspector_node_labels() const const Sketch_nodes::Node& n = m_nodes[i]; if (n.permanent && !n.deleted) { - std::string lbl = n.name.empty() ? ("N" + std::to_string(i)) : n.name; + std::string lbl; + if (n.origin) + lbl = n.name.empty() ? "Origin" : n.name; + else + lbl = n.name.empty() ? ("N" + std::to_string(i)) : n.name; + labels.push_back(std::move(lbl)); } } @@ -355,6 +362,34 @@ void Sketch::get_originating_face_snp_pts_3d_(std::vector& out) append(out, snp_pts); } +void Sketch::ensure_origin_node_() +{ + 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; + } + + 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); + } + } + + 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(); +} + 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 @@ -409,6 +444,9 @@ void Sketch::remove_permanent_node_mark(Sketch_AIS_node_mark& mark) EZY_ASSERT(&mark.owner_sketch == this); const size_t i = mark.node_idx; EZY_ASSERT(i < m_nodes.size()); + if (m_nodes[i].origin) + return; + m_nodes[i].deleted = true; remove_length_dimensions_referencing_node_(i); m_node_marks.remove_at(i); diff --git a/src/sketch.h b/src/sketch.h index 7c36e63..a306902 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -198,6 +198,7 @@ class Sketch bool show_operation_axis_() const; bool operation_axis_suppresses_sketch_snap_() const; void update_originating_face_style(); + void ensure_origin_node_(); 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/src/sketch_display.cpp b/src/sketch_display.cpp index 5028482..3da342a 100644 --- a/src/sketch_display.cpp +++ b/src/sketch_display.cpp @@ -186,6 +186,7 @@ void Sketch::set_current() } sync_operation_axis_display_(); + 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 3397410..597ab5f 100644 --- a/src/sketch_json.cpp +++ b/src/sketch_json.cpp @@ -46,6 +46,9 @@ json node_to_json_(const Sketch_nodes::Node& nd) if (nd.permanent) o["permanent"] = true; + if (nd.origin) + o["origin"] = true; + if (!nd.name.empty()) o["name"] = nd.name; @@ -63,7 +66,7 @@ void Sketch_json::load_nodes_(Sketch& sketch, const json& nodes_json) const json& el = nodes_json[i]; if (el.is_null()) { - sketch.get_nodes().set_node(i, gp_Pnt2d(0., 0.), true, false, false, ""); + sketch.get_nodes().set_node(i, gp_Pnt2d(0., 0.), true, false, false, false, ""); continue; } @@ -71,7 +74,8 @@ void Sketch_json::load_nodes_(Sketch& sketch, const json& nodes_json) const gp_Pnt2d pt = ::from_json_pnt2d(el); const bool midpoint = el.contains("midpoint") && el["midpoint"].is_boolean() && el["midpoint"].get(); const bool permanent = el.contains("permanent") && el["permanent"].is_boolean() && el["permanent"].get(); - sketch.get_nodes().set_node(i, pt, false, midpoint, permanent, + const bool origin = el.contains("origin") && el["origin"].is_boolean() && el["origin"].get(); + sketch.get_nodes().set_node(i, pt, false, midpoint, permanent, origin, el.contains("name") && el["name"].is_string() ? el["name"].get() : ""); } } @@ -321,6 +325,8 @@ Sketch::sptr Sketch_json::from_json(Occt_view& view, const nlohmann::json& j) if (j["isCurrent"]) ret->set_current(); + ret->ensure_origin_node_(); + return ret; } diff --git a/src/sketch_nodes.cpp b/src/sketch_nodes.cpp index 938ee69..46b663d 100644 --- a/src/sketch_nodes.cpp +++ b/src/sketch_nodes.cpp @@ -112,8 +112,10 @@ class Sketch_nodes::Impl size_t size() const; void resize(size_t count); - void set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, const std::string& name); - void restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, const std::string& name); + void set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, + const std::string& name); + void restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, + const std::string& name); void finalize(); void cancel(); @@ -724,7 +726,7 @@ size_t Sketch_nodes::Impl::size() const { return m_nodes.size(); } void Sketch_nodes::Impl::resize(size_t count) { m_nodes.assign(count, Node{}); } -void Sketch_nodes::Impl::set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, +void Sketch_nodes::Impl::set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, const std::string& name) { EZY_ASSERT(idx < m_nodes.size()); @@ -734,16 +736,17 @@ void Sketch_nodes::Impl::set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, n.deleted = deleted; n.midpoint = midpoint; n.permanent = permanent; + n.origin = origin; n.name = name; } void Sketch_nodes::Impl::restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, - const std::string& name) + bool origin, const std::string& name) { if (idx >= size()) resize(idx + 1); - set_node(idx, pt, deleted, midpoint, permanent, name); + set_node(idx, pt, deleted, midpoint, permanent, origin, name); } void Sketch_nodes::Impl::finalize() { m_prev_num_nodes = m_nodes.size(); } @@ -827,20 +830,20 @@ size_t Sketch_nodes::size() const { return m_impl->size(); } void Sketch_nodes::resize(size_t count) { m_impl->resize(count); } -void Sketch_nodes::set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, +void Sketch_nodes::set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, const std::string& name) { - m_impl->set_node(idx, pt, deleted, midpoint, permanent, name); + m_impl->set_node(idx, pt, deleted, midpoint, permanent, origin, name); } void Sketch_nodes::finalize() { m_impl->finalize(); } void Sketch_nodes::cancel() { m_impl->cancel(); } -void Sketch_nodes::restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, +void Sketch_nodes::restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, const std::string& name) { - m_impl->restore_node_at(idx, pt, deleted, midpoint, permanent, name); + m_impl->restore_node_at(idx, pt, deleted, midpoint, permanent, origin, name); } void Sketch_nodes::clear_outside_snap_pnts() { m_impl->clear_outside_snap_pnts(); } diff --git a/src/sketch_nodes.h b/src/sketch_nodes.h index 1b5c177..a4a0171 100644 --- a/src/sketch_nodes.h +++ b/src/sketch_nodes.h @@ -28,6 +28,7 @@ class Sketch_nodes bool midpoint = false; bool deleted = false; bool permanent = false; + bool origin = false; std::string name; }; @@ -65,7 +66,7 @@ class Sketch_nodes void cancel(); /// Restores node slot `idx` for undo/redo (appends tombstone slots as needed). - void restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, + void restore_node_at(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, const std::string& name = {}); void clear_outside_snap_pnts(); @@ -103,7 +104,8 @@ class Sketch_nodes /// Resize storage so indices `0..count-1` exist (JSON load). void resize(size_t count); /// Assign slot `idx` (used after `resize`). - void set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, const std::string& name = {}); + void set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, bool origin, + const std::string& name = {}); class Impl; std::unique_ptr m_impl; diff --git a/src/sketch_topo.cpp b/src/sketch_topo.cpp index e1654c0..7aef2c4 100644 --- a/src/sketch_topo.cpp +++ b/src/sketch_topo.cpp @@ -553,7 +553,7 @@ void Sketch_topo::update_faces() // Nodes become unused when all edges that reference them are removed. // Permanent add-node points are never auto-tombstoned when unused; user delete sets `deleted` and it stays. for (size_t idx = 0, num = m_sketch.m_nodes.size(); idx < num; ++idx) - if (!m_sketch.m_nodes[idx].permanent) + if (!m_sketch.m_nodes[idx].permanent && !m_sketch.m_nodes[idx].origin) m_sketch.m_nodes[idx].deleted = !used_nodes[idx]; // Book keeping diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index 8148a54..dc59725 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -263,7 +263,9 @@ TEST_F(Sketch_test, CreateSketch) EXPECT_EQ(sketch.get_name(), "TestSketch"); EXPECT_TRUE(sketch.is_visible()); - EXPECT_TRUE(sketch.get_nodes().empty()); + EXPECT_EQ(sketch.get_nodes().size(), 1u); + EXPECT_TRUE(sketch.get_nodes()[0].origin); + EXPECT_TRUE(sketch.get_nodes()[0].permanent); EXPECT_TRUE(planes_equal(sketch.get_plane(), default_plane)); } @@ -283,7 +285,7 @@ TEST_F(Sketch_test, NodeManagement) EXPECT_TRUE(node_idx2.has_value()); EXPECT_EQ(node_idx, *node_idx2); - EXPECT_EQ(sketch.get_nodes().size(), 1); + EXPECT_EQ(sketch.get_nodes().size(), 2u); EXPECT_FALSE(sketch.get_nodes().empty()); EXPECT_TRUE(sketch.get_nodes()[node_idx].IsEqual(point, Precision::Confusion())); } @@ -305,8 +307,8 @@ TEST_F(Sketch_test, EdgeManagement) // Verify edge was created EXPECT_FALSE(sketch.get_nodes().empty()); - // With midpoints enabled in fixture (for test compatibility), 2 endpoints + 1 mid = 3 nodes. - EXPECT_EQ(sketch.get_nodes().size(), 3); + // Origin node + 2 endpoints + 1 midpoint (midpoints enabled in fixture). + EXPECT_EQ(sketch.get_nodes().size(), 4u); } // Test that adding a single edge via add_edge_ creates exactly three nodes: @@ -1251,8 +1253,8 @@ TEST_F(Sketch_test, CreateSquare) normal_nodes.push_back(n); } - EXPECT_EQ(edge_mid_point_nodes.size(), 4); - EXPECT_EQ(normal_nodes.size(), 4); + EXPECT_EQ(edge_mid_point_nodes.size(), 4u); + EXPECT_EQ(normal_nodes.size(), 5u); // 4 corners + sketch origin at center } // Test circle creation @@ -1681,6 +1683,83 @@ TEST_F(Sketch_test, OriginatingFaceSnapPointsSquare) << "Mismatch at index " << i << ": got " << snap_pts[i].X() << "," << snap_pts[i].Y(); } +TEST_F(Sketch_test, SketchOriginNodePlane) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("PlaneOrigin", view(), default_plane); + + std::optional origin_idx; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (!n.deleted && n.origin) + { + origin_idx = i; + break; + } + } + + ASSERT_TRUE(origin_idx.has_value()); + const Sketch_nodes::Node& origin = sketch.get_nodes()[*origin_idx]; + EXPECT_TRUE(origin.permanent); + EXPECT_NEAR(origin.X(), 0.0, Precision::Confusion()); + EXPECT_NEAR(origin.Y(), 0.0, Precision::Confusion()); + EXPECT_EQ(origin.name, "Origin"); +} + +TEST_F(Sketch_test, SketchOriginNodeFromFaceBBoxCenter) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + + gp_Pnt center(5.0, 3.0, 0.0); + TopoDS_Wire outer_wire = create_wire_box(default_plane, center, 20.0, 10.0); + Sketch sketch("FaceOrigin", view(), default_plane, outer_wire); + + std::optional origin_idx; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (!n.deleted && n.origin) + { + origin_idx = i; + break; + } + } + + ASSERT_TRUE(origin_idx.has_value()); + const Sketch_nodes::Node& origin = sketch.get_nodes()[*origin_idx]; + EXPECT_TRUE(origin.permanent); + EXPECT_NEAR(origin.X(), 5.0, Precision::Confusion()); + EXPECT_NEAR(origin.Y(), 3.0, Precision::Confusion()); +} + +TEST_F(Sketch_test, SketchOriginNodeSurvivesOnMode) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + + gp_Pnt center(5.0, 3.0, 0.0); + TopoDS_Wire outer_wire = create_wire_box(default_plane, center, 20.0, 10.0); + Sketch sketch("FaceOrigin", view(), default_plane, outer_wire); + + // Same path as create_sketch_from_planar_face_ -> set_mode(Sketch_inspection_mode). + sketch.on_mode(); + + std::optional origin_idx; + for (size_t i = 0; i < sketch.get_nodes().size(); ++i) + { + const Sketch_nodes::Node& n = sketch.get_nodes()[i]; + if (!n.deleted && n.origin) + { + origin_idx = i; + break; + } + } + + ASSERT_TRUE(origin_idx.has_value()); + EXPECT_NEAR(sketch.get_nodes()[*origin_idx].X(), 5.0, Precision::Confusion()); + EXPECT_NEAR(sketch.get_nodes()[*origin_idx].Y(), 3.0, Precision::Confusion()); +} + TEST_F(Sketch_test, OriginatingFaceSnapPointsCircle) { gp_Pln default_plane(gp::Origin(), gp::DZ());