diff --git a/bugs.md b/bugs.md index 45ff364..0a606ba 100644 --- a/bugs.md +++ b/bugs.md @@ -7,6 +7,7 @@ * Create sketch from face needs a temporary face select mode. When mode is changed it should go back to the previous mode. Other tools like chamfer need to do this also. * Add sketch node should have the ability to allow the user to specify placement distance from another node. * Ability to show/hide sketch nodes +* Underlay: edge calibration can introduce shear (non-orthogonal bitmap axes). The transform panel currently assumes center + half extents + rotation (orthogonal). Need a full six-parameter affine in the UI so users can adjust position, scale, rotation, and shear without losing calibration. * For selection mode, not all of the current modes are useful. * Scale mode broken. diff --git a/src/gui.cpp b/src/gui.cpp index 9f0db45..ec36cbf 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -32,6 +32,8 @@ // Must be here to prevent compiler warning #include +using namespace glm; + GUI* gui_instance = nullptr; GUI::GUI() @@ -627,7 +629,7 @@ void GUI::set_dist_edit(float dist, std::function&& callback, if (screen_coords.has_value()) m_dist_edit_loc = *screen_coords; else - m_dist_edit_loc = ScreenCoords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + m_dist_edit_loc = ScreenCoords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); } m_dist_callback = std::move(callback); @@ -713,7 +715,7 @@ void GUI::set_angle_edit(float angle, std::function&& callbac if (screen_coords.has_value()) m_angle_edit_loc = *screen_coords; else - m_angle_edit_loc = ScreenCoords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + m_angle_edit_loc = ScreenCoords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); } m_angle_callback = std::move(callback); @@ -1155,37 +1157,47 @@ void GUI::sketch_underlay_panel_settings_(const std::shared_ptr& sk) const bool pick_y = m_underlay_calib_phase == Underlay_calib_phase::PickY1 || m_underlay_calib_phase == Underlay_calib_phase::PickY2 || m_underlay_calib_phase == Underlay_calib_phase::AwaitDistY; + const bool pick_datum = m_underlay_calib_phase == Underlay_calib_phase::PickDatumO || + m_underlay_calib_phase == Underlay_calib_phase::PickDatumU; - if (pick_x || pick_y) + if (pick_x || pick_y || pick_datum) { const char* hint = ""; switch (m_underlay_calib_phase) { case Underlay_calib_phase::PickX1: - hint = "Click first point: corner at bitmap (0,0)."; + hint = "First click: bitmap corner (0,0) in the current underlay transform (sliders / rotation)."; break; case Underlay_calib_phase::PickX2: - hint = "Click second point: end of width (bitmap +U), like a line edge."; + hint = "Second click: along bitmap +U from that corner; calibration refines scale from this placement."; break; case Underlay_calib_phase::AwaitDistX: - hint = "Enter the drawing distance for X (dimension popup). Y length is set from image aspect (H:W)."; + hint = + "Enter the drawing distance for X (dimension popup). If Y is not calibrated yet, its scale still follows " + "image aspect."; break; case Underlay_calib_phase::PickY1: - hint = "Click first point along image height (+V)."; + hint = "First click: along bitmap height (+V) using the current underlay transform."; break; case Underlay_calib_phase::PickY2: - hint = "Click second point: end of height (+V)."; + hint = "Second click: farther along +V; calibration refines Y from this placement."; break; case Underlay_calib_phase::AwaitDistY: hint = "Enter the drawing distance for Y (dimension popup)."; break; + case Underlay_calib_phase::PickDatumO: + hint = "Datum: first click places bitmap corner (0,0) on the sketch plane (underlay origin)."; + break; + case Underlay_calib_phase::PickDatumU: + hint = "Datum: second click sets direction along bitmap +U from that origin; half width and height are kept."; + break; default: break; } ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.25f, 1.0f), "%s", hint); } - ImGui::BeginDisabled(!sk_is_cur || pick_y); + ImGui::BeginDisabled(!sk_is_cur || pick_y || pick_datum); if (ImGui::Button("Set X from edge...")) begin_underlay_calib_set_x_(sk); @@ -1193,21 +1205,38 @@ void GUI::sketch_underlay_panel_settings_(const std::shared_ptr& sk) if (m_show_tool_tips && ImGui::IsItemHovered()) ImGui::SetTooltip( "Two clicks along width (+U), then type the real drawing distance (same units as sketch dimensions). " - "Y is set from image aspect until you use Set Y."); + "You can use Set Y before or after; until both are set, the other axis still follows default aspect."); ImGui::SameLine(); - ImGui::BeginDisabled(!sk_is_cur || !m_underlay_calib_have_x || pick_x); + ImGui::BeginDisabled(!sk_is_cur || pick_x || pick_datum); if (ImGui::Button("Set Y from edge...")) begin_underlay_calib_set_y_(sk); ImGui::EndDisabled(); if (m_show_tool_tips && ImGui::IsItemHovered()) - ImGui::SetTooltip("After Set X: two clicks along height (+V), then enter the drawing distance for Y."); + ImGui::SetTooltip( + "Two clicks along height (+V), then enter the drawing distance for Y. Order vs. Set X does not matter."); + + ImGui::BeginDisabled(!sk_is_cur || pick_x || pick_y); + if (ImGui::Button("Define underlay datum...")) + begin_underlay_calib_define_datum_(sk); + + ImGui::EndDisabled(); + if (m_show_tool_tips && ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Two picks on the sketch plane: first sets bitmap corner (0,0); second sets the +U axis direction. " + "Keeps current half width and height; aligns the image to your sketch datum."); } ImGui::Separator(); ImGui::TextUnformatted("Transform (sketch plane, vs. current view)"); { + const bool ul_ortho = sk->underlay_axes_orthogonal(); + if (!ul_ortho) + ImGui::TextWrapped( + "Edge calibration produced shear (bitmap U and V are not perpendicular). These sliders only describe an " + "orthogonal transform; they stay off so they cannot overwrite your calibrated affine."); + const ImGuiIO& io = ImGui::GetIO(); double min_u = 0., min_v = 0., max_u = 1., max_v = 1.; const bool have_view = @@ -1235,7 +1264,12 @@ void GUI::sketch_underlay_panel_settings_(const std::shared_ptr& sk) double rot_max = 180.0; auto apply_ul_xform = [&]() - { sk->underlay_set_center_extents_rotation(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); }; + { + if (!sk->underlay_axes_orthogonal()) + return; + + sk->underlay_set_center_extents_rotation(dvec2(m_ul_cx, m_ul_cy), dvec2(m_ul_hw, m_ul_hh), m_ul_rot); + }; auto transform_slider = [&](const char* label, ImGuiDataType type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags = 0) @@ -1248,6 +1282,21 @@ void GUI::sketch_underlay_panel_settings_(const std::shared_ptr& sk) apply_ul_xform(); }; + auto transform_input_double = [&](const char* label, double* p_data, double v_min, double v_max, + const char* format) + { + const bool changed = ImGui::InputDouble(label, p_data, 0.0, 0.0, format); + if (ImGui::IsItemActivated()) + m_view->push_undo_snapshot(); + + if (changed) + { + *p_data = std::clamp(*p_data, v_min, v_max); + apply_ul_xform(); + } + }; + + ImGui::BeginDisabled(!ul_ortho); // Labels match typical sketch axes on screen: "Center X" drives plane Y (gp_Pnt2d::Y / view v), "Center Y" plane X (u). transform_slider("Center X", ImGuiDataType_Double, &m_ul_cy, &min_v, &max_v, "%.4f", ImGuiSliderFlags_ClampOnInput); transform_slider("Center Y", ImGuiDataType_Double, &m_ul_cx, &min_u, &max_u, "%.4f", ImGuiSliderFlags_ClampOnInput); @@ -1257,6 +1306,8 @@ void GUI::sketch_underlay_panel_settings_(const std::shared_ptr& sk) ImGuiSliderFlags_ClampOnInput | ImGuiSliderFlags_Logarithmic); transform_slider("Rotation (deg)", ImGuiDataType_Double, &m_ul_rot, &rot_min, &rot_max, "%.1f", ImGuiSliderFlags_ClampOnInput); + transform_input_double("Rotation value (deg)", &m_ul_rot, rot_min, rot_max, "%.4f"); + ImGui::EndDisabled(); } } @@ -1267,7 +1318,6 @@ void GUI::cancel_underlay_calib_() m_underlay_calib_sketch_wk.reset(); - m_underlay_calib_have_x = false; m_underlay_calib_axis_u = gp_Vec2d(0., 0.); } @@ -1283,10 +1333,11 @@ void GUI::begin_underlay_calib_set_x_(const std::shared_ptr& sk) } cancel_underlay_calib_(); + sk->underlay_ui_params(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); m_underlay_calib_sketch_wk = sk; m_underlay_calib_phase = Underlay_calib_phase::PickX1; show_message( - "Underlay X: click corner (0,0), then point along +U; you will enter the drawing distance for that span."); + "Underlay X: uses the current transform. Click bitmap corner (0,0), then along +U; then enter the distance."); } void GUI::begin_underlay_calib_set_y_(const std::shared_ptr& sk) @@ -1294,21 +1345,36 @@ void GUI::begin_underlay_calib_set_y_(const std::shared_ptr& sk) if (!sk || !sk->has_underlay()) return; - if (!m_underlay_calib_have_x) + if (m_view->curr_sketch_shared() != sk) { - show_message("Use Set X from edge first (two points along image width)."); + show_message("Make this sketch current in the sketch list, then try again."); return; } + cancel_underlay_calib_(); + sk->underlay_ui_params(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); + m_underlay_calib_sketch_wk = sk; + m_underlay_calib_phase = Underlay_calib_phase::PickY1; + show_message( + "Underlay Y: uses the current transform. Click two points along +V; then enter the drawing distance."); +} + +void GUI::begin_underlay_calib_define_datum_(const std::shared_ptr& sk) +{ + if (!sk || !sk->has_underlay()) + return; + if (m_view->curr_sketch_shared() != sk) { show_message("Make this sketch current in the sketch list, then try again."); return; } + cancel_underlay_calib_(); + sk->underlay_ui_params(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); m_underlay_calib_sketch_wk = sk; - m_underlay_calib_phase = Underlay_calib_phase::PickY1; - show_message("Underlay Y: click two points along +V, then enter the drawing distance for that span."); + m_underlay_calib_phase = Underlay_calib_phase::PickDatumO; + show_message("Datum: click where bitmap corner (0,0) should lie on the sketch plane."); } void GUI::underlay_calib_prompt_x_distance_(const std::shared_ptr& sk) @@ -1316,7 +1382,7 @@ void GUI::underlay_calib_prompt_x_distance_(const std::shared_ptr& sk) m_underlay_calib_phase = Underlay_calib_phase::AwaitDistX; const double L_model = m_underlay_calib_x0.Distance(m_underlay_calib_x1); const float dist_show = static_cast(L_model / m_view->get_dimension_scale()); - const ScreenCoords spos(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords spos(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); std::weak_ptr wk = sk; auto on_dist = [this, wk](float new_dist, bool is_final) @@ -1354,10 +1420,11 @@ void GUI::underlay_calib_prompt_x_distance_(const std::shared_ptr& sk) s->underlay_ui_params(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); m_underlay_panel_sketch = nullptr; - m_dist_callback = nullptr; - m_underlay_calib_phase = Underlay_calib_phase::None; - m_underlay_calib_have_x = true; - show_message("X scale applied to the picked segment. Use Set Y from edge for the vertical span and its distance."); + m_dist_callback = nullptr; + m_underlay_calib_phase = Underlay_calib_phase::None; + show_message( + "X distance applied to the picked segment. Use Set Y from edge for the vertical span if needed, or adjust " + "transforms."); }; set_dist_edit(dist_show, std::move(on_dist), spos); @@ -1368,7 +1435,7 @@ void GUI::underlay_calib_prompt_y_distance_(const std::shared_ptr& sk) m_underlay_calib_phase = Underlay_calib_phase::AwaitDistY; const double L_model = m_underlay_calib_y0.Distance(m_underlay_calib_y1); const float dist_show = static_cast(L_model / m_view->get_dimension_scale()); - const ScreenCoords spos(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords spos(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); std::weak_ptr wk = sk; auto on_dist = [this, wk](float new_dist, bool is_final) @@ -1408,7 +1475,9 @@ void GUI::underlay_calib_prompt_y_distance_(const std::shared_ptr& sk) m_dist_callback = nullptr; cancel_underlay_calib_(); - show_message("Underlay X/Y calibration complete."); + show_message( + "Y distance applied to the picked segment. Use Set X from edge for the horizontal span if needed, or adjust " + "transforms."); }; set_dist_edit(dist_show, std::move(on_dist), spos); @@ -1487,6 +1556,34 @@ bool GUI::try_underlay_calib_click_(const ScreenCoords& screen_coords) underlay_calib_prompt_y_distance_(sk); return true; + case Underlay_calib_phase::PickDatumO: + m_underlay_calib_datum_o = *pt; + m_underlay_calib_phase = Underlay_calib_phase::PickDatumU; + show_message("Datum: click a second point along bitmap +U from that origin (direction only)."); + return true; + + case Underlay_calib_phase::PickDatumU: + if (too_short(m_underlay_calib_datum_o, *pt)) + { + show_message("Datum direction segment too short."); + return true; + } + + m_view->push_undo_snapshot(); + if (!sk->underlay_set_datum_origin_and_u_direction(m_underlay_calib_datum_o, *pt)) + { + m_view->pop_undo_snapshot(); + show_message("Could not set underlay datum (degenerate direction or axes)."); + cancel_underlay_calib_(); + return true; + } + + sk->underlay_ui_params(m_ul_cx, m_ul_cy, m_ul_hw, m_ul_hh, m_ul_rot); + m_underlay_panel_sketch = nullptr; + cancel_underlay_calib_(); + show_message("Underlay datum set: origin at (0,0) and +U direction; half sizes unchanged."); + return true; + default: return false; } @@ -2038,7 +2135,7 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) void GUI::on_mouse_button(int button, int action, int mods) { - const ScreenCoords screen_coords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords screen_coords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS && mods == 0) if (try_underlay_calib_click_(screen_coords)) diff --git a/src/gui.h b/src/gui.h index f0aafb5..7843d84 100644 --- a/src/gui.h +++ b/src/gui.h @@ -40,7 +40,10 @@ enum class Underlay_calib_phase : std::uint8_t AwaitDistX, PickY1, PickY2, - AwaitDistY + AwaitDistY, + /// Two clicks: bitmap corner (0,0), then a point along bitmap +U (datum / origin on plane). + PickDatumO, + PickDatumU }; /// One entry under File > Examples (menu label + path to `.ezy` on disk). @@ -203,6 +206,7 @@ class GUI bool try_underlay_calib_click_(const ScreenCoords& screen_coords); void begin_underlay_calib_set_x_(const std::shared_ptr& sk); void begin_underlay_calib_set_y_(const std::shared_ptr& sk); + void begin_underlay_calib_define_datum_(const std::shared_ptr& sk); void underlay_calib_prompt_x_distance_(const std::shared_ptr& sk); void underlay_calib_prompt_y_distance_(const std::shared_ptr& sk); #if defined(__EMSCRIPTEN__) @@ -326,12 +330,12 @@ class GUI void* m_underlay_panel_sketch {nullptr}; Underlay_calib_phase m_underlay_calib_phase {Underlay_calib_phase::None}; std::weak_ptr m_underlay_calib_sketch_wk {}; - bool m_underlay_calib_have_x {false}; gp_Pnt2d m_underlay_calib_x0 {}; gp_Pnt2d m_underlay_calib_x1 {}; gp_Vec2d m_underlay_calib_axis_u {}; // After X distance (model units) gp_Pnt2d m_underlay_calib_y0 {}; gp_Pnt2d m_underlay_calib_y1 {}; + gp_Pnt2d m_underlay_calib_datum_o {}; bool m_sketch_properties_open {false}; std::weak_ptr m_sketch_properties_sketch; /// If set, next underlay import (menu or async) applies to this sketch; otherwise current sketch. diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 2d08433..5ef3121 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -19,6 +19,8 @@ #include "sketch.h" #include "utl_occt.h" +using namespace glm; + namespace { @@ -99,7 +101,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) if (action != GLFW_PRESS) return; - const ScreenCoords screen_coords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords screen_coords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); bool ctrl_pressed = (mods & GLFW_MOD_CONTROL) != 0; if (ctrl_pressed) @@ -523,7 +525,7 @@ void GUI::options_shape_polar_duplicate_mode_() void GUI::on_key_rotate_mode_(int key) { - const ScreenCoords screen_coords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords screen_coords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); switch (key) { @@ -567,7 +569,7 @@ void GUI::on_key_rotate_mode_(int key) void GUI::on_key_move_mode_(int key) { Move_options& opts = m_view->shp_move().get_opts(); - const ScreenCoords screen_coords(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + const ScreenCoords screen_coords(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); switch (key) { diff --git a/src/main.cpp b/src/main.cpp index 28a5a40..5888e9c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -88,6 +88,8 @@ void scroll_callback_wrapper(GLFWwindow* window, double xoffset, double yoffset) scroll_callback(window, xoffset, yoffset); } +using namespace glm; + // Main code int main(int, char**) { @@ -221,7 +223,7 @@ int main(int, char**) { ImGui_ImplGlfw_CursorPosCallback(window, xpos, ypos); if (!io.WantCaptureMouse) - gui.on_mouse_pos(ScreenCoords(glm::dvec2(xpos, ypos))); + gui.on_mouse_pos(ScreenCoords(dvec2(xpos, ypos))); }; mouseButtonCallback = [&](GLFWwindow* window, int button, int action, int mods) diff --git a/src/occt_view.cpp b/src/occt_view.cpp index ae23da7..1848765 100644 --- a/src/occt_view.cpp +++ b/src/occt_view.cpp @@ -1,5 +1,4 @@ #include "occt_view.h" -#include "ply_io.h" #include #include @@ -28,20 +27,20 @@ #include #include #include +#include +#include #include "dbg.h" #include "geom.h" -#include "types.h" -#include "utl.h" - -#include -#include -#include "utl_occt.h" #include "gui.h" +#include "ply_io.h" #include "shp_create.h" #include "sketch.h" #include "sketch_json.h" +#include "types.h" +#include "utl.h" #include "utl_json.h" +#include "utl_occt.h" #ifdef __EMSCRIPTEN__ #include @@ -58,6 +57,8 @@ #include #include +using namespace glm; + Occt_view::Occt_view(GUI& gui) : m_gui(gui), m_shp_move(*this), m_shp_rotate(*this), m_shp_scale(*this), m_shp_chamfer(*this), m_shp_fillet(*this), m_shp_cut(*this), m_shp_fuse(*this), m_shp_common(*this), m_shp_polar_dup(*this), m_shp_extrude(*this) { @@ -160,7 +161,7 @@ void Occt_view::init_viewer() aLightIter.More(); aLightIter.Next()) { - const Handle(V3d_Light) & aLight = aLightIter.Value(); + const Handle(V3d_Light)& aLight = aLightIter.Value(); if (aLight->Type() == Graphic3d_TypeOfLightSource_Directional) aLight->SetCastShadows(true); } @@ -177,8 +178,8 @@ void Occt_view::init_viewer() // dimensions, etc. (CSS "serif" / OS fonts are not exposed as paths to WASM.) // Preload matches CMake --preload-file ... DroidSans.ttf@/DroidSans.ttf (shared with ImGui). { - Handle(Font_FontMgr) font_mgr = Font_FontMgr::GetInstance(); - Handle(Font_SystemFont) sys_font = font_mgr->CheckFont("/DroidSans.ttf"); + Handle(Font_FontMgr) font_mgr = Font_FontMgr::GetInstance(); + Handle(Font_SystemFont) sys_font = font_mgr->CheckFont("/DroidSans.ttf"); if (!sys_font.IsNull()) { font_mgr->RegisterFont(sys_font, Standard_True); @@ -338,7 +339,7 @@ ScreenCoords Occt_view::get_screen_coords(const gp_Pnt& point) Standard_Integer screen_x; Standard_Integer screen_y; m_view->Convert(point.X(), point.Y(), point.Z(), screen_x, screen_y); - return ScreenCoords(glm::dvec2(screen_x, screen_y)); + return ScreenCoords(dvec2(screen_x, screen_y)); } std::optional Occt_view::pt3d_on_plane(const ScreenCoords& screen_coords, const gp_Pln& plane) const @@ -588,11 +589,16 @@ bool Occt_view::sketch_plane_view_aabb_2d(const gp_Pln& pln, double display_w, d struct { double x, y; - } corners[4] = {{x0, y0}, {x1, y0}, {x1, y1}, {x0, y1}}; + } corners[4] = { + {x0, y0}, + {x1, y0}, + {x1, y1}, + {x0, y1} + }; - bool any = false; - double min_u = 0., min_v = 0., max_u = 0., max_v = 0.; - const auto consider = [&](const std::optional& p2) + bool any = false; + double min_u = 0., min_v = 0., max_u = 0., max_v = 0.; + const auto consider = [&](const std::optional& p2) { if (!p2) return; @@ -602,7 +608,7 @@ bool Occt_view::sketch_plane_view_aabb_2d(const gp_Pln& pln, double display_w, d { min_u = max_u = u; min_v = max_v = v; - any = true; + any = true; } else { @@ -614,7 +620,7 @@ bool Occt_view::sketch_plane_view_aabb_2d(const gp_Pln& pln, double display_w, d }; for (const auto& c : corners) - consider(pt_on_plane(ScreenCoords(glm::dvec2(c.x, c.y)), pln)); + consider(pt_on_plane(ScreenCoords(dvec2(c.x, c.y)), pln)); if (!any) return false; @@ -1188,7 +1194,7 @@ void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) if (get_mode() == Mode::Sketch_from_planar_face && theButton == GLFW_MOUSE_BUTTON_LEFT) { flush_view_events(); - create_sketch_from_planar_face_(ScreenCoords(glm::dvec2(pos.x(), pos.y()))); + create_sketch_from_planar_face_(ScreenCoords(dvec2(pos.x(), pos.y()))); m_planar_face_lmb_skipped_view_controller = true; return; } @@ -1207,7 +1213,7 @@ void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) switch (get_mode()) { case Mode::Sketch_dim_anno: - return curr_sketch().toggle_edge_dim_anno(ScreenCoords(glm::dvec2(pos.x(), pos.y()))); + return curr_sketch().toggle_edge_dim_anno(ScreenCoords(dvec2(pos.x(), pos.y()))); default: break; @@ -1269,7 +1275,7 @@ void Occt_view::set_shp_selection_mode(const TopAbs_ShapeEnum mode) if (m_shp_selection_mode == mode) return; - m_shp_selection_mode = mode; + m_shp_selection_mode = mode; const std::size_t idx = static_cast(mode); if (idx < c_names_TopAbs_ShapeEnum.size()) m_gui.log_message(std::string("Selection mode: ") + std::string(c_names_TopAbs_ShapeEnum[idx])); @@ -1283,6 +1289,7 @@ const Graphic3d_MaterialAspect& Occt_view::get_default_material() const { return m_default_material; } + void Occt_view::set_default_material(const Graphic3d_MaterialAspect& mat) { m_default_material = mat; @@ -1512,6 +1519,7 @@ Shp_fuse& Occt_view::shp_fuse() { return m_shp_fuse; } Shp_common& Occt_view::shp_common() { return m_shp_common; } Shp_polar_dup& Occt_view::shp_polar_dup() { return m_shp_polar_dup; } Shp_extrude& Occt_view::shp_extrude() { return m_shp_extrude; } + // clang-format on // --------------------------------------------------------------------------- @@ -1612,7 +1620,7 @@ constexpr int k_ezy_file_format_version = 2; std::string Occt_view::to_json() const { using namespace nlohmann; - json j; + json j; j["ezyFormat"] = k_ezy_file_format_version; json& sketches = j["sketches"] = json::array(); json& shps = j["shapes"] = json::array(); @@ -1678,7 +1686,7 @@ void Occt_view::load(const std::string& json_str, bool restore_view) clear_all(m_sketches, m_cur_sketch, m_shps); const json j = json::parse(json_str); - (void)j.value("ezyFormat", 1); // Reserved for future migrations; sketch JSON migrates per-edge dim flags in Sketch_json. + (void) j.value("ezyFormat", 1); // Reserved for future migrations; sketch JSON migrates per-edge dim flags in Sketch_json. EZY_ASSERT(j.contains("sketches") && j["sketches"].is_array()); for (const auto& s : j["sketches"]) { @@ -1875,7 +1883,7 @@ Status Occt_view::import_step(const std::string& step_data) if (reader.TransferRoots() == 0) return Status::user_error("STEP: no geometry was transferred from the file."); - const Standard_Integer num_shps = reader.NbShapes(); + const Standard_Integer num_shps = reader.NbShapes(); std::vector to_add; to_add.reserve(static_cast(num_shps)); for (Standard_Integer i = 1; i <= num_shps; ++i) @@ -1919,6 +1927,7 @@ GUI& Occt_view::gui() { return m_gui; } + AIS_InteractiveContext& Occt_view::ctx() { return *m_ctx; diff --git a/src/sketch.cpp b/src/sketch.cpp index 530a7f6..68baeba 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -31,6 +31,8 @@ #include "sketch_underlay.h" #include "utl.h" +using namespace glm; + Sketch::Sketch(const std::string& name, Occt_view& view, const gp_Pln& pln) : m_view(view), m_ctx(view.ctx()), m_pln(pln), m_name(name), m_nodes(view, pln) { @@ -518,7 +520,7 @@ void Sketch::move_line_string_pt_(const ScreenCoords& screen_coords) m_show_angle_input = !is_finial; // Recalculate the point with the new angle using current mouse position // Get current mouse position from ImGui - ScreenCoords current_pos(glm::dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); + ScreenCoords current_pos(dvec2(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y)); sketch_pt_move(current_pos); }; @@ -694,9 +696,9 @@ double Sketch::plane_pick_snap_radius_world_() const const gp_Pnt2d ref2(0.0, 0.0); const ScreenCoords s0 = m_view.get_screen_coords(to_3d(m_pln, ref2)); - const glm::dvec2 base = s0.unsafe_get(); + const dvec2 base = s0.unsafe_get(); std::optional p1 = - m_view.pt_on_plane(ScreenCoords(glm::dvec2(base.x + px, base.y)), m_pln); + m_view.pt_on_plane(ScreenCoords(dvec2(base.x + px, base.y)), m_pln); if (!p1) return std::max(px, Precision::Confusion() * 1e9); return ref2.Distance(*p1); @@ -2663,28 +2665,55 @@ gp_Vec2d Sketch::underlay_axis_u_vec() const return m_underlay->axis_u(); } -void Sketch::underlay_set_center_extents_rotation(double cx, double cy, double half_w, double half_h, double rot_deg) +bool Sketch::underlay_set_datum_origin_and_u_direction(const gp_Pnt2d& origin, const gp_Pnt2d& along_u_point) { if (!m_underlay || !m_underlay->has_image()) - return; - constexpr double k_min = 1e-9; - if (half_w < k_min) - half_w = k_min; - if (half_h < k_min) - half_h = k_min; - - const double rad = rot_deg * (std::numbers::pi / 180.0); - const double c = std::cos(rad); - const double s = std::sin(rad); - const gp_Vec2d axis_u(2.0 * half_w * c, 2.0 * half_w * s); - const gp_Vec2d axis_v(-2.0 * half_h * s, 2.0 * half_h * c); - const gp_Pnt2d base( - cx - 0.5 * (axis_u.X() + axis_v.X()), - cy - 0.5 * (axis_u.Y() + axis_v.Y())); + return false; + + gp_Vec2d du(along_u_point.X() - origin.X(), along_u_point.Y() - origin.Y()); + const double du_len = du.Magnitude(); + const gp_Vec2d au_old = m_underlay->axis_u(); + const gp_Vec2d av_old = m_underlay->axis_v(); + const double len_u = au_old.Magnitude(); + const double len_v = av_old.Magnitude(); + if (len_u <= 1e-12 || len_v <= 1e-12 || du_len <= 1e-12) + return false; + + du.Multiply(1.0 / du_len); + const double ux = du.X(); + const double uy = du.Y(); + + const double det_old = au_old.X() * av_old.Y() - au_old.Y() * av_old.X(); + double px {}; + double py {}; + if (det_old >= 0.0) + { + px = -uy; + py = ux; + } + else + { + px = uy; + py = -ux; + } + + const gp_Vec2d au_new(len_u * ux, len_u * uy); + const gp_Vec2d av_new(len_v * px, len_v * py); + + underlay_set_affine_plane(origin, au_new, av_new); + return true; +} + +void Sketch::underlay_set_center_extents_rotation(const dvec2& center, const dvec2& half_extents, + double rot_deg) +{ + EZY_ASSERT(m_underlay); + EZY_ASSERT(m_underlay->has_image()); + EZY_ASSERT(m_visible); + + m_underlay->set_center_extents_rotation(center, half_extents, rot_deg); + m_underlay->rebuild_and_display(m_pln, m_ctx); - m_underlay->set_affine(base, axis_u, axis_v); - if (m_visible) - m_underlay->rebuild_and_display(m_pln, m_ctx); m_ctx.UpdateCurrentViewer(); } @@ -2778,6 +2807,19 @@ void Sketch::underlay_ui_params(double& cx, double& cy, double& half_w, double& rot_deg = std::atan2(au.Y(), au.X()) * (180.0 / std::numbers::pi); } +bool Sketch::underlay_axes_orthogonal() const +{ + if (!m_underlay || !m_underlay->has_image()) + return true; + const gp_Vec2d au = m_underlay->axis_u(); + const gp_Vec2d av = m_underlay->axis_v(); + const double dot = au.X() * av.X() + au.Y() * av.Y(); + const double scale = au.Magnitude() * av.Magnitude(); + if (scale < 1e-24) + return true; + return std::abs(dot) < 1e-9 * scale; +} + void Sketch::underlay_rebuild_display() { if (!m_underlay || !m_underlay->has_image()) diff --git a/src/sketch.h b/src/sketch.h index d8d0f13..4d0309a 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -128,7 +128,7 @@ class Sketch [[nodiscard]] int underlay_image_h() const; [[nodiscard]] bool load_underlay_image(const std::string& file_bytes); void clear_underlay(); - void underlay_set_center_extents_rotation(double cx, double cy, double half_w, double half_h, double rot_deg); + void underlay_set_center_extents_rotation(const glm::dvec2& center, const glm::dvec2& half_extents, double rot_deg); void underlay_set_opacity(float opaque01); void underlay_set_visible(bool v); [[nodiscard]] float underlay_opacity() const; @@ -140,6 +140,8 @@ class Sketch [[nodiscard]] bool underlay_line_tint_enabled() const; void underlay_line_tint_rgb(uint8_t& r, uint8_t& g, uint8_t& b) const; void underlay_ui_params(double& cx, double& cy, double& half_w, double& half_h, double& rot_deg) const; + /// True when texture U and V directions are perpendicular (no shear). Orthogonal UI assumes this. + [[nodiscard]] bool underlay_axes_orthogonal() const; void underlay_rebuild_display(); /// Same snap / plane rules as the line-edge tool (for underlay calibration clicks). @@ -151,6 +153,8 @@ class Sketch /// Keep U axis; adjust V and base so segment \a y0-\a y1 has length \a target_len (after X calibration). [[nodiscard]] bool underlay_rescale_v_chord_to_length(const gp_Pnt2d& y0, const gp_Pnt2d& y1, double target_len); [[nodiscard]] gp_Vec2d underlay_axis_u_vec() const; + /// Datum on sketch plane: bitmap (0,0) at \a origin; +U toward \a along_u_point; keeps |axis_u|, |axis_v| and winding. + [[nodiscard]] bool underlay_set_datum_origin_and_u_direction(const gp_Pnt2d& origin, const gp_Pnt2d& along_u_point); // private: friend class Sketch_json; diff --git a/src/sketch_underlay.cpp b/src/sketch_underlay.cpp index 05056a8..ecbccce 100644 --- a/src/sketch_underlay.cpp +++ b/src/sketch_underlay.cpp @@ -1,7 +1,5 @@ #include "sketch_underlay.h" -#include "geom.h" - #include #include #include @@ -10,32 +8,39 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include +#include -#include +#include "geom.h" + +using namespace glm; namespace { -constexpr int k_max_image_dim = 8192; +constexpr int k_max_image_dim = 8192; constexpr std::size_t k_max_rgba_bytes = 64u * 1024u * 1024u; // 64 MiB safety cap std::string base64_encode(const uint8_t* data, std::size_t len) { static const char tbl[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; + std::string out; out.reserve(((len + 2) / 3) * 4); for (std::size_t i = 0; i < len; i += 3) { - const std::size_t n = len - i; - const unsigned b0 = data[i]; - const unsigned b1 = n > 1 ? data[i + 1] : 0u; - const unsigned b2 = n > 2 ? data[i + 2] : 0u; - const unsigned triple = (b0 << 16) | (b1 << 8) | b2; + const std::size_t n = len - i; + const unsigned b0 = data[i]; + const unsigned b1 = n > 1 ? data[i + 1] : 0u; + const unsigned b2 = n > 2 ? data[i + 2] : 0u; + const unsigned triple = (b0 << 16) | (b1 << 8) | b2; out.push_back(tbl[(triple >> 18) & 63]); out.push_back(tbl[(triple >> 12) & 63]); out.push_back(n > 1 ? tbl[(triple >> 6) & 63] : '='); @@ -68,7 +73,7 @@ bool base64_decode(const std::string& in, std::vector& out) while (len > 0 && (in[len - 1] == '=' || in[len - 1] == '\n' || in[len - 1] == '\r' || in[len - 1] == ' ')) --len; out.reserve((len * 3) / 4); - unsigned buf = 0; + unsigned buf = 0; int bits = 0; for (std::size_t i = 0; i < len; ++i) { @@ -123,12 +128,12 @@ inline void sample_rgba_bilinear(const uint8_t* rgba, int w, int h, double xf, d out[0] = out[1] = out[2] = out[3] = 0; return; } - const int x0 = static_cast(std::floor(xf)); - const int y0 = static_cast(std::floor(yf)); - const int x1 = std::min(x0 + 1, w - 1); - const int y1 = std::min(y0 + 1, h - 1); - const double tx = xf - static_cast(x0); - const double ty = yf - static_cast(y0); + const int x0 = static_cast(std::floor(xf)); + const int y0 = static_cast(std::floor(yf)); + const int x1 = std::min(x0 + 1, w - 1); + const int y1 = std::min(y0 + 1, h - 1); + const double tx = xf - static_cast(x0); + const double ty = yf - static_cast(y0); const auto pix = [&](int x, int y) -> const uint8_t* { return rgba + (static_cast(y) * static_cast(w) + static_cast(x)) * 4u; @@ -145,19 +150,56 @@ inline void sample_rgba_bilinear(const uint8_t* rgba, int w, int h, double xf, d } } -/// Builds a bottom-up pixmap for AIS_TexturedShape. \a axis_u / \a axis_v span the bitmap (2*half-width x 2*half-height). -/// Uses an axis-aligned face in the sketch plane and inverse-rotated sampling so rotation matches OCCT texture mapping -/// (which normalizes surface UV to an axis-aligned box and would shear a parallelogram face). -Handle(Image_PixMap) make_pixmap_bottom_up_rgba(const uint8_t* rgba, - int w, - int h, - const gp_Vec2d& axis_u, - const gp_Vec2d& axis_v, - bool key_white_transparent, - bool line_tint_enabled, - uint8_t tr, - uint8_t tg, - uint8_t tb) +/// Straight copy of the image to a bottom-up pixmap (optional row flip for OCCT/OpenGL), with key + tint. +Handle(Image_PixMap) make_pixmap_bottom_up_linear(const uint8_t* rgba, + int w, + int h, + bool key_white_transparent, + bool line_tint_enabled, + uint8_t tr, + uint8_t tg, + uint8_t tb) +{ + if (w <= 0 || h <= 0) + return {}; + + Handle(Image_PixMap) pix = new Image_PixMap(); + if (!pix->InitTrash(Image_Format_RGBA, static_cast(w), static_cast(h))) + return {}; + + const Standard_Size rowBytes = static_cast(w) * 4u; + uint8_t* dst = pix->ChangeData(); + + for (int rj = 0; rj < h; ++rj) + { + const int src_row = h - 1 - rj; + const uint8_t* srcRow = rgba + static_cast(src_row) * static_cast(w) * 4u; + uint8_t* dstRow = dst + static_cast(rj) * rowBytes; + for (int ox = 0; ox < w; ++ox) + { + uint8_t px[4] = {srcRow[ox * 4 + 0], srcRow[ox * 4 + 1], srcRow[ox * 4 + 2], srcRow[ox * 4 + 3]}; + apply_key_and_tint(px[0], px[1], px[2], px[3], key_white_transparent, line_tint_enabled, tr, tg, tb); + dstRow[static_cast(ox) * 4u + 0] = px[0]; + dstRow[static_cast(ox) * 4u + 1] = px[1]; + dstRow[static_cast(ox) * 4u + 2] = px[2]; + dstRow[static_cast(ox) * 4u + 3] = px[3]; + } + } + return pix; +} + +/// Builds a bottom-up pixmap for AIS_TexturedShape when the underlay axes are sheared (non-orthogonal): uses an +/// axis-aligned face in the sketch plane and inverse-rotated sampling so the bitmap matches OCCT UV on the AABB. +Handle(Image_PixMap) make_pixmap_bottom_up_warped(const uint8_t* rgba, + int w, + int h, + const gp_Vec2d& axis_u, + const gp_Vec2d& axis_v, + bool key_white_transparent, + bool line_tint_enabled, + uint8_t tr, + uint8_t tg, + uint8_t tb) { const double hw = 0.5 * axis_u.Magnitude(); const double hh = 0.5 * axis_v.Magnitude(); @@ -186,22 +228,20 @@ Handle(Image_PixMap) make_pixmap_bottom_up_rgba(const uint8_t* rgba, for (int rj = 0; rj < out_h; ++rj) { // Bottom row rj = 0 -> bottom of texture (small plane dv). - const double t_b = (static_cast(rj) + 0.5) / static_cast(out_h); - const double dv = (2.0 * t_b - 1.0) * hy; + const double t_b = (static_cast(rj) + 0.5) / static_cast(out_h); + const double dv = (2.0 * t_b - 1.0) * hy; uint8_t* dstRow = dst + static_cast(rj) * rowBytes; for (int ox = 0; ox < out_w; ++ox) { - const double s01 = (static_cast(ox) + 0.5) / static_cast(out_w); - const double du = (2.0 * s01 - 1.0) * hx; + const double s01 = (static_cast(ox) + 0.5) / static_cast(out_w); + const double du = (2.0 * s01 - 1.0) * hx; // Inverse rotate from plane (du,dv) to image (u,v) offsets from quad center. const double img_u = du * c + dv * s; const double img_v = -du * s + dv * c; uint8_t px[4]; if (std::abs(img_u) > hw + k_eps || std::abs(img_v) > hh + k_eps) - { px[0] = px[1] = px[2] = px[3] = 0; - } else { const double sx = (img_u + hw) / (2.0 * hw) * static_cast(w - 1); @@ -209,6 +249,7 @@ Handle(Image_PixMap) make_pixmap_bottom_up_rgba(const uint8_t* rgba, const double sy = (hh - img_v) / (2.0 * hh) * static_cast(h - 1); sample_rgba_bilinear(rgba, w, h, sx, sy, px); } + apply_key_and_tint(px[0], px[1], px[2], px[3], key_white_transparent, line_tint_enabled, tr, tg, tb); const Standard_Size o = static_cast(ox) * 4u; dstRow[o + 0] = px[0]; @@ -237,8 +278,10 @@ bool Sketch_underlay::set_image_rgba(std::vector&& rgba, int w, int h) { if (w <= 0 || h <= 0 || rgba.size() < static_cast(w) * static_cast(h) * 4u) return false; + if (w > k_max_image_dim || h > k_max_image_dim) return false; + if (rgba.size() > k_max_rgba_bytes) return false; @@ -261,6 +304,32 @@ void Sketch_underlay::set_affine(const gp_Pnt2d& base, const gp_Vec2d& axis_u, c m_axis_v = axis_v; } +void Sketch_underlay::set_center_extents_rotation(const dvec2& center, const dvec2& half_extents, double rot_deg) +{ + if (!has_image()) + return; + + double half_w = half_extents.x; + double half_h = half_extents.y; + + constexpr double k_min = 1e-9; + if (half_w < k_min) + half_w = k_min; + if (half_h < k_min) + half_h = k_min; + + const double rad = rot_deg * (std::numbers::pi / 180.0); + const double c = std::cos(rad); + const double s = std::sin(rad); + const gp_Vec2d axis_u(2.0 * half_w * c, 2.0 * half_w * s); + const gp_Vec2d axis_v(-2.0 * half_h * s, 2.0 * half_h * c); + const gp_Pnt2d base( + center.x - 0.5 * (axis_u.X() + axis_v.X()), + center.y - 0.5 * (axis_u.Y() + axis_v.Y())); + + set_affine(base, axis_u, axis_v); +} + void Sketch_underlay::set_opacity(float opaque01) { if (opaque01 < 0.f) @@ -310,55 +379,90 @@ void Sketch_underlay::build_ais_(const gp_Pln& pln, AIS_InteractiveContext& ctx) gp_Vec nudge(pln.Axis().Direction()); nudge.Multiply(-10.0 * Precision::Confusion()); - // Axis-aligned rectangle in sketch-plane coords matching the rotated image's bounding box. - // (Parallelogram geometry + default AIS_TexturedShape UV mapping would shear the bitmap instead of rotating it.) - const double hw = 0.5 * m_axis_u.Magnitude(); - const double hh = 0.5 * m_axis_v.Magnitude(); - const double theta = std::atan2(m_axis_u.Y(), m_axis_u.X()); - const double c = std::cos(theta); - const double s = std::sin(theta); - const double hx = std::max(std::abs(hw * c) + std::abs(hh * s), 1e-12); - const double hy = std::max(std::abs(hw * s) + std::abs(hh * c), 1e-12); - const double cx = m_base.X() + 0.5 * (m_axis_u.X() + m_axis_v.X()); - const double cy = m_base.Y() + 0.5 * (m_axis_u.Y() + m_axis_v.Y()); - - const gp_Pnt2d c00(cx - hx, cy - hy); - const gp_Pnt2d c10(cx + hx, cy - hy); - const gp_Pnt2d c11(cx + hx, cy + hy); - const gp_Pnt2d c01(cx - hx, cy + hy); - - const gp_Pnt p00 = to_3d(pln, c00).Translated(nudge); - const gp_Pnt p10 = to_3d(pln, c10).Translated(nudge); - const gp_Pnt p11 = to_3d(pln, c11).Translated(nudge); - const gp_Pnt p01 = to_3d(pln, c01).Translated(nudge); - - BRepBuilderAPI_MakeWire wireMk; - wireMk.Add(BRepBuilderAPI_MakeEdge(p00, p10).Edge()); - wireMk.Add(BRepBuilderAPI_MakeEdge(p10, p11).Edge()); - wireMk.Add(BRepBuilderAPI_MakeEdge(p11, p01).Edge()); - wireMk.Add(BRepBuilderAPI_MakeEdge(p01, p00).Edge()); - if (!wireMk.IsDone()) + TopoDS_Face face; + + const gp_Pnt2d bu2(m_base.X() + m_axis_u.X(), m_base.Y() + m_axis_u.Y()); + const gp_Pnt2d bv2(m_base.X() + m_axis_v.X(), m_base.Y() + m_axis_v.Y()); + const gp_Pnt P0 = to_3d(pln, m_base).Translated(nudge); + const gp_Pnt Pu = to_3d(pln, bu2).Translated(nudge); + const gp_Pnt Pv = to_3d(pln, bv2).Translated(nudge); + gp_Vec Du(P0, Pu); + const double du_len = Du.Magnitude(); + const double dv_len = gp_Vec(P0, Pv).Magnitude(); + if (du_len <= Precision::Confusion() || dv_len <= Precision::Confusion()) return; - BRepBuilderAPI_MakeFace faceMk(wireMk.Wire(), Standard_True); - if (!faceMk.IsDone()) - return; + const auto axes_orthogonal = [](const gp_Vec2d& au, const gp_Vec2d& av) -> bool + { + const double dot = au.X() * av.X() + au.Y() * av.Y(); + const double scale = au.Magnitude() * av.Magnitude(); + if (scale < 1e-24) + return true; + return std::abs(dot) < 1e-9 * scale; + }; + + Handle(Image_PixMap) pix; + + if (axes_orthogonal(m_axis_u, m_axis_v)) + { + // Rotation + uniform scale: build a rectangular face in a plane whose U/V match bitmap axes so texture is not + // sheared (no inverse-resample pixmap needed). + const gp_Dir Zpl = pln.Position().Direction(); + const gp_Dir Xu(Du); + const gp_Ax3 ax3(P0, Zpl, Xu); + const gp_Pln local_pln(ax3); + BRepBuilderAPI_MakeFace faceMk(local_pln, 0.0, du_len, 0.0, dv_len); + if (!faceMk.IsDone()) + return; + face = faceMk.Face(); + + pix = make_pixmap_bottom_up_linear(m_rgba.data(), m_w, m_h, m_key_white_transparent, m_line_tint_enabled, + m_tint_r, m_tint_g, m_tint_b); + } + else + { + // Sheared affine (e.g. after Y-from-edge calibration): axis-aligned bbox + inverse-rotated pixmap resample. + const double hw = 0.5 * m_axis_u.Magnitude(); + const double hh = 0.5 * m_axis_v.Magnitude(); + const double theta = std::atan2(m_axis_u.Y(), m_axis_u.X()); + const double c = std::cos(theta); + const double s = std::sin(theta); + const double hx = std::max(std::abs(hw * c) + std::abs(hh * s), 1e-12); + const double hy = std::max(std::abs(hw * s) + std::abs(hh * c), 1e-12); + const double cx = m_base.X() + 0.5 * (m_axis_u.X() + m_axis_v.X()); + const double cy = m_base.Y() + 0.5 * (m_axis_u.Y() + m_axis_v.Y()); + + const gp_Pnt2d c00(cx - hx, cy - hy); + const gp_Pnt2d c10(cx + hx, cy - hy); + const gp_Pnt2d c11(cx + hx, cy + hy); + const gp_Pnt2d c01(cx - hx, cy + hy); + + const gp_Pnt p00 = to_3d(pln, c00).Translated(nudge); + const gp_Pnt p10 = to_3d(pln, c10).Translated(nudge); + const gp_Pnt p11 = to_3d(pln, c11).Translated(nudge); + const gp_Pnt p01 = to_3d(pln, c01).Translated(nudge); + + BRepBuilderAPI_MakeWire wireMk; + wireMk.Add(BRepBuilderAPI_MakeEdge(p00, p10).Edge()); + wireMk.Add(BRepBuilderAPI_MakeEdge(p10, p11).Edge()); + wireMk.Add(BRepBuilderAPI_MakeEdge(p11, p01).Edge()); + wireMk.Add(BRepBuilderAPI_MakeEdge(p01, p00).Edge()); + if (!wireMk.IsDone()) + return; + + BRepBuilderAPI_MakeFace faceMk(wireMk.Wire(), Standard_True); + if (!faceMk.IsDone()) + return; + face = faceMk.Face(); + + pix = make_pixmap_bottom_up_warped(m_rgba.data(), m_w, m_h, m_axis_u, m_axis_v, m_key_white_transparent, + m_line_tint_enabled, m_tint_r, m_tint_g, m_tint_b); + } - Handle(Image_PixMap) pix = make_pixmap_bottom_up_rgba( - m_rgba.data(), - m_w, - m_h, - m_axis_u, - m_axis_v, - m_key_white_transparent, - m_line_tint_enabled, - m_tint_r, - m_tint_g, - m_tint_b); if (pix.IsNull()) return; - m_ais = new AIS_TexturedShape(faceMk.Face()); + m_ais = new AIS_TexturedShape(face); m_ais->SetTexturePixMap(pix); m_ais->SetTextureMapOn(); m_ais->DisableTextureModulate(); @@ -402,12 +506,21 @@ nlohmann::json Sketch_underlay::to_json() const json j; if (!has_image()) return j; - j["rgba_b64"] = base64_encode(m_rgba.data(), m_rgba.size()); - j["w"] = m_w; - j["h"] = m_h; - j["base"] = json::object({{"x", m_base.X()}, {"y", m_base.Y()}}); - j["axis_u"] = json::object({{"x", m_axis_u.X()}, {"y", m_axis_u.Y()}}); - j["axis_v"] = json::object({{"x", m_axis_v.X()}, {"y", m_axis_v.Y()}}); + j["rgba_b64"] = base64_encode(m_rgba.data(), m_rgba.size()); + j["w"] = m_w; + j["h"] = m_h; + j["base"] = json::object({ + {"x", m_base.X()}, + {"y", m_base.Y()} + }); + j["axis_u"] = json::object({ + {"x", m_axis_u.X()}, + {"y", m_axis_u.Y()} + }); + j["axis_v"] = json::object({ + {"x", m_axis_v.X()}, + {"y", m_axis_v.Y()} + }); j["opacity"] = m_opacity; j["visible"] = m_visible; j["key_white_transparent"] = m_key_white_transparent; @@ -445,10 +558,10 @@ bool Sketch_underlay::from_json(const nlohmann::json& j) if (j.contains("axis_v")) m_axis_v = gp_Vec2d(j["axis_v"].at("x").get(), j["axis_v"].at("y").get()); - m_opacity = j.value("opacity", 0.88f); - m_visible = j.value("visible", true); - m_key_white_transparent = j.value("key_white_transparent", true); - m_line_tint_enabled = j.value("line_tint_enabled", true); + m_opacity = j.value("opacity", 0.88f); + m_visible = j.value("visible", true); + m_key_white_transparent = j.value("key_white_transparent", true); + m_line_tint_enabled = j.value("line_tint_enabled", true); if (j.contains("line_tint_rgb") && j["line_tint_rgb"].is_array() && j["line_tint_rgb"].size() >= 3) { m_tint_r = static_cast(j["line_tint_rgb"][0].get()); diff --git a/src/sketch_underlay.h b/src/sketch_underlay.h index 2bd5186..e14d03c 100644 --- a/src/sketch_underlay.h +++ b/src/sketch_underlay.h @@ -1,16 +1,16 @@ #pragma once +#include #include +#include +#include +#include #include +#include #include #include #include -#include -#include -#include -#include - class AIS_TexturedShape; class AIS_InteractiveContext; class gp_Pln; @@ -35,30 +35,68 @@ class Sketch_underlay [[nodiscard]] bool set_image_rgba(std::vector&& rgba, int w, int h); void set_affine(const gp_Pnt2d& base, const gp_Vec2d& axis_u, const gp_Vec2d& axis_v); + /// Center \a center, half width/height \a half_extents.x/.y, rotation in degrees (matches Sketch UI transform). + void set_center_extents_rotation(const glm::dvec2& center, const glm::dvec2& half_extents, double rot_deg); void set_opacity(float opaque01); void set_visible(bool v); /// When true (default), bright pixels (white paper) become transparent in the texture; dark linework stays opaque. void set_key_white_transparent(bool on); - [[nodiscard]] bool key_white_transparent() const { return m_key_white_transparent; } + + [[nodiscard]] bool key_white_transparent() const + { + return m_key_white_transparent; + } void set_line_tint_enabled(bool on); void set_line_tint_rgb(uint8_t r, uint8_t g, uint8_t b); - [[nodiscard]] bool line_tint_enabled() const { return m_line_tint_enabled; } - void line_tint_rgb(uint8_t& r, uint8_t& g, uint8_t& b) const; - [[nodiscard]] float opacity() const { return m_opacity; } - [[nodiscard]] bool visible() const { return m_visible; } - [[nodiscard]] gp_Pnt2d base() const { return m_base; } - [[nodiscard]] gp_Vec2d axis_u() const { return m_axis_u; } - [[nodiscard]] gp_Vec2d axis_v() const { return m_axis_v; } - [[nodiscard]] int image_w() const { return m_w; } - [[nodiscard]] int image_h() const { return m_h; } + [[nodiscard]] bool line_tint_enabled() const + { + return m_line_tint_enabled; + } + + void line_tint_rgb(uint8_t& r, uint8_t& g, uint8_t& b) const; + + [[nodiscard]] float opacity() const + { + return m_opacity; + } + + [[nodiscard]] bool visible() const + { + return m_visible; + } + + [[nodiscard]] gp_Pnt2d base() const + { + return m_base; + } + + [[nodiscard]] gp_Vec2d axis_u() const + { + return m_axis_u; + } + + [[nodiscard]] gp_Vec2d axis_v() const + { + return m_axis_v; + } + + [[nodiscard]] int image_w() const + { + return m_w; + } + + [[nodiscard]] int image_h() const + { + return m_h; + } void rebuild_and_display(const gp_Pln& pln, AIS_InteractiveContext& ctx); void erase(AIS_InteractiveContext& ctx); void sync_visibility(const gp_Pln& pln, AIS_InteractiveContext& ctx); - nlohmann::json to_json() const; + nlohmann::json to_json() const; /// Returns false if JSON is invalid or image decode fails. [[nodiscard]] bool from_json(const nlohmann::json& j); @@ -74,15 +112,15 @@ class Sketch_underlay gp_Vec2d m_axis_u {100., 0.}; gp_Vec2d m_axis_v {0., 100.}; - float m_opacity {0.88f}; - bool m_visible {true}; + float m_opacity {0.88f}; + bool m_visible {true}; /// Luminance key applied only when building the GPU texture (stored pixels stay raw). - bool m_key_white_transparent {true}; + bool m_key_white_transparent {true}; /// Recolor pixels that remain visible (alpha > 0 after key) for contrast on dark views. - bool m_line_tint_enabled {true}; - uint8_t m_tint_r {255}; - uint8_t m_tint_g {220}; - uint8_t m_tint_b {0}; + bool m_line_tint_enabled {true}; + uint8_t m_tint_r {255}; + uint8_t m_tint_g {220}; + uint8_t m_tint_b {0}; opencascade::handle m_ais; }; diff --git a/tests/sketch_tests.cpp b/tests/sketch_tests.cpp index 647ca50..fb8183a 100644 --- a/tests/sketch_tests.cpp +++ b/tests/sketch_tests.cpp @@ -12,6 +12,8 @@ #include "sketch.h" #include "sketch_json.h" +using namespace glm; + namespace bg = boost::geometry; // General templated helper for any Boost.Geometry geometry type @@ -251,12 +253,12 @@ TEST_F(Sketch_test, CreateSquare) // First point - center of square gp_Pnt2d center(0.0, 0.0); - ScreenCoords screen_coords(glm::dvec2(center.X(), center.Y())); + ScreenCoords screen_coords(dvec2(center.X(), center.Y())); sketch.add_sketch_pt(screen_coords); // Second point - center of right edge (defines width and orientation) gp_Pnt2d edge_center(10.0, 0.0); - screen_coords = ScreenCoords(glm::dvec2(edge_center.X(), edge_center.Y())); + screen_coords = ScreenCoords(dvec2(edge_center.X(), edge_center.Y())); sketch.add_sketch_pt(screen_coords); // Finalize the square @@ -337,12 +339,12 @@ TEST_F(Sketch_test, CreateCircle) // First point - center of circle gp_Pnt2d center(0.0, 0.0); - ScreenCoords screen_coords(glm::dvec2(center.X(), center.Y())); + ScreenCoords screen_coords(dvec2(center.X(), center.Y())); sketch.add_sketch_pt(screen_coords); // Second point - point on circle (defines radius) gp_Pnt2d edge_point(10.0, 0.0); - screen_coords = ScreenCoords(glm::dvec2(edge_point.X(), edge_point.Y())); + screen_coords = ScreenCoords(dvec2(edge_point.X(), edge_point.Y())); sketch.add_sketch_pt(screen_coords); // Finalize the circle @@ -1339,10 +1341,10 @@ TEST_F(Sketch_test, MirrorSelectedEdges_NoEdgesSelected) gp_Pnt2d axis_start(0.0, 0.0); gp_Pnt2d axis_end(10.0, 0.0); - ScreenCoords screen_coords_start(glm::dvec2(axis_start.X(), axis_start.Y())); + ScreenCoords screen_coords_start(dvec2(axis_start.X(), axis_start.Y())); sketch.add_sketch_pt(screen_coords_start); - ScreenCoords screen_coords_end(glm::dvec2(axis_end.X(), axis_end.Y())); + ScreenCoords screen_coords_end(dvec2(axis_end.X(), axis_end.Y())); sketch.add_sketch_pt(screen_coords_end); // finalize_elm() is called automatically when the second point is added for Single linestring type @@ -1408,12 +1410,12 @@ TEST_F(Sketch_test, SplitEdge_HasMidpoints) // Create a new edge that snaps to the midpoint, which will split the original edge // Start from the midpoint - ScreenCoords screen_coords_mid(glm::dvec2(10.0, 0.0)); + ScreenCoords screen_coords_mid(dvec2(10.0, 0.0)); sketch.add_sketch_pt(screen_coords_mid); // Add another point above the midpoint to create a vertical edge gp_Pnt2d pt_above(10.0, 10.0); - ScreenCoords screen_coords_above(glm::dvec2(pt_above.X(), pt_above.Y())); + ScreenCoords screen_coords_above(dvec2(pt_above.X(), pt_above.Y())); sketch.add_sketch_pt(screen_coords_above); // Finalize the edge - this should trigger the edge splitting @@ -1508,9 +1510,9 @@ TEST_F(Sketch_test, AddNode_splits_linear_edge_interior) gui().set_mode(Mode::Sketch_add_node); // Snap to an existing endpoint first (rubber band), then place the new node on the edge interior. - ScreenCoords anchor(glm::dvec2(0.0, 0.0)); + ScreenCoords anchor(dvec2(0.0, 0.0)); sketch.add_sketch_pt(anchor); - ScreenCoords interior(glm::dvec2(7.0, 0.0)); + ScreenCoords interior(dvec2(7.0, 0.0)); sketch.add_sketch_pt(interior); EXPECT_EQ(count_real_edges(sketch), 2u) << "Add node on edge interior should replace one edge with two"; @@ -1563,7 +1565,7 @@ TEST_F(Sketch_test, AddNode_off_edge_adds_node_only) gui().set_mode(Mode::Sketch_add_node); // Far enough from y=0 that headless snap radius (~100 plane units) does not pull onto the segment. - ScreenCoords away(glm::dvec2(10.0, 200.0)); + ScreenCoords away(dvec2(10.0, 200.0)); sketch.add_sketch_pt(away); size_t edge_count = 0; @@ -1597,7 +1599,7 @@ TEST_F(Sketch_test, AddNode_near_edge_snaps_onto_segment_and_splits) Sketch_access::update_faces_(sketch); gui().set_mode(Mode::Sketch_add_node); - ScreenCoords near_edge(glm::dvec2(7.0, 0.15)); + ScreenCoords near_edge(dvec2(7.0, 0.15)); sketch.add_sketch_pt(near_edge); size_t edge_count = 0;