mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
Design tab: Text is its own feature, previewed in the view and editable
Reported on the rig: Text showed no preview where it would go, its dialog was pinned over the middle of the window (GNOME attaches a modal dialog to its parent and it cannot be moved), and once confirmed the text could not be edited and did not appear in the feature tree. Inside an open sketch it became loose lines of that sketch. - Text is always a feature, "Text N" in the tree. A new text goes on the plane of the open sketch (committed first when it holds anything, closed when it is empty), else centred on the picked face, else on the reference plane. - The dialog is modeless and opens at the top right of the window. The feature is created at the first character and redrawn on every change, so the text appears in the view where it will be as it is typed. Enter inserts it, then the usual move/scale gizmo and Confirm; Esc, Cancel or closing the dialog takes it out again (undo to the checkpoint taken when it appeared). - CadFeature keeps text_string, text_font (the WxFontUtils descriptor) and text_height, appended at the end of the framed recipe. Editing a Text feature reopens the dialog with them and redraws the outline in place, keeping its placement. The outlines are still saved, so the project opens the same on a machine without that font. - The MCP control refuses writes while the Text dialog is open. Tests: the text parameters survive a save and load along with the outline; the truncated-recipe test accounts for the new tail fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
+16
-3
@@ -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.
|
||||
|
||||
@@ -118,6 +118,14 @@ struct CadFeature {
|
||||
// one edge instead of falling back to the whole face group.
|
||||
std::vector<int> 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<int> 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<class Archive>
|
||||
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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <wx/font.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
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<void()> on_change; // the outline changed (text, font or height)
|
||||
std::function<void()> on_accept; // Enter / OK with something to insert
|
||||
std::function<void()> 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};
|
||||
|
||||
@@ -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]")
|
||||
|
||||
Reference in New Issue
Block a user