diff --git a/docs/CAD/design_tab.md b/docs/CAD/design_tab.md index 87ef8fe7a4..675c94bb4c 100644 --- a/docs/CAD/design_tab.md +++ b/docs/CAD/design_tab.md @@ -315,9 +315,22 @@ variable and the whole model follows. ## Import and export -**Text** asks for the words, the font (any installed font, bold, italic) and the height, and -shows the outline that will be inserted with its size in millimetres before anything is added. -The last font and height are remembered. +**Text** is a feature of its own, "Text N" in the tree. Its dialog asks for the words, the +font (any installed font, bold, italic) and the height, and shows the size in millimetres. The +dialog does not block the window and can be moved aside. As you type, the text is drawn in the +view where it will go: +- on the plane of the sketch that is open (a sketch holding anything is committed first); +- else centred on the picked face; +- else on the reference plane. + +Enter inserts the text; then drag or scale it and Confirm. Esc takes it out again. Editing +a Text feature reopens the dialog with its words, font and height, and redraws it in place. +The outlines are saved with the project, so it opens the same on a machine without that font. +A new text starts from the last font and height used. + +To engrave or emboss, extrude the Text feature onto the solid, Cut or Join. A text is no +longer loose lines inside another sketch, so it no longer acts as a hole in that sketch's +profile. **Import STEP** brings in a real B-rep solid, not a mesh: its faces and edges can be filleted, shelled and cut like anything modelled here. diff --git a/src/libslic3r/CAD/CadDocument.hpp b/src/libslic3r/CAD/CadDocument.hpp index c85dcac220..78473af560 100644 --- a/src/libslic3r/CAD/CadDocument.hpp +++ b/src/libslic3r/CAD/CadDocument.hpp @@ -118,6 +118,14 @@ struct CadFeature { // one edge instead of falling back to the whole face group. std::vector dressup_edges; // The edges this dress-up targets: the list, else the single edge, else none (face group). + // Text feature: a Sketch whose imported_regions were vectorised from this string in this font + // (a WxFontUtils descriptor, bold/italic included) at this cap height in mm. The regions are + // what gets built — they are saved too, so the project opens on a machine without the font — + // and these three are what an edit reopens the Text dialog with. Empty = not a text feature. + std::string text_string; + std::string text_font; + double text_height{0.0}; + bool is_text() const { return !text_string.empty(); } std::vector dressup_edge_ids() const { if (!dressup_edges.empty()) return dressup_edges; if (dressup_edge >= 0) return { dressup_edge }; @@ -395,7 +403,8 @@ struct CadFeature { mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip, coordsys_face_kind, coordsys_face_edges, thread_major_nominal, pattern_inclusive, - dressup_edges); + dressup_edges, + text_string, text_font, text_height); } template void load(Archive& ar) { @@ -436,7 +445,8 @@ struct CadFeature { mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip, coordsys_face_kind, coordsys_face_edges, thread_major_nominal, pattern_inclusive, - dressup_edges); + dressup_edges, + text_string, text_font, text_height); imported_solid = brep_from_string(brep); } // The pre-framing (v4) layout, FROZEN. A v4 recipe is one flat stream with no per-feature diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 665aa0f382..6205b82b9e 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -4566,12 +4566,139 @@ void DesignPanel::sync_sketch_display() void DesignPanel::on_add_text() { - // Font, height and a live outline with its size are chosen up front; the bbox handles - // (Move/Scale) still resize the art in the canvas afterwards. - DesignTextDialog dlg(this); - if (dlg.ShowModal() != wxID_OK || dlg.regions().empty()) + open_text_dialog(-1); +} + +// Text is a FEATURE, "Text N" in the tree, never loose lines in a sketch: that is what makes it +// visible, and editable afterwards (the feature keeps its string, font and height). While the +// dialog is open the feature is drawn in the canvas where it will be — the dialog is modeless +// so it can be moved off it — and Cancel takes it out again. A new text goes on the plane of +// the sketch that is open (committed first if it holds anything), else on the picked face +// (centred on it, ready to engrave), else on the reference plane. +void DesignPanel::open_text_dialog(int feat) +{ + if (m_text_dlg != nullptr) { m_text_dlg->Raise(); return; } + m_text_editing = feat >= 0 && feat < int(m_doc.features.size()) && m_doc.features[feat].is_text(); + m_text_feat = m_text_editing ? feat : -1; + m_text_face_body = -1; + m_text_offset = Vec2d(0, 0); + + DesignTextDialog::Spec initial; + if (m_text_editing) { + const CadFeature& f = m_doc.features[feat]; + initial = { wxString::FromUTF8(f.text_string), f.text_font, f.text_height }; + m_doc.checkpoint(); // undo boundary: Cancel restores the text as it was + } else { + if (m_viewport && m_viewport->is_sketching()) { + m_text_plane = m_viewport->mcp_sketch_tool().plane(); + if (m_viewport->live_sketch_has_work()) { + m_viewport->sketch_confirm_pending(); + m_viewport->finish_sketch(); // keep what was drawn: it is its own feature + } else { + m_viewport->cancel_sketch(); // an empty sketch was only a way to pick the plane + } + m_edit_index = -1; + set_ui_mode(UiMode::Feature); + sync_sketch_display(); + refresh_tree(); + } else if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) { + const TopoDS_Face face = GeometryEngine::face_by_index(m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face); + if (!face.IsNull()) { + m_text_plane = SketchPlane::from_face(face); + m_text_offset = m_text_plane.project(GeometryEngine::face_centroid_world(face), m_text_plane.normal); + m_text_face_body = m_sel_solid_body; + } else { + m_text_plane = plane_from_choice(m_ref_plane); + } + } else { + m_text_plane = plane_from_choice(m_ref_plane); + } + } + + m_text_dlg = new DesignTextDialog(this, m_text_editing ? &initial : nullptr); + m_text_dlg->on_change = [this] { text_dialog_changed(); }; + m_text_dlg->on_accept = [this] { text_dialog_done(true); }; + m_text_dlg->on_cancel = [this] { text_dialog_done(false); }; + m_text_dlg->Show(); + set_status(StatusKind::Info, m_text_editing ? _L("Edit the text — Enter applies, Esc keeps it as it was") + : _L("Type the text — it appears in the view as you type; Enter inserts, Esc cancels")); +} + +void DesignPanel::text_dialog_changed() +{ + if (m_text_dlg == nullptr) return; + const ImportRegions& regions = m_text_dlg->regions(); + const DesignTextDialog::Spec sp = m_text_dlg->spec(); + const bool have = m_text_feat >= 0 && m_text_feat < int(m_doc.features.size()); + if (regions.empty()) { + // Nothing to draw. An edited text keeps its last outline (OK is disabled until there is + // text again); a new one that has not been accepted simply goes away again. + if (!m_text_editing && have) { + m_doc.undo(); + m_text_feat = -1; + m_feature_counter--; // the next keystroke brings the same "Text N" back + refresh_tree(); + sync_sketch_display(); + } return; - add_imported_sketch(dlg.regions(), _L("Text")); + } + if (!have) { + m_doc.checkpoint(); // undo boundary: the new text (Cancel removes it) + m_feature_counter++; + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = feature_name(_L("Text")); + f.plane = m_text_plane; + f.import_offset = m_text_offset; // the regions are centred on the origin + f.import_on_face = m_text_face_body >= 0; + f.import_face_body = m_text_face_body; + m_doc.features.push_back(f); + m_text_feat = int(m_doc.features.size()) - 1; + refresh_tree(); + set_tree_selection(m_text_feat); + } + CadFeature& f = m_doc.features[m_text_feat]; + f.imported_regions = regions; + f.text_string = std::string(sp.text.ToUTF8().data()); + f.text_font = sp.font; + f.text_height = sp.height; + sync_sketch_display(); // the overlay draws the feature's regions: the canvas preview +} + +void DesignPanel::text_dialog_done(bool accepted) +{ + DesignTextDialog* dlg = m_text_dlg; + m_text_dlg = nullptr; + if (dlg != nullptr) dlg->Destroy(); + const int feat = m_text_feat; + const bool editing = m_text_editing; + m_text_feat = -1; + m_text_editing = false; + const bool have = feat >= 0 && feat < int(m_doc.features.size()); + + if (!accepted) { + if (have) m_doc.undo(); // a new text leaves, an edited one gets its old outline back + refresh_tree(); + sync_sketch_display(); + set_status(StatusKind::Info, editing ? _L("Text unchanged") : _L("Text cancelled")); + return; + } + if (!have) return; + // Drop a solid-face pick now that the text sits on it, so the next Extrude acts on the text. + m_sel_solid_face = m_sel_solid_edge = m_sel_solid_body = -1; + m_doc.recompute(); // a lone sketch yields an empty body; that is expected + refresh_tree(); + set_tree_selection(feat); + sync_sketch_display(); + if (editing) { + set_status_ok(); + return; + } + // A new text still has to be placed: the same move/scale gizmo and Confirm/Cancel card as + // any inserted art. Its Cancel undoes to the checkpoint taken when the text appeared. + on_transform_imported(feat); + m_insert_feat = feat; + open_insert_card(wxString::FromUTF8(m_doc.features[feat].name)); } void DesignPanel::on_import_svg() @@ -4630,6 +4757,7 @@ bool DesignPanel::mcp_busy(bool sketch_method, std::string& why) const // A GUI editor holds a candidate built from the document as it was when it opened; a // feature added or removed underneath it would be overwritten, or overwrite, on Confirm. if (m_active != Tool::None || m_edit_index >= 0) { why = "a feature card is open in the Design tab"; return true; } + if (m_text_dlg != nullptr) { why = "the Text dialog is open in the Design tab"; return true; } if (m_ui_mode == UiMode::Constrain) { why = "the Design tab is constraining a sketch"; return true; } // Sketch methods drive the live sketch session, which is the point of them; everything else // changes the feature list, which a live sketch session is about to commit into. @@ -9323,7 +9451,12 @@ void DesignPanel::on_edit_feature() switch (f.type) { case CadFeatureType::Sketch: - // Imported Text/SVG art has no editable sketch dialog — edit means + // Text reopens its dialog: change the words, the font or the height in place. + if (f.is_text()) { + open_text_dialog(sel); + break; + } + // Imported SVG art has no editable sketch dialog — edit means // move / scale its placement instead, behind the same Confirm/Cancel gate as // the initial insert (Cancel = undo restores the prior placement). if (!f.imported_regions.empty()) { diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index c9b6e65f47..c7e20186df 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -42,6 +42,8 @@ class ScalableButton; namespace Slic3r { namespace GUI { +class DesignTextDialog; + class DesignCanvas; // Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel. @@ -199,6 +201,11 @@ private: // imported_regions (no solver entities). on_add_text/on_import_svg gather // input; add_imported_sketch builds the feature, refreshes tree + display. void on_add_text(); + // Text is its own feature ("Text N"), drawn in the canvas while its (modeless) dialog is + // open. feat < 0 starts a new one; otherwise the text feature `feat` is reopened for editing. + void open_text_dialog(int feat); + void text_dialog_changed(); + void text_dialog_done(bool accepted); void on_import_svg(); void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh) void on_import_mesh(); // STL/OBJ -> B-rep body via GeometryEngine::mesh_to_brep @@ -410,6 +417,15 @@ private: wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art wxSizer* m_box_expr{nullptr}; // expression binding card (visible during edit only) int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm + // The open Text dialog, the feature it draws into (-1 until there is text to draw) and, + // for a new text, where it goes: plane, offset in that plane, and the body of the face it + // sits on (-1 = not on a face). + DesignTextDialog* m_text_dlg{nullptr}; + int m_text_feat{-1}; + bool m_text_editing{false}; + SketchPlane m_text_plane; + Vec2d m_text_offset{0, 0}; + int m_text_face_body{-1}; // Move-body gizmo runs through the unified action bar too: Confirm keeps the placement, // Cancel reverts to the pose captured when the move started. int m_move_body{-1}; diff --git a/src/slic3r/GUI/CAD/DesignTextDialog.cpp b/src/slic3r/GUI/CAD/DesignTextDialog.cpp index 73fd8535e6..38e1db91e3 100644 --- a/src/slic3r/GUI/CAD/DesignTextDialog.cpp +++ b/src/slic3r/GUI/CAD/DesignTextDialog.cpp @@ -25,17 +25,21 @@ namespace Slic3r { namespace GUI { static const char* kFontKey = "cad_text_font"; // WxFontUtils::store_wxFont descriptor static const char* kHeightKey = "cad_text_height"; // mm -DesignTextDialog::DesignTextDialog(wxWindow* parent) +DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial) : DPIDialog(parent, wxID_ANY, _L("Text"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) { SetFont(wxGetApp().normal_font()); SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(0x2d, 0x2d, 0x31) : *wxWHITE); const int em = em_unit(); - // Last used font and height, else the system GUI font at 10 mm. + // The text being edited, else the last used font and height, else the GUI font at 10 mm. wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); double height = 10.0; - if (AppConfig* cfg = wxGetApp().app_config) { + if (initial != nullptr) { + wxFont f = WxFontUtils::load_wxFont(initial->font); + if (f.IsOk()) font = f; + if (initial->height > 0.0) height = std::clamp(initial->height, 0.5, 500.0); + } else if (AppConfig* cfg = wxGetApp().app_config) { const std::string desc = cfg->get(kFontKey); if (!desc.empty()) { wxFont f = WxFontUtils::load_wxFont(desc); @@ -50,8 +54,8 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent) auto* form = new wxFlexGridSizer(2, em / 2, em); form->AddGrowableCol(1, 1); - m_text = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(30 * em, -1), - wxTE_PROCESS_ENTER); + m_text = new wxTextCtrl(this, wxID_ANY, initial ? initial->text : wxString(), wxDefaultPosition, + wxSize(30 * em, -1), wxTE_PROCESS_ENTER); m_text->SetHint(_L("Type the text to insert")); form->Add(new wxStaticText(this, wxID_ANY, _L("Text")), 0, wxALIGN_CENTER_VERTICAL); form->Add(m_text, 1, wxEXPAND); @@ -84,7 +88,8 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent) form->Add(m_height, 0); // The outline of what will be inserted, fitted to the box, with its real size under it. - m_preview = new wxWindow(this, wxID_ANY, wxDefaultPosition, wxSize(30 * em, 10 * em)); + // A thumbnail only: the text itself is drawn in the canvas, where it will be. + m_preview = new wxWindow(this, wxID_ANY, wxDefaultPosition, wxSize(30 * em, 5 * em)); m_preview->SetBackgroundStyle(wxBG_STYLE_PAINT); m_preview->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { draw_preview(m_preview); }); m_preview->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { m_preview->Refresh(); e.Skip(); }); @@ -100,9 +105,7 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent) top->Add(buttons, 0, wxEXPAND); m_text->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_preview(); }); - m_text->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { - if (!m_regions.empty()) EndModal(wxID_OK); // Enter = OK, as everywhere in the tab - }); + m_text->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); }); // Enter = OK, as everywhere in the tab auto refont = [this](wxCommandEvent&) { load_font(); update_preview(); }; m_face->Bind(wxEVT_CHOICE, refont); m_bold->Bind(wxEVT_CHECKBOX, refont); @@ -110,18 +113,25 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent) m_height->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { update_preview(); }); m_height->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_preview(); }); Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - if (e.GetId() != wxID_OK) { e.Skip(); return; } - if (m_regions.empty()) return; // nothing to insert: the size line says why - if (AppConfig* cfg = wxGetApp().app_config) { - cfg->set(kFontKey, WxFontUtils::store_wxFont(current_font())); - cfg->set(kHeightKey, std::to_string(m_height->GetValue())); - } - EndModal(wxID_OK); + if (e.GetId() == wxID_OK) accept(); + else if (e.GetId() == wxID_CANCEL) cancel(); + else e.Skip(); + }); + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { cancel(); }); + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { + if (e.GetKeyCode() == WXK_ESCAPE) cancel(); + else e.Skip(); }); wxGetApp().UpdateDlgDarkUI(this); SetSizerAndFit(top); - CenterOnParent(); + // Out of the middle of the window, where the text is being placed: top right of the parent. + if (parent != nullptr) { + const wxRect pr = parent->GetScreenRect(); + SetPosition(wxPoint(std::max(pr.GetLeft(), pr.GetRight() - GetSize().x - 2 * em), pr.GetTop() + 8 * em)); + } else { + CenterOnParent(); + } load_font(); update_preview(); @@ -130,6 +140,29 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent) wxString DesignTextDialog::text() const { return m_text->GetValue(); } +DesignTextDialog::Spec DesignTextDialog::spec() const +{ + return { m_text->GetValue(), WxFontUtils::store_wxFont(current_font()), m_height->GetValue() }; +} + +void DesignTextDialog::accept() +{ + if (m_done || m_regions.empty()) return; // nothing to insert: the size line says why + m_done = true; + if (AppConfig* cfg = wxGetApp().app_config) { + cfg->set(kFontKey, WxFontUtils::store_wxFont(current_font())); + cfg->set(kHeightKey, std::to_string(m_height->GetValue())); + } + if (on_accept) on_accept(); +} + +void DesignTextDialog::cancel() +{ + if (m_done) return; + m_done = true; + if (on_cancel) on_cancel(); +} + wxFont DesignTextDialog::current_font() const { wxFontInfo info(12); @@ -175,6 +208,7 @@ void DesignTextDialog::update_preview() m_size->SetLabel(line); if (m_ok) m_ok->Enable(!m_regions.empty()); m_preview->Refresh(); + if (on_change) on_change(); } void DesignTextDialog::draw_preview(wxWindow* canvas) diff --git a/src/slic3r/GUI/CAD/DesignTextDialog.hpp b/src/slic3r/GUI/CAD/DesignTextDialog.hpp index 76ac86267f..3200301880 100644 --- a/src/slic3r/GUI/CAD/DesignTextDialog.hpp +++ b/src/slic3r/GUI/CAD/DesignTextDialog.hpp @@ -6,7 +6,9 @@ #include +#include #include +#include class wxTextCtrl; class wxChoice; @@ -18,18 +20,31 @@ namespace Slic3r { namespace Emboss { struct FontFile; } namespace GUI { -// Text for the Design tab: the words, the font (face, bold, italic) and the height, with a -// live outline of exactly what will be inserted and its size in millimetres. The outline IS -// the result — the same vectorisation the sketch receives — so there is nothing to guess. -// Enter inserts, Esc cancels (charter 4.2). The last font and height are remembered. +// Text for the Design tab: the words, the font (face, bold, italic) and the height, with the +// size in millimetres of exactly what will be inserted. MODELESS: the host draws the text in +// the canvas as it is typed (on_change), so the dialog stays out of the way and can be moved — +// a modal dialog is pinned over the middle of the window on GNOME. Enter or OK accepts, Esc, +// Cancel or closing it cancels (charter 4.2); each fires its callback once and the host then +// destroys the dialog. A new text starts from the last font and height used. class DesignTextDialog : public DPIDialog { public: - explicit DesignTextDialog(wxWindow* parent); + struct Spec { + wxString text; + std::string font; // WxFontUtils::store_wxFont descriptor (face, bold, italic) + double height{10.0}; + }; + // `initial` reopens an existing text for editing; nullptr starts a new one. + DesignTextDialog(wxWindow* parent, const Spec* initial = nullptr); - // The vectorised text, centred on the origin, in mm. Empty unless the dialog ended wxID_OK. + // The vectorised text, centred on the origin, in mm. Empty when there is nothing to insert. const ImportRegions& regions() const { return m_regions; } wxString text() const; + Spec spec() const; + + std::function on_change; // the outline changed (text, font or height) + std::function on_accept; // Enter / OK with something to insert + std::function on_cancel; // Esc / Cancel / closed protected: void on_dpi_changed(const wxRect& suggested_rect) override; @@ -39,6 +54,9 @@ private: void update_preview(); // text/font/height -> m_regions + size label wxFont current_font() const; void draw_preview(wxWindow* canvas); + void accept(); + void cancel(); + bool m_done{false}; // accept/cancel fire once wxTextCtrl* m_text{nullptr}; wxChoice* m_face{nullptr}; diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 5ce265e06d..ce695079f7 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -2348,11 +2348,12 @@ TEST_CASE("a truncated feature keeps what it could read", "[CadDocument][recipe] // Shorten feature 1 so it ends right after coordsys_face_kind: drop coordsys_face_edges // (4 bytes), the two flags appended after it (thread_major_nominal, pattern_inclusive: - // 1 byte each) and the empty dressup_edges list (its 8-byte size tag). Rewrite its length - // prefix and erase the tail bytes. The reader then runs out inside fa(f), throws, and keeps - // everything it had already assigned — that is the whole point of the try/catch. (Cut on a - // field boundary: a field cut in half is read as whatever half arrived.) - const size_t drop = sizeof(uint32_t) + 2 * sizeof(bool) + sizeof(cereal::size_type); + // 1 byte each), the empty dressup_edges list and the two empty text strings (an 8-byte size + // tag each) and text_height (a double). Rewrite its length prefix and erase the tail bytes. + // The reader then runs out inside fa(f), throws, and keeps everything it had already + // assigned — that is the whole point of the try/catch. (Cut on a field boundary: a field + // cut in half is read as whatever half arrived.) + const size_t drop = sizeof(uint32_t) + 2 * sizeof(bool) + 3 * sizeof(cereal::size_type) + sizeof(double); REQUIRE(f_len[1] > drop); std::string shortened = blob; shortened.erase(f_off[1] + 4 + f_len[1] - drop, drop); @@ -2371,6 +2372,7 @@ TEST_CASE("a truncated feature keeps what it could read", "[CadDocument][recipe] REQUIRE_FALSE(loaded.features[1].thread_major_nominal); // ...and so were the later flags REQUIRE_FALSE(loaded.features[1].pattern_inclusive); REQUIRE(loaded.features[1].dressup_edges.empty()); + REQUIRE_FALSE(loaded.features[1].is_text()); REQUIRE(loaded.features[0].name == doc.features[0].name); REQUIRE(loaded.features[2].name == doc.features[2].name); } @@ -8079,6 +8081,31 @@ TEST_CASE("extrude: a profile that folds back on itself is refused with a reason REQUIRE(mp.volume > 0.0); } +TEST_CASE("text feature: its string, font and height survive a save and load", "[CadDocument][recipe]") +{ + CadDocument doc; + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = "Text 1"; + f.plane = SketchPlane::XY(); + f.imported_regions = { { { Vec2d(0, 0), Vec2d(4, 0), Vec2d(4, 6), Vec2d(0, 6) } } }; + f.text_string = "Ab ÷ 12"; + f.text_font = "Noto Sans;Bold"; + f.text_height = 7.5; + doc.features.push_back(f); + REQUIRE(doc.recompute()); + + CadDocument back; + REQUIRE(back.deserialize_recipe(doc.serialize_recipe())); + REQUIRE(back.features.size() == 1); + const CadFeature& g = back.features[0]; + REQUIRE(g.is_text()); + REQUIRE(g.text_string == f.text_string); + REQUIRE(g.text_font == f.text_font); + REQUIRE(g.text_height == f.text_height); + REQUIRE(g.imported_regions == f.imported_regions); // the outline is saved, not re-derived +} + // Several picked edges dressed by ONE feature: every id is resolved against the same body, so // capturing them up-front is correct here (unlike the chain of single-edge features above). TEST_CASE("dressup: one fillet on four picked edges equals the Top face group", "[CadDocument][dressup]")