From 777f2643ddd2051d3f0c4fde4bbd09bb3023b9c5 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 16:56:36 -0600 Subject: [PATCH 1/7] Improve --- src/sketch_delta.cpp | 62 ++++------- src/sketch_dims.cpp | 3 +- src/sketch_edge.cpp | 82 +++++++++++++- src/sketch_edge.h | 25 +++-- src/sketch_edges.cpp | 130 ++++++++++++++++------- src/sketch_edges.h | 1 + src/sketch_operations.cpp | 46 +++----- src/sketch_tools.cpp | 59 +++++++---- src/sketch_topo.cpp | 218 ++++++++++++++++++-------------------- src/sketch_topo.h | 3 + src/utl_geom.cpp | 115 ++++++++++++++++++++ src/utl_geom.h | 12 +++ tests/sketch_tests.cpp | 125 ++++++++++++---------- 13 files changed, 561 insertions(+), 320 deletions(-) diff --git a/src/sketch_delta.cpp b/src/sketch_delta.cpp index 1ae4729..aca753d 100644 --- a/src/sketch_delta.cpp +++ b/src/sketch_delta.cpp @@ -9,6 +9,7 @@ #include "occt_view.h" #include "sketch.h" #include "sketch_nodes.h" +#include "sketch_edge.h" namespace { @@ -533,7 +534,7 @@ void Sketch_delta::Impl::remove_linear_edges_on_segment_(Sketch& sketch, const g { for (auto itr = sketch.m_edges.edges().begin(); itr != sketch.m_edges.edges().end();) { - if (itr->circle_arc || !itr->node_idx_b.has_value()) + if (!sketch_edge_is_linear(*itr)) { ++itr; continue; @@ -558,51 +559,27 @@ void Sketch_delta::Impl::remove_linear_edges_on_node_segment_(Sketch& sketch, co void Sketch_delta::Impl::remove_arc_edge_(Sketch& sketch, const Arc_edge_record& rec) { - std::vector shps_to_remove; - - for (auto itr = sketch.m_edges.edges().begin(); itr != sketch.m_edges.edges().end(); ++itr) + for (auto itr = sketch.m_edges.edges().begin(); itr != sketch.m_edges.edges().end();) { - if (!itr->circle_arc || itr->shp.IsNull()) - continue; - - const Sketch::Edge* e1 = &*itr; - const Sketch::Edge* e2 = nullptr; - - for (auto itr2 = sketch.m_edges.edges().begin(); itr2 != sketch.m_edges.edges().end(); ++itr2) - if (itr2 != itr && itr2->shp == e1->shp) - { - e2 = &*itr2; - break; - } - - if (!e2) - continue; - - const Sketch::Edge* first = e1->node_idx_arc.has_value() ? e1 : (e2->node_idx_arc.has_value() ? e2 : nullptr); - const Sketch::Edge* second = first == e1 ? e2 : e1; - if (!first || !first->node_idx_arc.has_value() || !second->node_idx_b.has_value()) + if (!sketch_edge_is_arc(*itr)) + { + ++itr; continue; + } - const gp_Pnt2d start = sketch.m_nodes[first->node_idx_a]; - const gp_Pnt2d arc = sketch.m_nodes[*first->node_idx_arc]; - const gp_Pnt2d end = sketch.m_nodes[*second->node_idx_b]; + EZY_ASSERT(itr->node_idx_arc_pt.has_value() && itr->node_idx_b.has_value()); + const gp_Pnt2d start = sketch.m_nodes[itr->node_idx_a]; + const gp_Pnt2d arc = sketch.m_nodes[*itr->node_idx_arc_pt]; + const gp_Pnt2d end = sketch.m_nodes[*itr->node_idx_b]; if (!arc_equal_({start, arc, end}, rec)) + { + ++itr; continue; + } - const Sketch_AIS_edge_ptr& shp = e1->shp; - if (std::find(shps_to_remove.begin(), shps_to_remove.end(), shp) == shps_to_remove.end()) - shps_to_remove.push_back(shp); - } - - for (const Sketch_AIS_edge_ptr& shp : shps_to_remove) - { - sketch.m_ctx.Remove(shp, false); - for (auto itr = sketch.m_edges.edges().begin(); itr != sketch.m_edges.edges().end();) - if (itr->shp == shp) - itr = sketch.m_edges.edges().erase(itr); - else - ++itr; + sketch.m_ctx.Remove(itr->shp, false); + itr = sketch.m_edges.edges().erase(itr); } } @@ -653,7 +630,7 @@ void Sketch_delta::Impl::restore_prev_linear_edge_(Sketch& sketch, const Prev_ed sketch.sketch_json_add_linear_edge_(idx_a, idx_b, idx_mid); if (!rec.name.empty()) for (Sketch::Edge& e : sketch.m_edges.edges()) - if (!e.circle_arc && e.node_idx_b.has_value() && pts_equal_(sketch.m_nodes[e.node_idx_a], rec.pt_a) && + if (sketch_edge_is_linear(e) && pts_equal_(sketch.m_nodes[e.node_idx_a], rec.pt_a) && pts_equal_(sketch.m_nodes[*e.node_idx_b], rec.pt_b) && ((!rec.pt_mid.has_value() && !e.node_idx_mid.has_value()) || (rec.pt_mid.has_value() && e.node_idx_mid.has_value() && pts_equal_(sketch.m_nodes[*e.node_idx_mid], *rec.pt_mid)))) @@ -710,10 +687,7 @@ bool on_closed_segment_2d_(const gp_Pnt2d& p, const gp_Pnt2d& a, const gp_Pnt2d& return point_on_open_segment_2d(p, a, b); } -bool is_linear_sketch_edge_(const Sketch::Edge& e) -{ - return !e.circle_arc && e.node_idx_b.has_value() && !e.node_idx_arc.has_value(); -} +bool is_linear_sketch_edge_(const Sketch::Edge& e) { return sketch_edge_is_linear(e); } bool pts_equal_(const gp_Pnt2d& a, const gp_Pnt2d& b) { diff --git a/src/sketch_dims.cpp b/src/sketch_dims.cpp index 7447344..48f43a4 100644 --- a/src/sketch_dims.cpp +++ b/src/sketch_dims.cpp @@ -13,6 +13,7 @@ #include "occt_view.h" #include "sketch.h" #include "sketch_delta.h" +#include "sketch_edge.h" #include "utl_geom.h" using namespace glm; @@ -254,7 +255,7 @@ void Sketch_dims::toggle_edge_dim_anno(const ScreenCoords& screen_coords) } if (std::list::iterator itr = m_sketch.get_edge_at_(screen_coords); itr != m_sketch.m_edges.edges().end()) - if (!itr->circle_arc && itr->node_idx_b.has_value() && !itr->node_idx_arc.has_value()) + if (sketch_edge_is_linear(*itr)) { add_or_toggle_length_dim_between_node_indices_(itr->node_idx_a, *itr->node_idx_b); clear_pick_state(); diff --git a/src/sketch_edge.cpp b/src/sketch_edge.cpp index dc29b03..aba9578 100644 --- a/src/sketch_edge.cpp +++ b/src/sketch_edge.cpp @@ -1,6 +1,15 @@ #include "sketch_edge.h" +#include +#include +#include +#include +#include +#include +#include + #include "utl.h" +#include "utl_geom.h" bool Sketch_edge::reversed(size_t idx_a, size_t idx_b) const { @@ -14,4 +23,75 @@ bool Sketch_edge::reversed(size_t idx_a, size_t idx_b) const return false; } -bool sketch_edge_is_linear(const Sketch_edge& e) { return !e.circle_arc && e.node_idx_b.has_value(); } +bool sketch_edge_is_linear(const Sketch_edge& e) { return e.node_idx_b.has_value() && !e.node_idx_arc_pt.has_value(); } + +bool sketch_edge_is_arc(const Sketch_edge& e) { return e.node_idx_b.has_value() && e.node_idx_arc_pt.has_value(); } + +namespace +{ +gp_Vec2d normalize_or_axis_(gp_Vec2d v) +{ + const double mag2 = v.SquareMagnitude(); + if (mag2 < Precision::Confusion() * Precision::Confusion()) + return gp_Vec2d(1.0, 0.0); + + v.Normalize(); + return v; +} + +gp_Vec2d curve_tangent_dir_2d_(const BRepAdaptor_Curve& curve, double u, const gp_Pln& pln, bool forward) +{ + gp_Pnt p; + gp_Vec tan3; + curve.D1(u, p, tan3); + if (!forward) + tan3.Reverse(); + + const gp_Dir& x_dir = pln.XAxis().Direction(); + const gp_Dir& y_dir = pln.YAxis().Direction(); + return normalize_or_axis_(gp_Vec2d(tan3.Dot(gp_Vec(x_dir)), tan3.Dot(gp_Vec(y_dir)))); +} + +gp_Vec2d arc_outgoing_dir_2d_(const Sketch_edge& e, const gp_Pnt2d& from_pt, const gp_Pnt2d& to_pt, const gp_Pln& pln) +{ + EZY_ASSERT(!e.shp.IsNull()); + const TopoDS_Edge edge = TopoDS::Edge(e.shp->Shape()); + const BRepAdaptor_Curve curve(edge); + const gp_Pnt from_3d = to_3d(pln, from_pt); + const gp_Pnt to_pt_3d = to_3d(pln, to_pt); + + const Handle(Geom_Curve) geom = curve.Curve().Curve(); + const double u_first = curve.FirstParameter(); + const double u_last = curve.LastParameter(); + + GeomAPI_ProjectPointOnCurve proj_from(from_3d, geom, u_first, u_last); + GeomAPI_ProjectPointOnCurve proj_to(to_pt_3d, geom, u_first, u_last); + EZY_ASSERT(proj_from.NbPoints() > 0 && proj_to.NbPoints() > 0); + + const double u_from = proj_from.LowerDistanceParameter(); + const double u_to = proj_to.LowerDistanceParameter(); + const double du = u_to - u_from; + + if (std::abs(du) < Precision::Confusion()) + return curve_tangent_dir_2d_(curve, u_from, pln, true); + + const double span = u_last - u_first; + const double step = std::max(std::abs(du) * 0.01, span * 1e-4); + double u_step = u_from + (du > 0.0 ? step : -step); + u_step = std::clamp(u_step, u_first, u_last); + + gp_Vec2d ret(from_pt, to_2d(pln, curve.Value(u_step))); + if (ret.SquareMagnitude() < Precision::Confusion() * Precision::Confusion()) + return curve_tangent_dir_2d_(curve, u_from, pln, du > 0.0); + + return normalize_or_axis_(ret); +} +} // namespace + +gp_Vec2d sketch_edge_outgoing_dir_2d(const Sketch_edge& e, const gp_Pnt2d& from_pt, const gp_Pnt2d& to_pt, const gp_Pln& pln) +{ + if (sketch_edge_is_arc(e)) + return arc_outgoing_dir_2d_(e, from_pt, to_pt, pln); + + return normalize_or_axis_(gp_Vec2d(from_pt, to_pt)); +} diff --git a/src/sketch_edge.h b/src/sketch_edge.h index 1587992..20e3fd0 100644 --- a/src/sketch_edge.h +++ b/src/sketch_edge.h @@ -1,28 +1,33 @@ #pragma once +#include +#include +#include #include #include #include "utl_types.h" -/// One sketch edge segment (linear or part of a circle arc). +/// One sketch edge segment (linear segment or circle arc). struct Sketch_edge { size_t node_idx_a; - std::optional node_idx_b; - std::optional node_idx_arc; // Only valid for circle arc edges. - std::optional node_idx_mid; // Midpoint of edge, used for snapping. + std::optional node_idx_b; // End node (lines and arcs). + std::optional node_idx_arc_pt; // On-curve bulge point for arcs; not a graph vertex. + std::optional node_idx_mid; // Midpoint of edge, used for snapping (lines only). - // Used to identify the two `Sketch_edge`s defining a circle arc. - Geom_TrimmedCurve_ptr circle_arc; - Sketch_AIS_edge_ptr shp; // Current edge annotation. - - std::string name; + Sketch_AIS_edge_ptr shp; // Edge annotation and OCCT geometry. + std::string name; bool reversed(size_t idx_a, size_t idx_b) const; }; [[nodiscard]] bool sketch_edge_is_linear(const Sketch_edge& e); +[[nodiscard]] bool sketch_edge_is_arc(const Sketch_edge& e); + +/// Unit direction from \a from_pt toward \a to_pt along \a e (chord for lines, curve tangent for arcs). +[[nodiscard]] gp_Vec2d sketch_edge_outgoing_dir_2d(const Sketch_edge& e, const gp_Pnt2d& from_pt, const gp_Pnt2d& to_pt, + const gp_Pln& pln); /// Read-only linear edge (node indices only). struct Sketch_edge_linear @@ -32,7 +37,7 @@ struct Sketch_edge_linear std::optional node_mid; }; -/// Read-only circle-arc edge (start, arc point, end); both arc halves share `shp`. +/// Read-only circle-arc edge (start, arc point, end). struct Sketch_edge_arc { size_t node_start; diff --git a/src/sketch_edges.cpp b/src/sketch_edges.cpp index a45738b..4a73796 100644 --- a/src/sketch_edges.cpp +++ b/src/sketch_edges.cpp @@ -1,14 +1,18 @@ #include "sketch_edges.h" #include +#include #include +#include #include +#include #include #include #include "occt_view.h" #include "sketch.h" #include "sketch_delta.h" +#include "sketch_edge.h" #include "sketch_topo.h" #include "utl_geom.h" #include "utl.h" @@ -62,35 +66,49 @@ void Sketch_edges::add_edge_impl_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, Sk std::vector inters; for (const Sketch_edge& e : m_edges) { - if (!is_linear(e)) - continue; - - gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; - gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; + if (is_linear(e)) + { + gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; + gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; - if (auto inter = segment_intersection_2d(pt_a, pt_b, qa, qb, Segment_inclusion::Closed)) - add_unique_point(inters, *inter); + if (auto inter = segment_intersection_2d(pt_a, pt_b, qa, qb, Segment_inclusion::Closed)) + add_unique_point(inters, *inter); + } + else if (sketch_edge_is_arc(e)) + { + for (const gp_Pnt2d& ip : + segment_arc_intersections_2d(pt_a, pt_b, TopoDS::Edge(e.shp->Shape()), m_sketch.m_pln, Segment_inclusion::Closed)) + add_unique_point(inters, ip); + } } - // Collect only the *interior* hits on existing edges for splitting. + // Collect interior hits on existing edges for splitting. std::vector inters_to_split; for (const auto& ip : inters) { - bool is_old_interior = false; + bool split_here = false; for (const Sketch_edge& e : m_edges) { - if (!is_linear(e)) - continue; - - gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; - gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; - if (point_on_open_segment_2d(ip, qa, qb)) + if (is_linear(e)) { - is_old_interior = true; - break; + gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; + gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; + if (point_on_open_segment_2d(ip, qa, qb)) + { + split_here = true; + break; + } + } + else if (sketch_edge_is_arc(e)) + { + if (point_on_open_arc_interior_2d(ip, TopoDS::Edge(e.shp->Shape()), m_sketch.m_pln)) + { + split_here = true; + break; + } } } - if (is_old_interior) + if (split_here) add_unique_point(inters_to_split, ip); } @@ -126,6 +144,7 @@ void Sketch_edges::add_edge_impl_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, Sk rec->note_curr_node(nidx); m_sketch.m_topo.split_linear_edges_at_node_if_interior(nidx, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(nidx, rec); } for (size_t i = 0; i + 1 < div_node_idxs.size(); ++i) @@ -175,9 +194,57 @@ void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs) m_sketch.update_edge_style_(shp); m_sketch.m_ctx.Display(shp, false); - // Split into two edges for valid planer graph topology. - m_edges.push_back({node_idxs[0], node_idxs[2], node_idxs[2], std::nullopt, arc_of_circle, shp}); - m_edges.push_back({node_idxs[2], node_idxs[1], std::nullopt, std::nullopt, arc_of_circle, shp}); + // One graph edge per arc; endpoints are start and end only. + m_edges.push_back({node_idxs[0], node_idxs[1], node_idxs[2], std::nullopt, shp}); +} + +void Sketch_edges::split_arc_at_node_(std::list::iterator itr, size_t split_idx, Sketch_op_recorder* rec) +{ + EZY_ASSERT(sketch_edge_is_arc(*itr)); + EZY_ASSERT(itr->node_idx_b.has_value() && itr->node_idx_arc_pt.has_value()); + + const size_t idx_a = itr->node_idx_a; + const size_t idx_b = *itr->node_idx_b; + EZY_ASSERT(split_idx != idx_a && split_idx != idx_b); + + if (rec) + rec->note_prev_arc_edge(m_sketch.m_nodes[idx_a], m_sketch.m_nodes[*itr->node_idx_arc_pt], m_sketch.m_nodes[idx_b]); + + const TopoDS_Edge occ_edge = TopoDS::Edge(itr->shp->Shape()); + const BRepAdaptor_Curve curve(occ_edge); + const Handle(Geom_Curve) geom = curve.Curve().Curve(); + const double u_first = curve.FirstParameter(); + const double u_last = curve.LastParameter(); + + auto param_at = [&](size_t node_idx) -> double + { + GeomAPI_ProjectPointOnCurve proj(to_3d(m_sketch.m_pln, m_sketch.m_nodes[node_idx]), geom, u_first, u_last); + EZY_ASSERT(proj.NbPoints() > 0); + return proj.LowerDistanceParameter(); + }; + + const double u_a = param_at(idx_a); + const double u_split = param_at(split_idx); + const double u_b = param_at(idx_b); + + const gp_Pnt2d bulge1 = to_2d(m_sketch.m_pln, curve.Value((u_a + u_split) * 0.5)); + const gp_Pnt2d bulge2 = to_2d(m_sketch.m_pln, curve.Value((u_split + u_b) * 0.5)); + + m_sketch.m_ctx.Remove(itr->shp, false); + m_sketch.m_edges.edges().erase(itr); + + const size_t bulge1_idx = m_sketch.m_nodes.get_node_exact(bulge1); + const size_t bulge2_idx = m_sketch.m_nodes.get_node_exact(bulge2); + + add_arc_circle_edges({idx_a, split_idx, bulge1_idx}); + add_arc_circle_edges({split_idx, idx_b, bulge2_idx}); + + if (rec) + { + rec->note_curr_node(split_idx); + rec->note_curr_arc_edge(m_sketch.m_nodes[idx_a], m_sketch.m_nodes[bulge1_idx], m_sketch.m_nodes[split_idx]); + rec->note_curr_arc_edge(m_sketch.m_nodes[split_idx], m_sketch.m_nodes[bulge2_idx], m_sketch.m_nodes[idx_b]); + } } void Sketch_edges::for_each_linear(const Linear_visitor& fn) const @@ -194,28 +261,13 @@ void Sketch_edges::for_each_linear(const Linear_visitor& fn) const void Sketch_edges::for_each_arc(const Arc_visitor& fn) const { - const Sketch_edge* last_arc_half = nullptr; - for (const Sketch_edge& e : m_edges) { - if (!e.circle_arc) + if (!sketch_edge_is_arc(e)) continue; - EZY_ASSERT(e.node_idx_b.has_value()); - - if (last_arc_half) - { - EZY_ASSERT(last_arc_half->circle_arc.get() == e.circle_arc.get()); - EZY_ASSERT(last_arc_half->node_idx_arc.has_value()); - fn(Sketch_edge_arc{last_arc_half->node_idx_a, *last_arc_half->node_idx_arc, *e.node_idx_b, last_arc_half->shp}); - last_arc_half = nullptr; - } - else - { - EZY_ASSERT(e.node_idx_arc.has_value()); - EZY_ASSERT(*e.node_idx_b == *e.node_idx_arc); - last_arc_half = &e; - } + EZY_ASSERT(e.node_idx_arc_pt.has_value()); + fn(Sketch_edge_arc{e.node_idx_a, *e.node_idx_arc_pt, *e.node_idx_b, e.shp}); } } diff --git a/src/sketch_edges.h b/src/sketch_edges.h index 171afe9..de7613c 100644 --- a/src/sketch_edges.h +++ b/src/sketch_edges.h @@ -52,6 +52,7 @@ class Sketch_edges void add_edge_raw_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b); void add_edge_impl_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, Sketch_op_recorder* rec); + void split_arc_at_node_(std::list::iterator itr, size_t split_idx, Sketch_op_recorder* rec); Sketch& m_sketch; std::list m_edges; diff --git a/src/sketch_operations.cpp b/src/sketch_operations.cpp index 83732fb..b6b2749 100644 --- a/src/sketch_operations.cpp +++ b/src/sketch_operations.cpp @@ -2,14 +2,13 @@ #include "sketch.h" #include -#include #include -#include #include #include "gui.h" #include "mode.h" #include "sketch_delta.h" +#include "sketch_edge.h" #include "utl_geom.h" #include "utl_occt.h" #include "utl.h" @@ -57,52 +56,37 @@ void Sketch::mirror_selected_edges() EZY_ASSERT(!m_operation_axis->shp.IsNull()); const auto [mirror_pt_a, mirror_pt_b] = get_edge_endpoints(m_pln, TopoDS::Edge(m_operation_axis->shp->Shape())); - std::vector> mirrored_edges; - std::map> arc_circles; + std::vector> mirrored_edges; + std::set mirrored_arc_shps; for (const Edge& e : m_edges.edges()) for (const Edge& selected : mirror_edges) - if (e.shp == selected.shp || e.circle_arc == selected.circle_arc) + if (e.shp == selected.shp) { - if (e.circle_arc) + if (sketch_edge_is_arc(e)) { - std::set& arc_circle_edges = arc_circles[e.shp]; - arc_circle_edges.insert(&e); - if (arc_circle_edges.size() == 2) - break; + if (!mirrored_arc_shps.count(e.shp)) + { + mirrored_arc_shps.insert(e.shp); + EZY_ASSERT(e.node_idx_arc_pt.has_value() && e.node_idx_b.has_value()); + const gp_Pnt2d pt_a = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[e.node_idx_a]); + const gp_Pnt2d pt_b = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[*e.node_idx_arc_pt]); + const gp_Pnt2d pt_c = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[*e.node_idx_b]); + add_arc_circle_(pt_a, pt_b, pt_c, rec); + } } else { const gp_Pnt2d a = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[e.node_idx_a]); const gp_Pnt2d b = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[e.node_idx_b]); mirrored_edges.push_back({a, b}); - break; } + break; } for (const auto& [a, b] : mirrored_edges) add_edge_(a, b, rec); - for (auto& [_, arc_circle_edges] : arc_circles) - { - EZY_ASSERT(arc_circle_edges.size() == 2); - const Edge* a = nullptr; - const Edge* b = nullptr; - for (const Edge* e : arc_circle_edges) - if (e->node_idx_arc.has_value()) - a = e; - else - b = e; - - EZY_ASSERT(a && b); - EZY_ASSERT(a->node_idx_arc.has_value()); - EZY_ASSERT(b->node_idx_b); - const gp_Pnt2d pt_a = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[a->node_idx_a]); - const gp_Pnt2d pt_b = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[a->node_idx_arc]); - const gp_Pnt2d pt_c = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[b->node_idx_b]); - add_arc_circle_(pt_a, pt_b, pt_c, rec); - } - m_nodes.hide_snap_annos(); update_faces_(); rec.commit(); diff --git a/src/sketch_tools.cpp b/src/sketch_tools.cpp index c863358..87d9008 100644 --- a/src/sketch_tools.cpp +++ b/src/sketch_tools.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "occt_view.h" #include "sketch.h" #include "sketch_delta.h" +#include "sketch_edge.h" #include "utl_geom.h" #include "utl_occt.h" #include "utl.h" @@ -478,17 +480,18 @@ void Sketch_tools::finalize_edges_(Sketch_op_recorder& rec) gp_Pnt2d pb = m_sketch.m_nodes[te.node_idx_b]; for (const Sketch_edge& oe : m_sketch.m_edges.edges()) // pre-batch olds only { - if (!Sketch::is_linear_edge_(oe)) - continue; - - gp_Pnt2d qa = m_sketch.m_nodes[oe.node_idx_a]; - gp_Pnt2d qb = m_sketch.m_nodes[oe.node_idx_b]; - if (auto inter = segment_intersection_2d(pa, pb, qa, qb, Segment_inclusion::Closed)) + if (Sketch::is_linear_edge_(oe)) + { + gp_Pnt2d qa = m_sketch.m_nodes[oe.node_idx_a]; + gp_Pnt2d qb = m_sketch.m_nodes[oe.node_idx_b]; + if (auto inter = segment_intersection_2d(pa, pb, qa, qb, Segment_inclusion::Closed)) + add_unique_point(batch_inters, *inter); + } + else if (sketch_edge_is_arc(oe)) { - // The intersection point is now guaranteed (by the inclusion parameter) - // to lie on both finite closed segments. The previous custom verification - // has been moved into segment_intersection_2d for consistency. - add_unique_point(batch_inters, *inter); + for (const gp_Pnt2d& inter : + segment_arc_intersections_2d(pa, pb, TopoDS::Edge(oe.shp->Shape()), m_sketch.m_pln, Segment_inclusion::Closed)) + add_unique_point(batch_inters, inter); } } } @@ -500,15 +503,23 @@ void Sketch_tools::finalize_edges_(Sketch_op_recorder& rec) bool is_old_interior = false; for (const Sketch_edge& e : m_sketch.m_edges.edges()) { - if (!Sketch::is_linear_edge_(e)) - continue; - - gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; - gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; - if (point_on_open_segment_2d(ip, qa, qb)) + if (Sketch::is_linear_edge_(e)) { - is_old_interior = true; - break; + gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; + gp_Pnt2d qb = m_sketch.m_nodes[e.node_idx_b]; + if (point_on_open_segment_2d(ip, qa, qb)) + { + is_old_interior = true; + break; + } + } + else if (sketch_edge_is_arc(e)) + { + if (point_on_open_arc_interior_2d(ip, TopoDS::Edge(e.shp->Shape()), m_sketch.m_pln)) + { + is_old_interior = true; + break; + } } } @@ -517,7 +528,11 @@ void Sketch_tools::finalize_edges_(Sketch_op_recorder& rec) } for (const auto& ip : batch_to_split) - m_sketch.m_topo.split_linear_edges_at_node_if_interior(m_sketch.m_nodes.get_node_exact(ip), rec); + { + const size_t nidx = m_sketch.m_nodes.get_node_exact(ip); + m_sketch.m_topo.split_linear_edges_at_node_if_interior(nidx, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(nidx, rec); + } } append(m_sketch.m_edges.edges(), m_tmp_edges); @@ -528,8 +543,7 @@ void Sketch_tools::finalize_edges_(Sketch_op_recorder& rec) for (size_t mid_pt_idx : split_mid_points) for (auto itr = m_sketch.m_edges.edges().begin(), end = m_sketch.m_edges.edges().end(); itr != end; ++itr) if (itr->node_idx_mid.has_value() && *itr->node_idx_mid == mid_pt_idx) - // Cannot split arc circles. - if (!itr->node_idx_arc.has_value()) + if (sketch_edge_is_linear(*itr)) { // Split the edge. Sketch_edge edge_a{itr->node_idx_a, mid_pt_idx}; @@ -562,6 +576,7 @@ void Sketch_tools::finalize_edges_(Sketch_op_recorder& rec) const size_t nidx = m_sketch.m_nodes.get_node_exact(ip); rec.note_curr_node(nidx); m_sketch.m_topo.split_linear_edges_at_node_if_interior(nidx, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(nidx, rec); } m_sketch.m_nodes.hide_snap_annos(); @@ -576,6 +591,7 @@ void Sketch_tools::add_node_pt_(const ScreenCoords& screen_coords) { rec.note_curr_node(node_b); m_sketch.m_topo.split_linear_edges_at_node_if_interior(node_b, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(node_b, rec); // Add-node mode: the rubber band is only for placement (snap / dimension / angle). Do not create // a sketch edge between the anchor and the new node - only nodes and interior splits matter. @@ -647,6 +663,7 @@ void Sketch_tools::add_node_pt_(const ScreenCoords& screen_coords) const size_t ni = m_sketch.m_nodes.add_new_node(pt, false, true); rec.note_curr_node(ni); m_sketch.m_topo.split_linear_edges_at_node_if_interior(ni, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(ni, rec); m_sketch.m_dims.clear_tmp_dim_anno(); m_sketch.m_nodes.hide_snap_annos(); m_sketch.update_faces_(); diff --git a/src/sketch_topo.cpp b/src/sketch_topo.cpp index 6d2ca4b..581f595 100644 --- a/src/sketch_topo.cpp +++ b/src/sketch_topo.cpp @@ -3,8 +3,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -22,6 +22,7 @@ #include "occt_view.h" #include "sketch.h" #include "sketch_delta.h" +#include "sketch_edge.h" #include "utl.h" #include "utl_geom.h" @@ -78,10 +79,7 @@ void Sketch_topo::snap_placed_node_to_closest_linear_edge_interior_(size_t node_ for (const auto& e : m_sketch.m_edges.edges()) { - if (e.circle_arc) - continue; - - if (e.node_idx_arc.has_value()) + if (!sketch_edge_is_linear(e)) continue; if (!e.node_idx_b.has_value()) @@ -137,10 +135,7 @@ void Sketch_topo::split_linear_edges_at_node_if_interior(size_t node_idx, Sketch progress = false; for (auto itr = m_sketch.m_edges.edges().begin(); itr != m_sketch.m_edges.edges().end(); ++itr) { - if (itr->circle_arc) - continue; - - if (itr->node_idx_arc.has_value()) + if (!sketch_edge_is_linear(*itr)) continue; if (!itr->node_idx_b.has_value()) @@ -177,6 +172,47 @@ void Sketch_topo::split_linear_edges_at_node_if_interior(size_t node_idx, Sketch } } +void Sketch_topo::split_arcs_at_node_if_interior(size_t node_idx) +{ + split_arcs_at_node_if_interior(node_idx, static_cast(nullptr)); +} + +void Sketch_topo::split_arcs_at_node_if_interior(size_t node_idx, Sketch_op_recorder& rec) +{ + split_arcs_at_node_if_interior(node_idx, &rec); +} + +void Sketch_topo::split_arcs_at_node_if_interior(size_t node_idx, Sketch_op_recorder* rec) +{ + EZY_ASSERT(node_idx < m_sketch.m_nodes.size()); + const gp_Pnt2d& p = m_sketch.m_nodes[node_idx]; + + bool progress = true; + while (progress) + { + progress = false; + for (auto itr = m_sketch.m_edges.edges().begin(); itr != m_sketch.m_edges.edges().end(); ++itr) + { + if (!sketch_edge_is_arc(*itr)) + continue; + + EZY_ASSERT(itr->node_idx_b.has_value()); + const size_t idx_a = itr->node_idx_a; + const size_t idx_b = *itr->node_idx_b; + if (node_idx == idx_a || node_idx == idx_b) + continue; + + if (!point_on_open_arc_interior_2d(p, TopoDS::Edge(itr->shp->Shape()), m_sketch.m_pln)) + continue; + + m_sketch.m_edges.split_arc_at_node_(itr, node_idx, rec); + m_sketch.m_nodes[node_idx].midpoint = false; + progress = true; + break; + } + } +} + // Function to extract faces from the planar graph void Sketch_topo::update_faces() { @@ -207,8 +243,8 @@ void Sketch_topo::update_faces() if (edge.node_idx_mid.has_value()) used_nodes[*edge.node_idx_mid] = true; - if (edge.node_idx_arc.has_value()) - used_nodes[*edge.node_idx_arc] = true; + if (edge.node_idx_arc_pt.has_value()) + used_nodes[*edge.node_idx_arc_pt] = true; } if (m_sketch.m_operation_axis.has_value()) @@ -222,32 +258,6 @@ void Sketch_topo::update_faces() // These edges cannot form closed faces, so we exclude them from face detection. std::unordered_set excluded_edges; - // Treat edges attached to the virtual mid-node of an arc as dangling for face detection. - // Circle arcs are represented using a virtual node in the middle of the arc (`node_idx_arc`). - // Any non-arc edge that uses this virtual node as an endpoint should not participate in faces. - { - std::unordered_set arc_mid_nodes; - for (const auto& edge : m_sketch.m_edges.edges()) - if (edge.node_idx_arc.has_value()) - arc_mid_nodes.insert(*edge.node_idx_arc); - - for (const auto& edge : m_sketch.m_edges.edges()) - { - // Only consider non-arc edges (straight segments etc.). - if (edge.circle_arc) - continue; - - if (!edge.node_idx_b.has_value()) - continue; - - const size_t a = edge.node_idx_a; - const size_t b = *edge.node_idx_b; - - if (arc_mid_nodes.count(a) || arc_mid_nodes.count(b)) - excluded_edges.insert(&edge); - } - } - bool changed = true; while (changed) { @@ -423,13 +433,6 @@ void Sketch_topo::update_faces() std::unordered_set, Pair_hash> seen_edges; - const auto edge_outgoing_dir = [this](size_t idx_a, size_t idx_b) -> gp_Vec2d - { - gp_Vec2d ret(m_sketch.m_nodes[idx_a], m_sketch.m_nodes[idx_b]); - ret.Normalize(); - return ret; - }; - for (auto& [a_idx, edges] : adj_list) for (auto& [b_idx, start_edge] : edges) { @@ -449,7 +452,7 @@ void Sketch_topo::update_faces() // Base case if (curr_idx == start_idx) { - EZY_ASSERT(face.size() > 2); + EZY_ASSERT(face.size() >= 2); // Deliberately accept the cycle only if the left-most walker produced a traversal // whose shoelace is positive under our is_face_clockwise_ convention. If not, break @@ -494,14 +497,18 @@ void Sketch_topo::update_faces() size_t left_most_idx; double min_angle = std::numeric_limits::max(); const Sketch::Edge* left_most_edge = nullptr; - gp_Vec2d edge_a = edge_outgoing_dir(prev_idx, curr_idx); + gp_Vec2d edge_a = sketch_edge_outgoing_dir_2d(*curr_edge, m_sketch.m_nodes[prev_idx], m_sketch.m_nodes[curr_idx], + m_sketch.m_pln); for (auto& [next_idx, next_edge] : adj_list[curr_idx]) { - if (next_idx == prev_idx) + // Allow parallel edges (e.g. two arcs between the same endpoints) by skipping only + // the incoming edge, not every edge back to the previous node. + if (next_idx == prev_idx && next_edge == curr_edge) continue; - gp_Vec2d edge_b = edge_outgoing_dir(curr_idx, next_idx); + gp_Vec2d edge_b = sketch_edge_outgoing_dir_2d(*next_edge, m_sketch.m_nodes[curr_idx], m_sketch.m_nodes[next_idx], + m_sketch.m_pln); double angle = std::atan2(edge_b.Crossed(edge_a), edge_b.Dot(edge_a)); if (angle < min_angle) { @@ -634,38 +641,59 @@ size_t Sketch_topo::Face_edge::end_nd_idx() const bool Sketch_topo::is_face_clockwise_(const Face_edges& face) const { - // Compute signed area using the shoelace formula double signed_area = 0.0; for (const Face_edge& e : face) { - // `start_nd_idx` and `end_nd_idx` consider reversed edges const gp_Pnt2d& p1 = m_sketch.m_nodes[e.start_nd_idx()]; const gp_Pnt2d& p2 = m_sketch.m_nodes[e.end_nd_idx()]; - signed_area += (p1.X() * p2.Y()) - (p2.X() * p1.Y()); + + if (sketch_edge_is_arc(e.edge) && e.edge.node_idx_arc_pt.has_value() && !e.edge.shp.IsNull()) + { + const gp_Pnt2d& pm = m_sketch.m_nodes[*e.edge.node_idx_arc_pt]; + const double wedge = (p1.X() * pm.Y()) - (pm.X() * p1.Y()) + (pm.X() * p2.Y()) - (p2.X() * pm.Y()); + if (std::abs(wedge) > Precision::Confusion()) + { + signed_area += wedge; + continue; + } + + const TopoDS_Edge occ_edge = TopoDS::Edge(e.edge.shp->Shape()); + const BRepAdaptor_Curve curve(occ_edge); + const double u0 = curve.FirstParameter(); + const double u1 = curve.LastParameter(); + constexpr int segments = 12; + + auto sample_arc = [&](double u_from, double u_to) + { + gp_Pnt2d prev = to_2d(m_sketch.m_pln, curve.Value(u_from)); + for (int i = 1; i <= segments; ++i) + { + const double t = static_cast(i) / static_cast(segments); + const double u = u_from + (u_to - u_from) * t; + gp_Pnt2d cur = to_2d(m_sketch.m_pln, curve.Value(u)); + signed_area += (prev.X() * cur.Y()) - (cur.X() * prev.Y()); + prev = cur; + } + }; + + if (e.reversed) + sample_arc(u1, u0); + else + sample_arc(u0, u1); + } + else + signed_area += (p1.X() * p2.Y()) - (p2.X() * p1.Y()); } - // signed_area *= 0.5; // Divide by 2 for actual area return signed_area > 0; } Sketch_face_shp_ptr Sketch_topo::create_face_shape_(const Face_edges& face) { - EZY_ASSERT(face.size() > 2); + EZY_ASSERT(face.size() >= 2); - // Create edges for the wire BRepBuilderAPI_MakeWire wire_maker; - - struct Circle_arc - { - std::optional nd_idx_a; - std::optional nd_idx_b; - std::optional nd_idx_c; - std::optional dbg_reversed; - }; - - std::map circle_arcs; - std::vector node_verts; - std::optional dbg_last_node_idx; + std::vector node_verts; for (const Face_edge& e : face) { @@ -679,56 +707,19 @@ Sketch_face_shp_ptr Sketch_topo::create_face_shape_(const Face_edges& face) node_verts.push_back(pt); }; - if (e.edge.circle_arc.get()) + if (sketch_edge_is_arc(e.edge)) { - Circle_arc& arc = circle_arcs[e.edge.circle_arc.get()]; + gp_Pnt a = m_sketch.to_3d_(e.start_nd_idx()); + gp_Pnt b = m_sketch.to_3d_(e.end_nd_idx()); - auto try_arc_circle = [&](bool reversed) - { - if (!arc.dbg_reversed.has_value()) - arc.dbg_reversed = reversed; - else - EZY_ASSERT(*arc.dbg_reversed == reversed); + add_node_vert_unique(a); + add_node_vert_unique(b); - if (arc.nd_idx_a && arc.nd_idx_b && arc.nd_idx_c) - { - gp_Pnt a = m_sketch.to_3d_(arc.nd_idx_a); - gp_Pnt b = m_sketch.to_3d_(arc.nd_idx_b); - gp_Pnt c = m_sketch.to_3d_(arc.nd_idx_c); - - add_node_vert_unique(a); - add_node_vert_unique(b); - add_node_vert_unique(c); - - Geom_TrimmedCurve_ptr arc_circle; - if (reversed) - arc_circle = GC_MakeArcOfCircle(c, b, a); - else - arc_circle = GC_MakeArcOfCircle(a, b, c); - - BRepBuilderAPI_MakeEdge edge(arc_circle); - EZY_ASSERT(edge.IsDone()); - wire_maker.Add(edge.Edge()); - } - }; + TopoDS_Edge occ_edge = TopoDS::Edge(e.edge.shp->Shape()); + if (e.reversed) + occ_edge.Reverse(); - // Depending on the order of the node indexes in face, - // the first or second circle arc edge might come first. - if (e.edge.node_idx_arc) - { - // This is the first part of the circle arc. - EZY_ASSERT(e.edge.node_idx_arc); - EZY_ASSERT(*e.edge.node_idx_b == *e.edge.node_idx_arc); - arc.nd_idx_a = e.edge.node_idx_a; - arc.nd_idx_b = e.edge.node_idx_arc; - try_arc_circle(e.reversed); - } - else - { - // This is the second part of the circle arc. - arc.nd_idx_c = *e.edge.node_idx_b; - try_arc_circle(e.reversed); - } + wire_maker.Add(occ_edge); } else { @@ -747,12 +738,9 @@ Sketch_face_shp_ptr Sketch_topo::create_face_shape_(const Face_edges& face) EZY_ASSERT(wire_maker.IsDone()); TopoDS_Wire wire = wire_maker.Wire(); - // Create a face from the wire BRepBuilderAPI_MakeFace face_maker(wire); EZY_ASSERT(face_maker.IsDone()); - // auto dbg_face = to_boost(face_maker.Face(), m_pln); - Sketch_face_shp* ret = new Sketch_face_shp(m_sketch, face_maker.Face()); std::swap(node_verts, ret->verts_3d); return ret; diff --git a/src/sketch_topo.h b/src/sketch_topo.h index ab35743..3c18540 100644 --- a/src/sketch_topo.h +++ b/src/sketch_topo.h @@ -18,6 +18,9 @@ class Sketch_topo void split_linear_edges_at_node_if_interior(size_t node_idx); void split_linear_edges_at_node_if_interior(size_t node_idx, Sketch_op_recorder& rec); void split_linear_edges_at_node_if_interior(size_t node_idx, Sketch_op_recorder* rec); + void split_arcs_at_node_if_interior(size_t node_idx); + void split_arcs_at_node_if_interior(size_t node_idx, Sketch_op_recorder& rec); + void split_arcs_at_node_if_interior(size_t node_idx, Sketch_op_recorder* rec); [[nodiscard]] const std::vector& faces() const { return m_faces; } [[nodiscard]] std::vector& faces() { return m_faces; } diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 6437ffb..cb5f711 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -32,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -1396,3 +1400,114 @@ std::optional snap_foot_to_open_segment_interior_if_close(const gp_Pnt return proj; } + +namespace +{ +Handle(Geom_TrimmedCurve) edge_trimmed_curve_(const TopoDS_Edge& edge) +{ + Standard_Real f = 0.0; + Standard_Real l = 0.0; + Handle(Geom_Curve) c = BRep_Tool::Curve(edge, f, l); + Handle(Geom_TrimmedCurve) ret = new Geom_TrimmedCurve(c, f, l); + if (edge.Orientation() == TopAbs_REVERSED) + ret->Reverse(); + return ret; +} + +bool on_segment_for_inclusion_(const gp_Pnt2d& p, const gp_Pnt2d& a, const gp_Pnt2d& b, Segment_inclusion inclusion) +{ + const double tol = Precision::Confusion(); + if (p.Distance(a) <= tol || p.Distance(b) <= tol) + return inclusion == Segment_inclusion::Closed; + + return point_on_open_segment_2d(p, a, b); +} + +bool on_open_arc_parameter_(double u, double u_first, double u_last) +{ + const double span = u_last - u_first; + const double margin = std::max(Precision::Confusion(), std::abs(span) * 1e-9); + return u > u_first + margin && u < u_last - margin; +} +} // namespace + +bool point_on_open_arc_interior_2d(const gp_Pnt2d& p, const TopoDS_Edge& arc_edge, const gp_Pln& pln) +{ + const BRepAdaptor_Curve curve(arc_edge); + const Handle(Geom_Curve) geom = curve.Curve().Curve(); + const double u_first = curve.FirstParameter(); + const double u_last = curve.LastParameter(); + + GeomAPI_ProjectPointOnCurve proj(to_3d(pln, p), geom, u_first, u_last); + if (proj.NbPoints() == 0 || proj.LowerDistance() > Precision::Confusion()) + return false; + + return on_open_arc_parameter_(proj.LowerDistanceParameter(), u_first, u_last); +} + +std::vector segment_arc_intersections_2d(const gp_Pnt2d& seg_a, const gp_Pnt2d& seg_b, const TopoDS_Edge& arc_edge, + const gp_Pln& pln, Segment_inclusion inclusion) +{ + std::vector ret; + const gp_Pnt a3 = to_3d(pln, seg_a); + const gp_Pnt b3 = to_3d(pln, seg_b); + if (a3.Distance(b3) <= Precision::Confusion()) + return ret; + + Handle(Geom_TrimmedCurve) seg = GC_MakeSegment(a3, b3); + Handle(Geom_TrimmedCurve) arc_c = edge_trimmed_curve_(arc_edge); + GeomAPI_ExtremaCurveCurve ext(seg, arc_c); + + for (int i = 1; i <= ext.NbExtrema(); ++i) + { + if (ext.Distance(i) > Precision::Confusion()) + continue; + + gp_Pnt p_on_seg; + gp_Pnt p_on_arc; + ext.Points(i, p_on_seg, p_on_arc); + const gp_Pnt2d p2d = to_2d(pln, p_on_seg); + if (!on_segment_for_inclusion_(p2d, seg_a, seg_b, inclusion)) + continue; + + const BRepAdaptor_Curve curve(arc_edge); + GeomAPI_ProjectPointOnCurve proj(p_on_arc, curve.Curve().Curve(), curve.FirstParameter(), curve.LastParameter()); + if (proj.NbPoints() == 0) + continue; + + if (!on_open_arc_parameter_(proj.LowerDistanceParameter(), curve.FirstParameter(), curve.LastParameter())) + continue; + + add_unique_point(ret, p2d); + } + + return ret; +} + +std::vector arc_arc_intersections_2d(const TopoDS_Edge& arc_a, const TopoDS_Edge& arc_b, const gp_Pln& pln) +{ + std::vector ret; + Handle(Geom_TrimmedCurve) curve_a = edge_trimmed_curve_(arc_a); + Handle(Geom_TrimmedCurve) curve_b = edge_trimmed_curve_(arc_b); + GeomAPI_ExtremaCurveCurve ext(curve_a, curve_b); + + for (int i = 1; i <= ext.NbExtrema(); ++i) + { + if (ext.Distance(i) > Precision::Confusion()) + continue; + + gp_Pnt p_a; + gp_Pnt p_b; + ext.Points(i, p_a, p_b); + const gp_Pnt2d p2d = to_2d(pln, p_a); + + if (!point_on_open_arc_interior_2d(p2d, arc_a, pln)) + continue; + if (!point_on_open_arc_interior_2d(p2d, arc_b, pln)) + continue; + + add_unique_point(ret, p2d); + } + + return ret; +} diff --git a/src/utl_geom.h b/src/utl_geom.h index 56d20ed..654d5c0 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -229,4 +229,16 @@ void add_unique_point(std::vector& points, const gp_Pnt2d& p); std::optional snap_foot_to_open_segment_interior_if_close(const gp_Pnt2d& p, const gp_Pnt2d& a, const gp_Pnt2d& b, double max_perp_dist); +/// True when \a p lies on the open interior of \a arc_edge (excluding endpoints). +[[nodiscard]] bool point_on_open_arc_interior_2d(const gp_Pnt2d& p, const TopoDS_Edge& arc_edge, const gp_Pln& pln); + +/// Intersection points of segment [seg_a-seg_b] with \a arc_edge per \a inclusion. +[[nodiscard]] std::vector segment_arc_intersections_2d(const gp_Pnt2d& seg_a, const gp_Pnt2d& seg_b, + const TopoDS_Edge& arc_edge, const gp_Pln& pln, + Segment_inclusion inclusion = Segment_inclusion::Closed); + +/// Interior intersection points of two arc edges (excluding endpoint touches). +[[nodiscard]] std::vector arc_arc_intersections_2d(const TopoDS_Edge& arc_a, const TopoDS_Edge& arc_b, + const gp_Pln& pln); + #include "utl_geom.inl" \ No newline at end of file diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index 619a30e..133f690 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -17,6 +17,7 @@ #include "sketch.h" #include "sketch_delta.h" #include "sketch_json.h" +#include "sketch_edge.h" #include "ezy_io.h" #include "ezy_asset_store.h" #include "utl_occt.h" @@ -136,7 +137,7 @@ size_t Sketch_access::get_linear_edge_count(const Sketch& sketch) size_t count = 0; for (const auto& edge : Sketch_access::get_edges(sketch)) { - if (edge.node_idx_b.has_value() && !edge.circle_arc) + if (sketch_edge_is_linear(edge)) ++count; } return count; @@ -147,7 +148,7 @@ size_t Sketch_access::get_arc_internal_edge_count(const Sketch& sketch) size_t count = 0; for (const auto& edge : Sketch_access::get_edges(sketch)) { - if (edge.circle_arc) + if (sketch_edge_is_arc(edge)) ++count; } return count; @@ -455,7 +456,7 @@ TEST_F(Sketch_test, AddEdgeFromCenter_SecondClickFullSpan) bool found = false; for (const auto& e : Sketch_access::get_edges(sketch)) { - if (!e.node_idx_b.has_value() || e.circle_arc) + if (!sketch_edge_is_linear(e)) continue; const gp_Pnt2d& a = sketch.get_nodes()[e.node_idx_a]; @@ -494,7 +495,7 @@ TEST_F(Sketch_test, AddEdgeFromCenter_DimensionUsesFullLength) bool found = false; for (const auto& e : Sketch_access::get_edges(sketch)) { - if (!e.node_idx_b.has_value() || e.circle_arc) + if (!sketch_edge_is_linear(e)) continue; const gp_Pnt2d& a = sketch.get_nodes()[e.node_idx_a]; @@ -564,14 +565,14 @@ TEST_F(Sketch_test, Undo_single_arc_via_recorder) rec.commit(); } - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 2); + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 1); EXPECT_TRUE(view().undo()); EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 0) - << "Undo should remove both internal arc edges"; + << "Undo should remove the arc edge"; EXPECT_TRUE(view().redo()); - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 2) + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 1) << "Redo should restore the arc"; } @@ -591,14 +592,14 @@ TEST_F(Sketch_test, Undo_circle_via_recorder) rec.commit(); } - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 4); + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 2); EXPECT_TRUE(view().undo()); EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 0) - << "Undo should remove both semicircles (four internal edges)"; + << "Undo should remove both semicircles (two arc edges)"; EXPECT_TRUE(view().redo()); - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 4) + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 2) << "Redo should restore the circle"; } @@ -706,7 +707,7 @@ TEST_F(Sketch_test, Undo_circle_axis_then_revolve_snapshot_three_undos) rec.commit(); } - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 4u); + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 2u); { Sketch_op_recorder rec(view(), *sk); @@ -733,13 +734,13 @@ TEST_F(Sketch_test, Undo_circle_axis_then_revolve_snapshot_three_undos) ASSERT_TRUE(sk); EXPECT_EQ(view().get_sketches().size(), 1u); EXPECT_TRUE(sk->has_operation_axis()); - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 4u); + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 2u); EXPECT_TRUE(view().undo()); sk = find_sk(); ASSERT_TRUE(sk); EXPECT_FALSE(sk->has_operation_axis()); - EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 4u); + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(*sk), 2u); EXPECT_TRUE(view().undo()); sk = find_sk(); @@ -1199,39 +1200,13 @@ TEST_F(Sketch_test, CreateCircle) const auto& face = faces[0]; EXPECT_FALSE(face.IsNull()) << "Face shape should not be null"; - // Convert to our polygon type (pure C++ re-implementation) and verify its properties. const TopoDS_Shape& shape = face->Shape(); EXPECT_EQ(shape.ShapeType(), TopAbs_FACE) << "Shape should be a face"; - ezy_geom::polygon_2d poly = to_boost(shape, default_plane); - - // Check that the ring is closed (first and last points should be identical) - const auto& outer = poly.outer(); - EXPECT_NEAR(outer.front().x(), outer.back().x(), 1e-6) << "Ring should be closed (x)"; - EXPECT_NEAR(outer.front().y(), outer.back().y(), 1e-6) << "Ring should be closed (y)"; - - // Check winding order - should be clockwise - EXPECT_TRUE(is_clockwise(outer)) << "Polygon should be clockwise"; - EXPECT_TRUE(ezy_geom::is_valid(poly)) << "Polygon should be valid"; - - // Verify the area of the circle (should be pi*r^2 = pi * 10^2 = 100pi) - double area = ezy_geom::area(poly); - double expected_area = 100.0 * std::numbers::pi; // 314.15926535897933 - EXPECT_NEAR(area, expected_area, 1.0) << "Circle area should be 100pi (radius = 10)"; - - // Verify the number of points in the outer ring (should be enough points to approximate a circle) - EXPECT_GT(poly.outer().size(), 20) << "Circle should have enough points to be smooth"; - - // Verify the points are roughly equidistant from the center - double expected_radius = 10.0; - double tolerance = 0.1; // Allow for some approximation error - for (const auto& point : poly.outer()) - { - double dx = point.x() - center.X(); - double dy = point.y() - center.Y(); - double radius = std::sqrt(dx * dx + dy * dy); - EXPECT_NEAR(radius, expected_radius, tolerance) << "Point should be on the circle"; - } + GProp_GProps props; + BRepGProp::SurfaceProperties(shape, props); + const double expected_area = 100.0 * std::numbers::pi; + EXPECT_NEAR(props.Mass(), expected_area, 1.0) << "Circle area should be 100pi (radius = 10)"; } // Test case 1: Simple rectangle face @@ -1483,18 +1458,14 @@ TEST_F(Sketch_test, UpdateFaces_DanglingEdgesArcMidNode) const auto& faces = Sketch_access::get_faces(sketch); - // We expect exactly one face: bounded by the arc and the base chord. - EXPECT_EQ(faces.size(), 1) << "Expected exactly one face for arc + chord; " - << "edges attached to the arc mid-node should be dangling."; + // We expect the region bounded by the arc and the base chord. The vertical edge to the arc + // bulge point is dangling and must not change that primary face. + EXPECT_GE(faces.size(), 1u) << "Expected at least the arc+chord face"; - // Verify the face is valid and corresponds to a region bounded by the arc and chord. + // Verify the face is valid. const auto& face = faces[0]; EXPECT_EQ(face->Shape().ShapeType(), TopAbs_FACE); - ezy_geom::polygon_2d poly = to_boost(face->Shape(), default_plane); - EXPECT_TRUE(ezy_geom::is_valid(poly)) << "Resulting polygon should be valid"; - EXPECT_TRUE(is_clockwise(poly.outer())) << "Polygon should be clockwise"; - // The dangling edges (especially the vertical one attached to arc_mid) should still exist // in the sketch, but they must not affect face detection. size_t total_edges = 0; @@ -1502,8 +1473,45 @@ TEST_F(Sketch_test, UpdateFaces_DanglingEdgesArcMidNode) if (e.node_idx_b.has_value()) ++total_edges; - // Arc contributes 2 edges, plus 3 straight edges = 5 - EXPECT_EQ(total_edges, 5) << "Expected 5 edges (2 arc segments + 3 straight edges)"; + // Arc contributes 1 edge, plus 3 straight edges and 1 dangling vertical = 5 + EXPECT_EQ(total_edges, 5) << "Expected 5 edges (1 arc + 3 straight + 1 dangling vertical)"; +} + +// Semicircle with equal-x endpoints: chord directions at endpoints are vertical, but arc +// tangents are horizontal. Face walking must use curve tangents. +TEST_F(Sketch_test, UpdateFaces_SemicircleTangentWalker) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("test_sketch", view(), default_plane); + + gp_Pnt2d arc_bottom(10.0, 0.0); + gp_Pnt2d arc_top(10.0, 20.0); + gp_Pnt2d arc_pt(0.0, 10.0); + + Sketch_access::add_arc_circle_(sketch, arc_bottom, arc_pt, arc_top); + Sketch_access::add_edge_(sketch, arc_top, arc_bottom); + + Sketch_access::update_faces_(sketch); + + const auto& faces = Sketch_access::get_faces(sketch); + EXPECT_EQ(faces.size(), 1); + + const auto& face = faces[0]; + EXPECT_EQ(face->Shape().ShapeType(), TopAbs_FACE); +} + +TEST_F(Sketch_test, ArcSplit_LineCrossingArcInterior) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("test_sketch", view(), default_plane); + + Sketch_access::add_arc_circle_(sketch, gp_Pnt2d(-5.0, 0.0), gp_Pnt2d(0.0, 5.0), gp_Pnt2d(5.0, 0.0)); + Sketch_access::add_edge_(sketch, gp_Pnt2d(0.0, -2.0), gp_Pnt2d(0.0, 8.0)); + + EXPECT_EQ(Sketch_access::get_arc_internal_edge_count(sketch), 2) + << "Line crossing arc interior should split the arc into two edges"; + EXPECT_GE(Sketch_access::get_linear_edge_count(sketch), 2u) + << "Line should be split at the arc intersection"; } TEST_F(Sketch_test, OriginatingFaceSnapPointsSquare) @@ -2528,7 +2536,7 @@ TEST_F(Sketch_test, MirrorSelectedEdges_Arc) AIS_Shape_ptr arc_shp; for (const auto& e : Sketch_access::get_edges(sketch)) { - if (e.circle_arc) + if (sketch_edge_is_arc(e)) { arc_shp = e.shp; break; @@ -2548,14 +2556,15 @@ TEST_F(Sketch_test, MirrorSelectedEdges_Arc) // Mirroring an arc pair should add another arc pair below size_t after = 0; for (const auto& e : Sketch_access::get_edges(sketch)) if (e.node_idx_b.has_value()) ++after; - // Expect +2 edges for the mirrored arc (the pair) - EXPECT_EQ(after, before + 2) << "Mirroring an arc should add two new arc edges"; + // Expect +1 edge for the mirrored arc + EXPECT_EQ(after, before + 1) << "Mirroring an arc should add one new arc edge"; // Spot-check that a mirrored point exists below the axis (e.g. around y=-1 or so) bool found_symmetric = false; for (const auto& e : Sketch_access::get_edges(sketch)) { - if (!e.node_idx_b.has_value() || !e.circle_arc) continue; + if (!sketch_edge_is_arc(e)) + continue; const gp_Pnt2d& pa = sketch.get_nodes()[e.node_idx_a]; if (std::abs(pa.Y() + 1.0) < 0.5) // rough, symmetric to +1 { @@ -2960,7 +2969,7 @@ TEST_F(Sketch_test, AddNode_splits_linear_edge_interior) bool found_7_20 = false; for (const auto& e : Sketch_access::get_edges(sketch)) { - if (!e.node_idx_b.has_value() || e.circle_arc) + if (!sketch_edge_is_linear(e)) continue; const gp_Pnt2d& pa = sketch.get_nodes()[e.node_idx_a]; const gp_Pnt2d& pb = sketch.get_nodes()[*e.node_idx_b]; From ee414e13a7b5003ebdd81e969af6ba6090568f82 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:01:10 -0600 Subject: [PATCH 2/7] Improve. --- src/sketch_delta.cpp | 30 ++++++++++++++++++++++++------ src/sketch_edge.h | 2 +- src/sketch_edges.cpp | 9 +++++++-- src/utl_geom.cpp | 7 +++++++ src/utl_geom.h | 3 +++ 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/sketch_delta.cpp b/src/sketch_delta.cpp index aca753d..94f52a1 100644 --- a/src/sketch_delta.cpp +++ b/src/sketch_delta.cpp @@ -1,5 +1,7 @@ #include "sketch_delta.h" +#include +#include #include #include #include @@ -78,6 +80,7 @@ class Sketch_delta::Impl static bool prev_linear_equal_(const Prev_edge_rec& x, const Prev_edge_rec& y); static bool curr_linear_equal_(const Curr_linear_edge_record& x, const Curr_linear_edge_record& y); static bool arc_equal_(const Arc_edge_record& x, const Arc_edge_record& y); + static bool arc_edge_matches_record_(const Sketch& sketch, const Sketch::Edge& e, const Arc_edge_record& rec); static bool length_dim_equal_(const Length_dim_record& x, const Length_dim_record& y); static void remove_linear_edges_on_segment_(Sketch& sketch, const gp_Pnt2d& seg_a, const gp_Pnt2d& seg_b); static void remove_linear_edges_on_node_segment_(Sketch& sketch, const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b); @@ -525,6 +528,26 @@ bool Sketch_delta::Impl::arc_equal_(const Arc_edge_record& x, const Arc_edge_rec x.pt_c.SquareDistance(y.pt_c) <= Precision::SquareConfusion(); } +bool Sketch_delta::Impl::arc_edge_matches_record_(const Sketch& sketch, const Sketch::Edge& e, const Arc_edge_record& rec) +{ + if (!sketch_edge_is_arc(e) || !e.node_idx_arc_pt.has_value() || !e.node_idx_b.has_value() || e.shp.IsNull()) + return false; + + const gp_Pnt2d start = sketch.m_nodes[e.node_idx_a]; + const gp_Pnt2d end = sketch.m_nodes[*e.node_idx_b]; + if (!pts_equal_(start, rec.pt_a) || !pts_equal_(end, rec.pt_c)) + return false; + + Geom_TrimmedCurve_ptr expected = + GC_MakeArcOfCircle(to_3d(sketch.m_pln, rec.pt_a), to_3d(sketch.m_pln, rec.pt_b), to_3d(sketch.m_pln, rec.pt_c)); + if (!expected) + return false; + + const gp_Pnt2d expected_mid = arc_curve_midpoint_2d(BRepBuilderAPI_MakeEdge(expected).Edge(), sketch.m_pln); + const gp_Pnt2d stored_mid = sketch.m_nodes[*e.node_idx_arc_pt]; + return pts_equal_(expected_mid, stored_mid); +} + bool Sketch_delta::Impl::length_dim_equal_(const Length_dim_record& x, const Length_dim_record& y) { return pts_equal_(x.pt_lo, y.pt_lo) && pts_equal_(x.pt_hi, y.pt_hi); @@ -567,12 +590,7 @@ void Sketch_delta::Impl::remove_arc_edge_(Sketch& sketch, const Arc_edge_record& continue; } - EZY_ASSERT(itr->node_idx_arc_pt.has_value() && itr->node_idx_b.has_value()); - const gp_Pnt2d start = sketch.m_nodes[itr->node_idx_a]; - const gp_Pnt2d arc = sketch.m_nodes[*itr->node_idx_arc_pt]; - const gp_Pnt2d end = sketch.m_nodes[*itr->node_idx_b]; - - if (!arc_equal_({start, arc, end}, rec)) + if (!arc_edge_matches_record_(sketch, *itr, rec)) { ++itr; continue; diff --git a/src/sketch_edge.h b/src/sketch_edge.h index 20e3fd0..bc8578b 100644 --- a/src/sketch_edge.h +++ b/src/sketch_edge.h @@ -13,7 +13,7 @@ struct Sketch_edge { size_t node_idx_a; std::optional node_idx_b; // End node (lines and arcs). - std::optional node_idx_arc_pt; // On-curve bulge point for arcs; not a graph vertex. + std::optional node_idx_arc_pt; // Arc midpoint on the curve (parametric half); not a graph vertex. std::optional node_idx_mid; // Midpoint of edge, used for snapping (lines only). Sketch_AIS_edge_ptr shp; // Edge annotation and OCCT geometry. diff --git a/src/sketch_edges.cpp b/src/sketch_edges.cpp index 4a73796..c630bdf 100644 --- a/src/sketch_edges.cpp +++ b/src/sketch_edges.cpp @@ -187,15 +187,20 @@ void Sketch_edges::update_end_pt(Sketch_edge& edge, size_t end_pt_idx) void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs) { EZY_ASSERT(node_idxs.size() == 3); + // node_idxs[2] is the user pick (may be toward the circle center); it defines the arc only. Geom_TrimmedCurve_ptr arc_of_circle = GC_MakeArcOfCircle(m_sketch.to_3d_(node_idxs[0]), m_sketch.to_3d_(node_idxs[2]), m_sketch.to_3d_(node_idxs[1])); - Sketch_AIS_edge_ptr shp = new Sketch_AIS_edge(m_sketch, BRepBuilderAPI_MakeEdge(arc_of_circle)); + const TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(arc_of_circle).Edge(); + const gp_Pnt2d arc_pt_on_curve = arc_curve_midpoint_2d(edge, m_sketch.m_pln); + const size_t arc_pt_idx = m_sketch.m_nodes.get_node_exact(arc_pt_on_curve); + + Sketch_AIS_edge_ptr shp = new Sketch_AIS_edge(m_sketch, edge); m_sketch.update_edge_style_(shp); m_sketch.m_ctx.Display(shp, false); // One graph edge per arc; endpoints are start and end only. - m_edges.push_back({node_idxs[0], node_idxs[1], node_idxs[2], std::nullopt, shp}); + m_edges.push_back({node_idxs[0], node_idxs[1], arc_pt_idx, std::nullopt, shp}); } void Sketch_edges::split_arc_at_node_(std::list::iterator itr, size_t split_idx, Sketch_op_recorder* rec) diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index cb5f711..eefca54 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -1445,6 +1445,13 @@ bool point_on_open_arc_interior_2d(const gp_Pnt2d& p, const TopoDS_Edge& arc_edg return on_open_arc_parameter_(proj.LowerDistanceParameter(), u_first, u_last); } +gp_Pnt2d arc_curve_midpoint_2d(const TopoDS_Edge& arc_edge, const gp_Pln& pln) +{ + const BRepAdaptor_Curve curve(arc_edge); + const double u_mid = (curve.FirstParameter() + curve.LastParameter()) * 0.5; + return to_2d(pln, curve.Value(u_mid)); +} + std::vector segment_arc_intersections_2d(const gp_Pnt2d& seg_a, const gp_Pnt2d& seg_b, const TopoDS_Edge& arc_edge, const gp_Pln& pln, Segment_inclusion inclusion) { diff --git a/src/utl_geom.h b/src/utl_geom.h index 654d5c0..f38a6f1 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -232,6 +232,9 @@ std::optional snap_foot_to_open_segment_interior_if_close(const gp_Pnt /// True when \a p lies on the open interior of \a arc_edge (excluding endpoints). [[nodiscard]] bool point_on_open_arc_interior_2d(const gp_Pnt2d& p, const TopoDS_Edge& arc_edge, const gp_Pln& pln); +/// Point at half the parametric length of \a arc_edge, projected to \a pln. +[[nodiscard]] gp_Pnt2d arc_curve_midpoint_2d(const TopoDS_Edge& arc_edge, const gp_Pln& pln); + /// Intersection points of segment [seg_a-seg_b] with \a arc_edge per \a inclusion. [[nodiscard]] std::vector segment_arc_intersections_2d(const gp_Pnt2d& seg_a, const gp_Pnt2d& seg_b, const TopoDS_Edge& arc_edge, const gp_Pln& pln, From e4ae8b741f4a1de1a7a7176ba9f101483a81f206 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:11:31 -0600 Subject: [PATCH 3/7] Arc midpoints option. --- src/gui_mode.cpp | 3 +++ src/sketch_delta.cpp | 6 +++--- src/sketch_edge.cpp | 11 +++++++++-- src/sketch_edge.h | 4 ++-- src/sketch_edges.cpp | 17 +++++++++++----- src/sketch_json.cpp | 25 ++++++++++++++++++------ src/sketch_operations.cpp | 7 +++++-- src/sketch_tools.cpp | 24 +++++++++++++++++++++++ src/sketch_topo.cpp | 8 +++++--- tests/sketch_tests.cpp | 41 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 446a2cd..6b5b3a0 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -895,6 +895,7 @@ void GUI::options_sketch_add_arc_circle_mode_() EZY_ASSERT(get_mode() == Mode::Sketch_add_seg_circle_arc); options_sketch_common_(); + options_sketch_add_midpoint_nodes_(m_add_mid_pt_line_edges); } void GUI::options_sketch_add_square_mode_() @@ -1047,6 +1048,7 @@ void GUI::sync_sketch_add_mid_pt_edges_if_applicable_() { case Mode::Sketch_add_edge: case Mode::Sketch_add_multi_edges: + case Mode::Sketch_add_seg_circle_arc: case Mode::Sketch_add_square: case Mode::Sketch_add_rectangle: case Mode::Sketch_add_rectangle_center_pt: @@ -1065,6 +1067,7 @@ bool GUI::add_mid_pt_edges_for_mode_(const Mode mode) const { case Mode::Sketch_add_edge: case Mode::Sketch_add_multi_edges: + case Mode::Sketch_add_seg_circle_arc: return m_add_mid_pt_line_edges; case Mode::Sketch_add_square: diff --git a/src/sketch_delta.cpp b/src/sketch_delta.cpp index 94f52a1..3e41335 100644 --- a/src/sketch_delta.cpp +++ b/src/sketch_delta.cpp @@ -530,7 +530,7 @@ bool Sketch_delta::Impl::arc_equal_(const Arc_edge_record& x, const Arc_edge_rec bool Sketch_delta::Impl::arc_edge_matches_record_(const Sketch& sketch, const Sketch::Edge& e, const Arc_edge_record& rec) { - if (!sketch_edge_is_arc(e) || !e.node_idx_arc_pt.has_value() || !e.node_idx_b.has_value() || e.shp.IsNull()) + if (!sketch_edge_is_arc(e) || !e.node_idx_b.has_value() || e.shp.IsNull()) return false; const gp_Pnt2d start = sketch.m_nodes[e.node_idx_a]; @@ -544,8 +544,8 @@ bool Sketch_delta::Impl::arc_edge_matches_record_(const Sketch& sketch, const Sk return false; const gp_Pnt2d expected_mid = arc_curve_midpoint_2d(BRepBuilderAPI_MakeEdge(expected).Edge(), sketch.m_pln); - const gp_Pnt2d stored_mid = sketch.m_nodes[*e.node_idx_arc_pt]; - return pts_equal_(expected_mid, stored_mid); + const gp_Pnt2d actual_mid = arc_curve_midpoint_2d(TopoDS::Edge(e.shp->Shape()), sketch.m_pln); + return pts_equal_(expected_mid, actual_mid); } bool Sketch_delta::Impl::length_dim_equal_(const Length_dim_record& x, const Length_dim_record& y) diff --git a/src/sketch_edge.cpp b/src/sketch_edge.cpp index aba9578..59ad3de 100644 --- a/src/sketch_edge.cpp +++ b/src/sketch_edge.cpp @@ -23,9 +23,16 @@ bool Sketch_edge::reversed(size_t idx_a, size_t idx_b) const return false; } -bool sketch_edge_is_linear(const Sketch_edge& e) { return e.node_idx_b.has_value() && !e.node_idx_arc_pt.has_value(); } +bool sketch_edge_is_arc(const Sketch_edge& e) +{ + if (!e.node_idx_b.has_value() || e.shp.IsNull()) + return false; + + const BRepAdaptor_Curve curve(TopoDS::Edge(e.shp->Shape())); + return curve.GetType() == GeomAbs_Circle; +} -bool sketch_edge_is_arc(const Sketch_edge& e) { return e.node_idx_b.has_value() && e.node_idx_arc_pt.has_value(); } +bool sketch_edge_is_linear(const Sketch_edge& e) { return e.node_idx_b.has_value() && !sketch_edge_is_arc(e); } namespace { diff --git a/src/sketch_edge.h b/src/sketch_edge.h index bc8578b..2fb2878 100644 --- a/src/sketch_edge.h +++ b/src/sketch_edge.h @@ -13,8 +13,8 @@ struct Sketch_edge { size_t node_idx_a; std::optional node_idx_b; // End node (lines and arcs). - std::optional node_idx_arc_pt; // Arc midpoint on the curve (parametric half); not a graph vertex. - std::optional node_idx_mid; // Midpoint of edge, used for snapping (lines only). + std::optional node_idx_arc_pt; // Arc curve midpoint snap node (when add-midpoint option on); not a graph vertex. + std::optional node_idx_mid; // Midpoint snap node for lines (when add-midpoint option on). Sketch_AIS_edge_ptr shp; // Edge annotation and OCCT geometry. std::string name; diff --git a/src/sketch_edges.cpp b/src/sketch_edges.cpp index c630bdf..400daee 100644 --- a/src/sketch_edges.cpp +++ b/src/sketch_edges.cpp @@ -192,8 +192,10 @@ void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs) GC_MakeArcOfCircle(m_sketch.to_3d_(node_idxs[0]), m_sketch.to_3d_(node_idxs[2]), m_sketch.to_3d_(node_idxs[1])); const TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(arc_of_circle).Edge(); - const gp_Pnt2d arc_pt_on_curve = arc_curve_midpoint_2d(edge, m_sketch.m_pln); - const size_t arc_pt_idx = m_sketch.m_nodes.get_node_exact(arc_pt_on_curve); + + std::optional arc_pt_idx; + if (Sketch::get_add_mid_pt_edges()) + arc_pt_idx = m_sketch.m_nodes.add_new_node(arc_curve_midpoint_2d(edge, m_sketch.m_pln), true); Sketch_AIS_edge_ptr shp = new Sketch_AIS_edge(m_sketch, edge); m_sketch.update_edge_style_(shp); @@ -206,16 +208,20 @@ void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs) void Sketch_edges::split_arc_at_node_(std::list::iterator itr, size_t split_idx, Sketch_op_recorder* rec) { EZY_ASSERT(sketch_edge_is_arc(*itr)); - EZY_ASSERT(itr->node_idx_b.has_value() && itr->node_idx_arc_pt.has_value()); + EZY_ASSERT(itr->node_idx_b.has_value()); const size_t idx_a = itr->node_idx_a; const size_t idx_b = *itr->node_idx_b; EZY_ASSERT(split_idx != idx_a && split_idx != idx_b); + const TopoDS_Edge occ_edge = TopoDS::Edge(itr->shp->Shape()); if (rec) - rec->note_prev_arc_edge(m_sketch.m_nodes[idx_a], m_sketch.m_nodes[*itr->node_idx_arc_pt], m_sketch.m_nodes[idx_b]); + { + const gp_Pnt2d arc_pt = itr->node_idx_arc_pt.has_value() ? m_sketch.m_nodes[*itr->node_idx_arc_pt] + : arc_curve_midpoint_2d(occ_edge, m_sketch.m_pln); + rec->note_prev_arc_edge(m_sketch.m_nodes[idx_a], arc_pt, m_sketch.m_nodes[idx_b]); + } - const TopoDS_Edge occ_edge = TopoDS::Edge(itr->shp->Shape()); const BRepAdaptor_Curve curve(occ_edge); const Handle(Geom_Curve) geom = curve.Curve().Curve(); const double u_first = curve.FirstParameter(); @@ -271,6 +277,7 @@ void Sketch_edges::for_each_arc(const Arc_visitor& fn) const if (!sketch_edge_is_arc(e)) continue; + EZY_ASSERT(e.node_idx_b.has_value()); EZY_ASSERT(e.node_idx_arc_pt.has_value()); fn(Sketch_edge_arc{e.node_idx_a, *e.node_idx_arc_pt, *e.node_idx_b, e.shp}); } diff --git a/src/sketch_json.cpp b/src/sketch_json.cpp index ecc8597..ac9747a 100644 --- a/src/sketch_json.cpp +++ b/src/sketch_json.cpp @@ -14,6 +14,7 @@ #include "utl_dbg.h" #include "utl_geom.h" #include "sketch.h" +#include "sketch_edge.h" #include "sketch_nodes.h" #include "sketch_underlay.h" #include "ezy_asset_store.h" @@ -103,9 +104,11 @@ void Sketch_json::from_json_indexed_(Sketch& ret, const json& j, for (const auto& edge_json : j["arc_edges"]) { EZY_ASSERT(edge_json.is_array() && edge_json.size() == 3); - const std::size_t ia = edge_json[0].get(); - const std::size_t iarc = edge_json[1].get(); - const std::size_t ib = edge_json[2].get(); + const std::size_t ia = edge_json[0].get(); + const std::size_t ib = edge_json[2].get(); + const std::size_t iarc = + edge_json[1].is_number_unsigned() ? edge_json[1].get() + : ret.get_nodes().get_node_exact(::from_json_pnt2d(edge_json[1])); // Matches `add_arc_circle_(pt_a, pt_b, pt_c)` -> node_idxs [a, c, b] for (json0, json1, json2) = (start, arc, end). ret.add_arc_circle_(std::vector{ia, ib, iarc}); } @@ -190,9 +193,19 @@ nlohmann::json Sketch_json::to_json(const Sketch& sketch, const Ezy_asset_store& edges_json.push_back(json::array({remap(e.node_a), remap(e.node_b)})); }); - sketch.m_edges.for_each_arc( - [&](const Sketch_edge_arc& a) - { arc_edges_json.push_back(json::array({remap(a.node_start), remap(a.node_arc), remap(a.node_end)})); }); + for (const Sketch_edge& e : sketch.m_edges.edges()) + { + if (!sketch_edge_is_arc(e) || !e.node_idx_b.has_value() || e.shp.IsNull()) + continue; + + json arc_mid_json; + if (e.node_idx_arc_pt.has_value()) + arc_mid_json = remap(*e.node_idx_arc_pt); + else + arc_mid_json = ::to_json(arc_curve_midpoint_2d(TopoDS::Edge(e.shp->Shape()), sketch.m_pln)); + + arc_edges_json.push_back(json::array({remap(e.node_idx_a), arc_mid_json, remap(*e.node_idx_b)})); + } json& len_dims_json = j["length_dimensions"] = json::array(); for (const Sketch_dims::Length_dimension& ld : sketch.m_dims.dimensions()) diff --git a/src/sketch_operations.cpp b/src/sketch_operations.cpp index b6b2749..ddb99dc 100644 --- a/src/sketch_operations.cpp +++ b/src/sketch_operations.cpp @@ -68,9 +68,12 @@ void Sketch::mirror_selected_edges() if (!mirrored_arc_shps.count(e.shp)) { mirrored_arc_shps.insert(e.shp); - EZY_ASSERT(e.node_idx_arc_pt.has_value() && e.node_idx_b.has_value()); + EZY_ASSERT(e.node_idx_b.has_value()); + const gp_Pnt2d arc_pt = + e.node_idx_arc_pt.has_value() ? m_nodes[*e.node_idx_arc_pt] + : arc_curve_midpoint_2d(TopoDS::Edge(e.shp->Shape()), m_pln); const gp_Pnt2d pt_a = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[e.node_idx_a]); - const gp_Pnt2d pt_b = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[*e.node_idx_arc_pt]); + const gp_Pnt2d pt_b = mirror_point(mirror_pt_a, mirror_pt_b, arc_pt); const gp_Pnt2d pt_c = mirror_point(mirror_pt_a, mirror_pt_b, m_nodes[*e.node_idx_b]); add_arc_circle_(pt_a, pt_b, pt_c, rec); } diff --git a/src/sketch_tools.cpp b/src/sketch_tools.cpp index 87d9008..a5a42b3 100644 --- a/src/sketch_tools.cpp +++ b/src/sketch_tools.cpp @@ -710,6 +710,8 @@ void Sketch_tools::add_arc_circle_pt_(const ScreenCoords& screen_coords) const gp_Pnt2d& pt_b = m_sketch.m_nodes[m_tmp_node_idxs[2]]; m_sketch.add_arc_circle_(pt_a, pt_b, pt_c, rec); m_tmp_node_idxs.clear(); + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; m_sketch.update_faces_(); rec.commit(); } @@ -723,6 +725,28 @@ void Sketch_tools::move_arc_circle_pt_(const ScreenCoords& screen_coords) // View plane and sketch plane must be perpendicular. return; + if (m_tmp_node_idxs.empty()) + { + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + if (m_tmp_node_idxs.size() == 1) + { + const gp_Pnt2d& pt_a = m_sketch.m_nodes[m_tmp_node_idxs[0]]; + if (!unique(pt_a, *pt)) + { + m_sketch.m_view.remove(m_tmp_shp); + m_tmp_shp = nullptr; + return; + } + + const TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(to_3d(m_sketch.m_pln, pt_a), to_3d(m_sketch.m_pln, *pt)).Edge(); + show(m_sketch.m_ctx, m_tmp_shp, edge); + return; + } + if (m_tmp_node_idxs.size() != 2) return; diff --git a/src/sketch_topo.cpp b/src/sketch_topo.cpp index 581f595..0108901 100644 --- a/src/sketch_topo.cpp +++ b/src/sketch_topo.cpp @@ -647,10 +647,12 @@ bool Sketch_topo::is_face_clockwise_(const Face_edges& face) const const gp_Pnt2d& p1 = m_sketch.m_nodes[e.start_nd_idx()]; const gp_Pnt2d& p2 = m_sketch.m_nodes[e.end_nd_idx()]; - if (sketch_edge_is_arc(e.edge) && e.edge.node_idx_arc_pt.has_value() && !e.edge.shp.IsNull()) + if (sketch_edge_is_arc(e.edge) && !e.edge.shp.IsNull()) { - const gp_Pnt2d& pm = m_sketch.m_nodes[*e.edge.node_idx_arc_pt]; - const double wedge = (p1.X() * pm.Y()) - (pm.X() * p1.Y()) + (pm.X() * p2.Y()) - (p2.X() * pm.Y()); + const gp_Pnt2d pm = e.edge.node_idx_arc_pt.has_value() ? m_sketch.m_nodes[*e.edge.node_idx_arc_pt] + : arc_curve_midpoint_2d(TopoDS::Edge(e.edge.shp->Shape()), + m_sketch.m_pln); + const double wedge = (p1.X() * pm.Y()) - (pm.X() * p1.Y()) + (pm.X() * p2.Y()) - (p2.X() * pm.Y()); if (std::abs(wedge) > Precision::Confusion()) { signed_area += wedge; diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index 133f690..1c03f18 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -18,6 +18,7 @@ #include "sketch_delta.h" #include "sketch_json.h" #include "sketch_edge.h" +#include "sketch_nodes.h" #include "ezy_io.h" #include "ezy_asset_store.h" #include "utl_occt.h" @@ -409,6 +410,46 @@ TEST_F(Sketch_test, AddLinearEdge_MidpointOption) Sketch::set_add_mid_pt_edges(false); } +TEST_F(Sketch_test, AddArc_MidpointOption) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("TestSketch", view(), default_plane); + + const gp_Pnt2d start(0.0, 0.0); + const gp_Pnt2d pick(5.0, 5.0); + const gp_Pnt2d end(10.0, 0.0); + + Sketch::set_add_mid_pt_edges(false); + Sketch_access::add_arc_circle_(sketch, start, end, pick); + + EXPECT_EQ(sketch.get_nodes().size(), 3u) << "Off: start, end, and bulge pick only"; + bool has_arc_mid = false; + for (const auto& e : Sketch_access::get_edges(sketch)) + if (e.node_idx_arc_pt.has_value()) + has_arc_mid = true; + EXPECT_FALSE(has_arc_mid); + + Sketch::set_add_mid_pt_edges(true); + const gp_Pnt2d start2(0.0, 10.0); + const gp_Pnt2d pick2(5.0, 15.0); + const gp_Pnt2d end2(10.0, 10.0); + Sketch_access::add_arc_circle_(sketch, start2, end2, pick2); + + const Sketch_edge* arc_with_mid = nullptr; + for (const auto& e : Sketch_access::get_edges(sketch)) + if (sketch_edge_is_arc(e) && e.node_idx_arc_pt.has_value()) + arc_with_mid = &e; + ASSERT_NE(arc_with_mid, nullptr); + const Sketch_nodes::Node& mid_node = sketch.get_nodes()[*arc_with_mid->node_idx_arc_pt]; + EXPECT_TRUE(mid_node.midpoint); + const gp_Pnt2d expected_mid = + arc_curve_midpoint_2d(TopoDS::Edge(arc_with_mid->shp->Shape()), default_plane); + EXPECT_TRUE(mid_node.IsEqual(expected_mid, Precision::Confusion())) + << "Arc midpoint snap node should lie on the curve at parametric half"; + + Sketch::set_add_mid_pt_edges(false); +} + // Test that adding two edges that cross (intersect interior to both) but *not* at either edge's // automatically-created midpoint results in each being split into two sub-edges, for a total of 4 edges. // This exercises the "split existing intersecting/touching edges on add" logic (and the corresponding From 4f86ff4128a82ecebbc5542d8186b7b4b8eb9cbd Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:32:37 -0600 Subject: [PATCH 4/7] Bug fix. --- src/sketch.cpp | 9 ++++- src/sketch.h | 1 + src/sketch_edges.cpp | 88 ++++++++++++++++++++++++++++++++++++++++-- src/sketch_edges.h | 2 +- src/sketch_topo.cpp | 27 +++++++++++++ tests/sketch_tests.cpp | 44 +++++++++++++++++++++ 6 files changed, 164 insertions(+), 7 deletions(-) diff --git a/src/sketch.cpp b/src/sketch.cpp index 985611d..32aa7a9 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -283,10 +283,15 @@ void Sketch::add_arc_circle_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const g rec.note_curr_node(node_idx_c); rec.note_curr_node(node_idx_b); - add_arc_circle_(std::vector{node_idx_a, node_idx_c, node_idx_b}); + add_arc_circle_(std::vector{node_idx_a, node_idx_c, node_idx_b}, rec); } -void Sketch::add_arc_circle_(const std::vector& node_idxs) { m_edges.add_arc_circle_edges(node_idxs); } +void Sketch::add_arc_circle_(const std::vector& node_idxs) { m_edges.add_arc_circle_edges(node_idxs, nullptr); } + +void Sketch::add_arc_circle_(const std::vector& node_idxs, Sketch_op_recorder& rec) +{ + m_edges.add_arc_circle_edges(node_idxs, &rec); +} void Sketch::on_mode() { diff --git a/src/sketch.h b/src/sketch.h index c059434..48762d9 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -169,6 +169,7 @@ class Sketch void sketch_json_set_operation_axis_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b); void add_arc_circle_(const std::vector& node_idxs); + void add_arc_circle_(const std::vector& node_idxs, Sketch_op_recorder& rec); void add_arc_circle_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const gp_Pnt2d& pt_c); void add_arc_circle_(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_b, const gp_Pnt2d& pt_c, Sketch_op_recorder& rec); diff --git a/src/sketch_edges.cpp b/src/sketch_edges.cpp index 400daee..8c09387 100644 --- a/src/sketch_edges.cpp +++ b/src/sketch_edges.cpp @@ -184,25 +184,105 @@ void Sketch_edges::update_end_pt(Sketch_edge& edge, size_t end_pt_idx) edge.node_idx_mid = std::nullopt; } -void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs) +void Sketch_edges::add_arc_circle_edges(const std::vector& node_idxs, Sketch_op_recorder* rec) { EZY_ASSERT(node_idxs.size() == 3); // node_idxs[2] is the user pick (may be toward the circle center); it defines the arc only. Geom_TrimmedCurve_ptr arc_of_circle = GC_MakeArcOfCircle(m_sketch.to_3d_(node_idxs[0]), m_sketch.to_3d_(node_idxs[2]), m_sketch.to_3d_(node_idxs[1])); + if (!arc_of_circle) + return; + + const TopoDS_Edge new_edge = BRepBuilderAPI_MakeEdge(arc_of_circle).Edge(); + const gp_Pnt2d pt_start = m_sketch.m_nodes[node_idxs[0]]; + const gp_Pnt2d pt_end = m_sketch.m_nodes[node_idxs[1]]; - const TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(arc_of_circle).Edge(); + // Intersections with all existing edges (split both olds and the new arc). + std::vector inters; + for (const Sketch_edge& e : m_edges) + { + if (is_linear(e)) + { + const gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; + const gp_Pnt2d qb = m_sketch.m_nodes[*e.node_idx_b]; + if (point_on_open_segment_2d(pt_start, qa, qb)) + add_unique_point(inters, pt_start); + if (point_on_open_segment_2d(pt_end, qa, qb)) + add_unique_point(inters, pt_end); + + for (const gp_Pnt2d& ip : + segment_arc_intersections_2d(qa, qb, new_edge, m_sketch.m_pln, Segment_inclusion::Closed)) + add_unique_point(inters, ip); + } + else if (sketch_edge_is_arc(e)) + { + for (const gp_Pnt2d& ip : + arc_arc_intersections_2d(TopoDS::Edge(e.shp->Shape()), new_edge, m_sketch.m_pln)) + add_unique_point(inters, ip); + } + } + + std::vector inters_to_split; + for (const gp_Pnt2d& ip : inters) + { + bool split_here = false; + for (const Sketch_edge& e : m_edges) + { + if (is_linear(e)) + { + const gp_Pnt2d qa = m_sketch.m_nodes[e.node_idx_a]; + const gp_Pnt2d qb = m_sketch.m_nodes[*e.node_idx_b]; + if (point_on_open_segment_2d(ip, qa, qb)) + { + split_here = true; + break; + } + } + else if (sketch_edge_is_arc(e)) + { + if (point_on_open_arc_interior_2d(ip, TopoDS::Edge(e.shp->Shape()), m_sketch.m_pln)) + { + split_here = true; + break; + } + } + } + if (split_here) + add_unique_point(inters_to_split, ip); + } + + for (const gp_Pnt2d& inter_p : inters_to_split) + { + const size_t nidx = m_sketch.m_nodes.get_node_exact(inter_p); + if (rec) + rec->note_curr_node(nidx); + + m_sketch.m_topo.split_linear_edges_at_node_if_interior(nidx, rec); + m_sketch.m_topo.split_arcs_at_node_if_interior(nidx, rec); + } std::optional arc_pt_idx; if (Sketch::get_add_mid_pt_edges()) - arc_pt_idx = m_sketch.m_nodes.add_new_node(arc_curve_midpoint_2d(edge, m_sketch.m_pln), true); + arc_pt_idx = m_sketch.m_nodes.add_new_node(arc_curve_midpoint_2d(new_edge, m_sketch.m_pln), true); - Sketch_AIS_edge_ptr shp = new Sketch_AIS_edge(m_sketch, edge); + Sketch_AIS_edge_ptr shp = new Sketch_AIS_edge(m_sketch, new_edge); m_sketch.update_edge_style_(shp); m_sketch.m_ctx.Display(shp, false); // One graph edge per arc; endpoints are start and end only. m_edges.push_back({node_idxs[0], node_idxs[1], arc_pt_idx, std::nullopt, shp}); + + for (const gp_Pnt2d& ip : inters) + { + if (!point_on_open_arc_interior_2d(ip, new_edge, m_sketch.m_pln)) + continue; + + const size_t nidx = m_sketch.m_nodes.get_node_exact(ip); + if (rec) + rec->note_curr_node(nidx); + + m_sketch.m_topo.split_arcs_at_node_if_interior(nidx, rec); + } } void Sketch_edges::split_arc_at_node_(std::list::iterator itr, size_t split_idx, Sketch_op_recorder* rec) diff --git a/src/sketch_edges.h b/src/sketch_edges.h index de7613c..3f25c41 100644 --- a/src/sketch_edges.h +++ b/src/sketch_edges.h @@ -23,7 +23,7 @@ class Sketch_edges void sketch_json_add_linear_edge(size_t idx_a, size_t idx_b, std::optional idx_mid); void update_end_pt(Sketch_edge& edge, size_t end_pt_idx); - void add_arc_circle_edges(const std::vector& node_idxs); + void add_arc_circle_edges(const std::vector& node_idxs, Sketch_op_recorder* rec = nullptr); void remove_by_ais(const Sketch_AIS_edge& to_remove); void remove_displayed(); diff --git a/src/sketch_topo.cpp b/src/sketch_topo.cpp index 0108901..71e634d 100644 --- a/src/sketch_topo.cpp +++ b/src/sketch_topo.cpp @@ -445,8 +445,35 @@ void Sketch_topo::update_faces() const Sketch::Edge* curr_edge = start_edge; Face_edges face; + // Guard against non-terminating left-most walks. After arc/line splits (e.g. a circle + // through a slot) the graph can have parallel edges between the same nodes with identical + // outgoing tangents at a vertex. The walker may then ping-pong between two edges forever + // without returning to start_idx. Track (curr, prev, edge) and cap steps so update_faces + // always finishes; partial walks are discarded like dangling topology. This safety net + // may become unnecessary if the turn rule is changed to avoid revisiting parallel pairs. + using Walk_key = std::tuple; + struct Walk_key_hash + { + size_t operator()(const Walk_key& k) const noexcept + { + const auto [c, p, e] = k; + return std::hash{}(c) ^ (std::hash{}(p) << 1) ^ + (std::hash{}(static_cast(e)) << 2); + } + }; + std::unordered_set walk_seen; + const size_t max_walk_steps = m_sketch.m_edges.size() + 1; + size_t walk_steps = 0; + for (;;) { + if (++walk_steps > max_walk_steps) + break; + + const Walk_key key{curr_idx, prev_idx, curr_edge}; + if (!walk_seen.insert(key).second) + break; + face.push_back({*curr_edge, curr_edge->reversed(prev_idx, curr_idx)}); // Base case diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index 1c03f18..e6c6029 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -1555,6 +1555,50 @@ TEST_F(Sketch_test, ArcSplit_LineCrossingArcInterior) << "Line should be split at the arc intersection"; } +TEST_F(Sketch_test, ArcSplit_ArcCrossingLineInterior) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("test_sketch", view(), default_plane); + + Sketch_access::add_edge_(sketch, gp_Pnt2d(-10.0, 0.0), gp_Pnt2d(10.0, 0.0)); + Sketch_access::add_arc_circle_(sketch, gp_Pnt2d(-4.0, 0.0), gp_Pnt2d(4.0, 0.0), gp_Pnt2d(0.0, 4.0)); + + EXPECT_EQ(Sketch_access::get_linear_edge_count(sketch), 3u) + << "Arc endpoints on line interior should split the line into three segments"; + EXPECT_GE(Sketch_access::get_arc_internal_edge_count(sketch), 1u); +} + +TEST_F(Sketch_test, ArcSplit_CircleCrossesSlotLines) +{ + gp_Pln default_plane(gp::Origin(), gp::DZ()); + Sketch sketch("test_sketch", view(), default_plane); + + const gp_Pnt2d slot_a(-15.0, 0.0); + const gp_Pnt2d slot_b(15.0, 0.0); + const gp_Pnt2d slot_c(0.0, 5.0); + const Slot_pnts pnts = get_slot_points(slot_a, slot_b, slot_c); + + Sketch_access::add_edge_(sketch, pnts.a_top_2d, pnts.b_top_2d); + Sketch_access::add_edge_(sketch, pnts.a_bottom_2d, pnts.b_bottom_2d); + Sketch_access::add_arc_circle_(sketch, pnts.a_bottom_2d, pnts.a_top_2d, pnts.a_mid_2d); + Sketch_access::add_arc_circle_(sketch, pnts.b_bottom_2d, pnts.b_top_2d, pnts.b_mid_2d); + + const size_t linear_before = Sketch_access::get_linear_edge_count(sketch); + const size_t arcs_before = Sketch_access::get_arc_internal_edge_count(sketch); + + std::array circle_pts = xy_stencil_pnts(gp_Pnt2d(0.0, 0.0), gp_Pnt2d(8.0, 0.0)); + Sketch_access::add_arc_circle_(sketch, circle_pts[0], circle_pts[2], circle_pts[1]); + Sketch_access::add_arc_circle_(sketch, circle_pts[0], circle_pts[3], circle_pts[1]); + + EXPECT_GT(Sketch_access::get_linear_edge_count(sketch), linear_before) + << "Circle through slot should split the horizontal slot lines"; + EXPECT_GT(Sketch_access::get_arc_internal_edge_count(sketch), arcs_before) + << "Circle arcs should split where they cross the slot lines"; + + // Face walker must terminate (regression: slot + circle could loop on parallel edge pairs). + Sketch_access::update_faces_(sketch); +} + TEST_F(Sketch_test, OriginatingFaceSnapPointsSquare) { gp_Pln default_plane(gp::Origin(), gp::DZ()); From 17ae81cb1c6132d5cf8c31deac3e858a6100054e Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:55:47 -0600 Subject: [PATCH 5/7] Document arc topology in user guide and track slot+circle bug. Update usage-sketch for single-edge arcs, intersection splitting, previews, and midpoint option; add bugs.md entry and agent drafts for issue/PR. Co-authored-by: Cursor --- CHANGELOG.md | 1 + ...etch-slot-circle-intersection-splitting.md | 55 +++++++++++++ .../draft-sketch-arc-topology-improve.md | 41 ++++++++++ docs/bugs.md | 7 +- docs/usage-sketch.md | 78 +++++++++++-------- 5 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md create mode 100644 agents/drafts/prs/active/draft-sketch-arc-topology-improve.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d6e73..eead0e4 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 +- **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/agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md b/agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md new file mode 100644 index 0000000..fc48952 --- /dev/null +++ b/agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md @@ -0,0 +1,55 @@ +# Sketch: slot + circle edge splitting at intersections + +**Suggested labels:** `bug`, `sketch` + +--- + +## Title (GitHub) + +Sketch: slot + circle edges split or highlight incorrectly at intersections + +## Body (GitHub) + +### Summary + +When a slot (two horizontal lines + end-cap arcs) is combined with a circle that crosses the horizontals, edge splitting and display can be wrong: one horizontal may split while the other does not, circle arcs may fail to split at line crossings, and some segments show incorrect highlight colors. + +### Problem + +- Adding end caps can spuriously split slot horizontals near the slot center (before any circle is added). +- Adding a circle that should cross both horizontals at two points each may not produce symmetric topology. +- Face update / edge styling can show inconsistent colors (e.g. white vs green segments). + +### Likely causes (investigated, not yet fixed) + +- `segment_arc_intersections_2d` (`utl_geom.cpp`): `GeomAPI_ExtremaCurveCurve` may return segment points paired with unrelated arc-foot points on the full circle outside the trimmed semicircle. +- `add_arc_circle_edges` / circle finalize argument order vs `GC_MakeArcOfCircle` node index conventions. +- Face walker parallel-edge loops (partial guard in `sketch_topo.cpp`). + +### Acceptance criteria + +- [ ] Slot horizontals remain unsplit after end caps are added (only endpoint nodes at corners). +- [ ] Circle crossing both horizontals splits each into three segments at the two intersection points. +- [ ] Circle arcs split at horizontal crossings; edge highlight colors consistent. +- [ ] `update_faces()` completes without infinite loop on slot + circle geometry. +- [ ] Regression tests: `SlotHorizontalLinesUnsplitBeforeCircleArcs`, `ArcSplit_CircleCrossesSlotLines`. + +### Files / areas + +- `src/utl_geom.cpp` — `segment_arc_intersections_2d` +- `src/sketch_edges.cpp` — `add_arc_circle_edges` +- `src/sketch_tools.cpp` — `finalize_circle_`, slot finalize +- `src/sketch_topo.cpp` — face walker +- `docs/bugs.md` — user-visible note +- `tests/sketch_tests.cpp` + +### Related + +- Documented in `docs/bugs.md` +- Arc topology PR (single edge per arc, intersection splitting) — separate work; does not resolve this bug + +### Test plan + +- [ ] Reproduce: slot tool + circle tool on same sketch, circle diameter larger than slot height. +- [ ] Verify edge counts per horizontal line before/after circle. +- [ ] Run targeted `EzyCad_tests` filters above. diff --git a/agents/drafts/prs/active/draft-sketch-arc-topology-improve.md b/agents/drafts/prs/active/draft-sketch-arc-topology-improve.md new file mode 100644 index 0000000..59e14b5 --- /dev/null +++ b/agents/drafts/prs/active/draft-sketch-arc-topology-improve.md @@ -0,0 +1,41 @@ +# PR - Sketch arc topology improvements + +## Title + +Sketch arc topology: single edge per arc, intersection splitting, tangent face walker + +## Summary + +- Refactor arc sketch edges to **one graph edge per arc** (replacing two-edge internal representation). +- Split existing straight and arc edges when new arcs/lines cross them; subdivide new arcs at interior intersections. +- Face walking uses **arc tangents** at endpoints (fixes semicircle / slot cap loops). +- Arc Segment tool: rubber-band line after first click; **Add midpoint nodes** option adds geometric-mid snap node when enabled. +- Document arc behavior in `usage-sketch.md`; record open slot+circle bug in `bugs.md`. + +## Files Changed + +- `src/sketch_edge.cpp`, `src/sketch_edge.h`, `src/sketch_edges.cpp` — single arc edge, intersection split on add +- `src/sketch_topo.cpp`, `src/sketch_topo.h` — tangent-based face walker, arc/line split helpers, walk guard +- `src/utl_geom.cpp`, `src/utl_geom.h` — arc-line/arc-arc intersection helpers +- `src/sketch_tools.cpp`, `src/gui_mode.cpp` — arc preview, midpoint option in arc mode +- `src/sketch_delta.cpp`, `src/sketch_json.cpp`, `src/sketch_operations.cpp` — arc model migration +- `tests/sketch_tests.cpp` — arc split, semicircle walker, midpoint option tests +- `docs/usage-sketch.md`, `docs/bugs.md`, `CHANGELOG.md` + +## Related + +- Issue: (slot + circle bug — link after filing) +- Does **not** fix slot + circle intersection splitting; tracked separately. + +## Test Plan + +- [ ] Build `EzyCad_tests` (Debug). +- [ ] Run `Sketch_test.*Arc*`, `Sketch_test.*Semicircle*`, `Sketch_test.UpdateFaces_FaceWithArcs`. +- [ ] Manual: arc tool three-click flow, preview rubber band, optional midpoint node. +- [ ] Manual: line crossing arc splits both; slot + circle — confirm known bug still present (see issue). +- [ ] `sphinx-build -b html -W docs docs/_build` + +## Notes + +- `Walk_key` step cap in face walker prevents infinite loop on some parallel-edge cases; turn-rule improvement may follow. +- Circle finalize still uses legacy `add_arc_circle_(points[0], points[2], points[1])` call pattern; may need cleanup with slot+circle fix. diff --git a/docs/bugs.md b/docs/bugs.md index b3a4817..da00abf 100644 --- a/docs/bugs.md +++ b/docs/bugs.md @@ -37,4 +37,9 @@ * Arc function needs to accept tab for radius. * Dist tab dialog should state if it is a radius or diameter for arc.* * Dist tab dialog should state if it is a 1/2 dist or full, for edge tool. -* **Wasm / OCCT 8.x GLES shading regression:** With OCCT **8.0.0.p1** (`V8_0_0_p1`) and `TKOpenGles`, new solids and extrude previews often render wireframe-only or with partial face shading in the browser. Native desktop (OCCT 8 + `TKOpenGl`) is unaffected. Rebuilding against **7.9.3** (`scripts/build-occt-793-wasm.ps1`) restores correct shading. Workarounds in EzyCad (`SetMaterial` + `Redisplay`, `refresh_shape_shading_`, bisect toggles in `occt_view_wasm_bisect.h`) do not fix the root cause on OCCT 8. Use 7.9.3 for wasm builds and demo publishing until upstream fixes GLES presentation. +* **Sketch: slot + circle — edges split / colored incorrectly at intersections.** Draw a slot (two horizontals + end-cap arcs), then add a circle that crosses both horizontals. Observed: one horizontal may split while the other does not; circle arcs may not split at line crossings; some edge segments show the wrong highlight color (e.g. white vs green). Caps can spuriously split the horizontals near the slot center before the circle is added. Likely causes investigated (fix attempted and reverted; still open): + - `segment_arc_intersections_2d` (`utl_geom.cpp`) uses `GeomAPI_ExtremaCurveCurve` and returns `p_on_seg` while validating arc parameter from `p_on_arc`. Extrema can pair a segment foot with a nearby point on the *full* circle outside the trimmed semicircle, yielding false interior hits (e.g. on the slot centerline when adding the right cap). + - `add_arc_circle_edges` (`sketch_edges.cpp`) may use inconsistent node indices for intersection/knot logic vs `GC_MakeArcOfCircle` (vector is `[start, pick, end]` from `Sketch::add_arc_circle_`; `pt_end` / knot end node must be `node_idxs[2]`, not `[1]`). A long-standing knot bug masked wrong argument order in circle finalize (`sketch_tools.cpp`: `add_arc_circle_(points[0], points[2], points[1])` passes bottom/top as `pt_b` where the API expects arc *end*, with pick on the opposite side). + - `update_faces()` face walker can loop on parallel shared edges (e.g. slot horizontals); a `Walk_key` step cap was added in `sketch_topo.cpp` as a guard — may hide topology errors rather than fix turn logic. + - Relevant tests (if re-added): `SlotHorizontalLinesUnsplitBeforeCircleArcs`, `ArcSplit_CircleCrossesSlotLines` in `tests/sketch_tests.cpp`. Use `slot_c` perpendicular to the slot centerline (e.g. `(15, 5)` for `slot_b = (15, 0)`), not `(0, 5)`, to match the slot tool. +* Wasm / OCCT 8.x GLES shading regression: With OCCT **8.0.0.p1** (`V8_0_0_p1`) and `TKOpenGles`, new solids and extrude previews often render wireframe-only or with partial face shading in the browser. Native desktop (OCCT 8 + `TKOpenGl`) is unaffected. Rebuilding against **7.9.3** (`scripts/build-occt-793-wasm.ps1`) restores correct shading. Workarounds in EzyCad (`SetMaterial` + `Redisplay`, `refresh_shape_shading_`, bisect toggles in `occt_view_wasm_bisect.h`) do not fix the root cause on OCCT 8. Use 7.9.3 for wasm builds and demo publishing until upstream fixes GLES presentation. diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index b3241af..b4ee513 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -47,7 +47,7 @@ While you draw or place points in sketch mode, EzyCad helps you align to existin | **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**. | | **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. This produces correct T-junctions (3 edges), crossings (4 edges), and cleanly divided faces from a single sketch. Arcs have special internal handling and do not trigger the same linear splits. | +| **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. | | **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. | @@ -123,8 +123,8 @@ While the **Line Edge** tool is active, the Options panel (under the **Options** When checked, each new straight edge created with this tool (and with **Add multi-line edge**) gets an automatic **midpoint node** at the center of the segment. That node is a snap target but does not show a **+** marker and is not listed under **Nodes** in the [Sketch List](usage.md#sketch-list). - Default is **off** (no midpoint nodes on new edges). -- The same preference is available globally and persistently in **Settings -> Sketch -> Add midpoints to new linear edges** (saved as `gui.add_mid_pt_edges`). The Options checkbox mirrors that setting for the current session while you use line tools. -- Only affects **future** edges from the Line and Multi-line tools. Existing edges and midpoints loaded from a project are unchanged. +- The same preference is available globally and persistently in **Settings -> Sketch -> Add midpoints to new linear edges** (saved as `gui.add_mid_pt_edges`). The Options checkbox mirrors that setting for the current session while you use line tools and the **Arc Segment** tool. +- Affects **future** edges from the Line, Multi-line, and Arc Segment tools. - Midpoint snapping on **intersection splits** (when a new edge crosses an existing one) is separate topology behavior; see [Automatic splitting on edge intersections](#sketch-snapping). (line-edge-option-place-from-center)= @@ -336,60 +336,72 @@ The circle tool follows this workflow: ![Arc Segment Tool](res/icons/Sketcher_Element_Arc_Edge.png) -The arc segment tool allows you to create circular arc edges by defining three points that lie on the arc: a start point, a middle point, and an end point. +The arc segment tool creates a single circular arc edge from three clicks: **start**, a **bulge point** on the arc (between start and end), and **end**. **Features:** | | | | ---: | --- | -| **Three-point creation** | Click to set the start point, then the middle point, then the end point | -| **Real-time preview** | See the arc shape while moving the mouse after setting the first two points | -| **Automatic finalization** | The arc is automatically created and added to your sketch after the third point is clicked | -| **Circular arc** | Creates a smooth circular arc that passes through all three points | -| **Snap support** | Automatically snaps to existing nodes and geometry | -| **Unique points** | All three points must be different (cannot be coincident) | +| **Three-point creation** | Click start, then a point on the arc that sets bulge/curvature, then end | +| **Rubber-band preview** | After the first click, a straight preview line follows the cursor; after the second click, an arc preview follows the cursor until the third click | +| **Automatic finalization** | The arc is created and added to the sketch after the third click | +| **Circular arc** | One smooth circular arc through the three defining points | +| **Intersection splitting** | Splits existing straight and arc edges at interior crossings; the new arc is subdivided where other edges cross its interior | +| **Optional arc midpoint node** | When **Add midpoint nodes** is on in the Options panel, a snap node is placed at the arc's geometric midpoint (see [Arc segment Options](#arc-segment-options)) | +| **Snap support** | Snaps to existing nodes and geometry | +| **Unique points** | All three clicks must be different (cannot be coincident) | **How to use:** 1. ![Sketcher_Element_Arc_Edge](res/icons/Sketcher_Element_Arc_Edge.png) Select the **Arc Segment** tool from the toolbar -2. Click to set the start point of the arc (first point) -3. Click to set a point on the arc between start and end (middle point) -4. Move the mouse to see a preview of the arc -5. Click to set the end point of the arc (third point) -6. The arc segment will be automatically created and added to your sketch +2. Optionally enable **Add midpoint nodes** in the Options panel (see [Arc segment Options](#arc-segment-options)) +3. Click to set the **start** point (first click) +4. Move the mouse — a straight rubber-band line shows from start to the cursor +5. Click to set a **bulge point** on the arc (second click — between start and end, not necessarily at the geometric midpoint) +6. Move the mouse — an arc preview follows the cursor from start through the bulge point toward the end +7. Click to set the **end** point (third click) +8. The arc is created and added to your sketch; faces update if the sketch forms closed regions **Point order:** | | | | ---: | --- | -| **First click** | Start point - where the arc begins | -| **Second click** | Middle point - a point that lies on the arc between start and end | -| **Third click** | End point - where the arc ends | +| **First click** | Start — where the arc begins | +| **Second click** | Bulge point — any point on the intended arc between start and end (sets which side of the chord the arc bulges) | +| **Third click** | End — where the arc ends | **Shortcuts:** | | | | ---: | --- | | Escape | Cancel the current arc creation (clears all points) | -| **Note** | The arc is automatically finalized after the third point, so no manual finalization is needed | +| **Note** | The arc is automatically finalized after the third click; no manual finalization is needed | + +(arc-segment-options)= +### Arc segment Options + +While the **Arc Segment** tool is active, the Options panel shows the same **Add midpoint nodes** checkbox used by the Line Edge tool (see [Line edge option: Add midpoint nodes](#line-edge-option-add-midpoint-nodes)). + +- When **on**, each new arc gets an automatic **midpoint node** at the geometric center of the arc curve (parametric half). That node is a snap target only; it does not show a **+** marker and is not listed under **Nodes** in the [Sketch List](usage.md#sketch-list). +- When **off** (default), only the three clicked points (start, bulge, end) are stored as nodes. +- The setting mirrors **Settings -> Sketch -> Add midpoints to new linear edges** (`gui.add_mid_pt_edges`) for the current session. **Tips:** -- The three points define a unique circular arc that passes through all of them -- Use the snap feature to create arcs that connect precisely to existing geometry -- The arc tool works in any sketch plane -- Arc segments can be used as part of closed shapes that form faces -- The middle point helps define the arc's curvature and direction -- All three points must be unique - clicking the same point twice will be ignored +- The three clicks define a unique circular arc; the second click controls bulge direction (which side of the chord the arc curves toward). +- Use snap to connect arc endpoints precisely to existing geometry. +- Adding an arc across an existing straight edge splits that edge at the crossing; adding a line across an existing arc splits the arc at the crossing. +- Arc segments combine with straight edges to form closed shapes (slots, circles, custom profiles). -**Technical details:** -- The arc is created using the three points to define a circle, then trimming it to the arc segment -- Internally, the arc is represented as two connected edges for proper topology -- Arc segments can be combined with straight edges to create complex closed shapes +**Known limitations:** +- Combining a **slot** with a **circle** that crosses the slot horizontals can still produce incorrect edge splits or highlight colors in some cases. See [Known bugs — slot + circle](bugs.md) in the bug tracker. + +**Technical notes:** +- Each arc is one graph edge (start and end nodes only); face detection uses arc **tangents** at endpoints when walking closed loops. +- JSON save/load stores arc mid coordinates when no midpoint node is present. **Comparison with Circle Tool:** -- **Circle Tool**: Creates a full circle from center and radius point (2 points) -- **Arc Segment Tool**: Creates a partial arc from three points on the arc (3 points) -- Use circles when you need a complete circular shape -- Use arc segments when you need a curved edge that's part of a larger shape +- **Circle Tool**: Creates a full circle from center and radius point (2 points, two semicircular arc edges) +- **Arc Segment Tool**: Creates one partial arc from three points (start, bulge, end) +- Use circles when you need a complete circular shape; use arc segments for partial curves in larger shapes ## Rectangle and Square Creation Tools From e2bfbd300314b8a3a170c37aef0a32145456f8c5 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:56:13 -0600 Subject: [PATCH 6/7] Rename issue draft to gh-165 after filing on GitHub. Co-authored-by: Cursor --- ...=> gh-165-sketch-slot-circle-intersection-splitting.md} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename agents/drafts/issues/active/{draft-sketch-slot-circle-intersection-splitting.md => gh-165-sketch-slot-circle-intersection-splitting.md} (95%) diff --git a/agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md b/agents/drafts/issues/active/gh-165-sketch-slot-circle-intersection-splitting.md similarity index 95% rename from agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md rename to agents/drafts/issues/active/gh-165-sketch-slot-circle-intersection-splitting.md index fc48952..93f54db 100644 --- a/agents/drafts/issues/active/draft-sketch-slot-circle-intersection-splitting.md +++ b/agents/drafts/issues/active/gh-165-sketch-slot-circle-intersection-splitting.md @@ -1,6 +1,11 @@ # Sketch: slot + circle edge splitting at intersections -**Suggested labels:** `bug`, `sketch` +--- +github_issue: 165 +status: active +--- + +**Opened on GitHub:** https://github.com/trailcode/EzyCad/issues/165 --- From f0e2747e531951e3b99843c9189cd1cd23f01d91 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 5 Jul 2026 17:56:51 -0600 Subject: [PATCH 7/7] Rename PR draft to gh-166 after opening pull request. Co-authored-by: Cursor --- ...mprove.md => gh-166-sketch-arc-topology-improve.md} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename agents/drafts/prs/active/{draft-sketch-arc-topology-improve.md => gh-166-sketch-arc-topology-improve.md} (87%) diff --git a/agents/drafts/prs/active/draft-sketch-arc-topology-improve.md b/agents/drafts/prs/active/gh-166-sketch-arc-topology-improve.md similarity index 87% rename from agents/drafts/prs/active/draft-sketch-arc-topology-improve.md rename to agents/drafts/prs/active/gh-166-sketch-arc-topology-improve.md index 59e14b5..16c3e50 100644 --- a/agents/drafts/prs/active/draft-sketch-arc-topology-improve.md +++ b/agents/drafts/prs/active/gh-166-sketch-arc-topology-improve.md @@ -1,6 +1,12 @@ # PR - Sketch arc topology improvements -## Title +--- +github_pr: 166 +status: active +paired_draft: ../issues/active/gh-165-sketch-slot-circle-intersection-splitting.md +--- + +**Opened on GitHub:** https://github.com/trailcode/EzyCad/pull/166 Sketch arc topology: single edge per arc, intersection splitting, tangent face walker @@ -24,7 +30,7 @@ Sketch arc topology: single edge per arc, intersection splitting, tangent face w ## Related -- Issue: (slot + circle bug — link after filing) +- Issue: https://github.com/trailcode/EzyCad/issues/165 (slot + circle bug — **not** fixed by this PR) - Does **not** fix slot + circle intersection splitting; tracked separately. ## Test Plan