Design tab: Text dialog with font, height and live outline; offer header

- Text opens a dialog instead of a bare text entry: any installed font
  (bold, italic), the height in mm, and a live outline of exactly what
  will be inserted with its size. Enter inserts, Esc cancels; the last
  font and height are remembered. text_to_regions gains an overload
  taking a loaded font.
- The offer menu opens with a greyed title naming what the rows act on
  ("Flat face 4 of Body 2", "Sketch line", "Nothing selected").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude
2026-09-30 08:34:25 +00:00
parent c74ddf8bb5
commit 205bc1b332
9 changed files with 361 additions and 13 deletions
+7
View File
@@ -60,6 +60,9 @@ pointing at things stays quiet.
The offer also opens by itself the moment you press Sketch on a face or plane, showing the
sketch tools — the app hands you the tools directly.
The first line names what the rows act on — *"Flat face 4 of Body 2"*, *"Sketch line"*,
*"Nothing selected"* — so the menu is readable even when the selection is not under the cursor.
**Eight families, always in this fixed order:** Create, Add material, Remove, Dress-up,
Repeat, Transform, Reference, Modify.
@@ -302,6 +305,10 @@ 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.
**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.
+1
View File
@@ -301,6 +301,7 @@ src/slic3r/GUI/CAD/DesignPanel.cpp
src/slic3r/GUI/CAD/DesignSketchTool.cpp
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
src/slic3r/GUI/CAD/DesignOffer.hpp
src/slic3r/GUI/CAD/DesignTextDialog.cpp
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
src/slic3r/GUI/KeyChord.cpp
+11 -5
View File
@@ -67,12 +67,18 @@ ImportRegions text_to_regions(const std::string& utf8, double size_mm,
return {};
const std::string path = font_path.empty() ? default_font_path() : font_path;
std::unique_ptr<Emboss::FontFile> ff = Emboss::create_font_file(path.c_str());
if (!ff)
return {};
Emboss::FontFileWithCache fwc(std::move(ff));
if (!fwc.has_value())
return text_to_regions(utf8, size_mm,
std::shared_ptr<const Emboss::FontFile>(Emboss::create_font_file(path.c_str())));
}
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::shared_ptr<const Emboss::FontFile>& ff)
{
if (utf8.empty() || size_mm <= 0.0 || !ff)
return {};
Emboss::FontFileWithCache fwc;
fwc.font_file = ff;
fwc.cache = std::make_shared<Emboss::Glyphs>();
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
+8
View File
@@ -3,11 +3,14 @@
#include "libslic3r/Point.hpp" // Vec2d
#include <memory>
#include <string>
#include <vector>
namespace Slic3r {
namespace Emboss { struct FontFile; }
// A rigid imported region: contour[0] = outer loop, contour[1..] = holes;
// points in plane (u,v) millimetres. The nested vector type matches
// CadFeature::imported_regions exactly, so results assign directly.
@@ -19,6 +22,11 @@ using ImportRegions = std::vector<ImportRegion>;
// font (resources/fonts). Returns an empty vector on any failure.
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::string& font_path = std::string());
// Same, from an already loaded font — the GUI resolves a system font (face, bold, italic)
// to one and keeps it while the user types. Returns an empty vector when the font is null or
// yields no shape.
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::shared_ptr<const Emboss::FontFile>& font);
// Parse an SVG file's filled paths into regions (mm), centred on the origin.
// `scale` multiplies the authored size (1.0 = as authored). Returns an empty
+2
View File
@@ -820,6 +820,8 @@ if (SLIC3R_CAD)
list(APPEND SLIC3R_GUI_SOURCES
GUI/CAD/DesignPanel.cpp
GUI/CAD/DesignPanel.hpp
GUI/CAD/DesignTextDialog.cpp
GUI/CAD/DesignTextDialog.hpp
GUI/CAD/DesignCanvas.cpp
GUI/CAD/DesignCanvas.hpp
GUI/CAD/DesignSketchTool.cpp
+49 -8
View File
@@ -1,6 +1,7 @@
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#include "slic3r/GUI/CAD/DesignCanvas.hpp"
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
#include "slic3r/GUI/CAD/DesignTextDialog.hpp" // Text: font, height, live outline
#include "slic3r/GUI/CAD/DesignOffer.hpp" // generated offer table — see docs/ux/tool_atlas.json
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
@@ -4562,15 +4563,12 @@ void DesignPanel::sync_sketch_display()
void DesignPanel::on_add_text()
{
wxTextEntryDialog dlg(this, _L("Text to insert:"), _L("Text"), wxEmptyString);
if (dlg.ShowModal() != wxID_OK)
// 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())
return;
const wxString text = dlg.GetValue();
if (text.empty())
return;
const std::string utf8(text.ToUTF8().data());
// Insert at a default height; resize in-canvas via the bbox handles (Move/Scale).
add_imported_sketch(text_to_regions(utf8, 10.0), _L("Text"));
add_imported_sketch(dlg.regions(), _L("Text"));
}
void DesignPanel::on_import_svg()
@@ -6369,6 +6367,38 @@ static void offer_trace(const char* fmt, ...)
fflush(stderr);
}
// The offer menu's title line for selection `kind` (an OfferSel), or empty for none.
wxString DesignPanel::offer_header(int kind) const
{
const int nb = int(m_doc.bodies.size());
auto body = [&]() {
if (m_sel_solid_body < 0 || m_sel_solid_body >= nb) return wxString();
const std::string& n = m_doc.bodies[m_sel_solid_body].name;
return n.empty() ? wxString::Format(_L("Body %d"), m_sel_solid_body + 1) : wxString::FromUTF8(n);
};
switch (OfferSel(kind)) {
case OfferSel::None: return _L("Nothing selected");
case OfferSel::BodySolid: return wxString::Format(_L("%s — solid body"), body());
case OfferSel::BodySheet: return wxString::Format(_L("%s — surface body"), body());
case OfferSel::FacePlanar: return wxString::Format(_L("Flat face %d of %s"), m_sel_solid_face, body());
case OfferSel::FaceCyl: return wxString::Format(_L("Cylindrical face %d of %s"), m_sel_solid_face, body());
case OfferSel::FaceOther: return wxString::Format(_L("Face %d of %s"), m_sel_solid_face, body());
case OfferSel::EdgeStr: return wxString::Format(_L("Straight edge %d of %s"), m_sel_solid_edge, body());
case OfferSel::EdgeCirc: return wxString::Format(_L("Circular edge %d of %s"), m_sel_solid_edge, body());
case OfferSel::Vertex: return wxString::Format(_L("Vertex of %s"), body());
case OfferSel::SkLoop: return _L("Sketch profile");
case OfferSel::SkNone: return _L("Sketch — nothing selected");
case OfferSel::SkLine: return _L("Sketch line");
case OfferSel::SkArc: return _L("Sketch curve");
case OfferSel::SkPoint: return _L("Sketch point");
case OfferSel::Sk2Ent: {
const int n = m_viewport ? m_viewport->sketch_selection_count() : 0;
return wxString::Format(_L("%d sketch entities"), n);
}
default: return wxString();
}
}
void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
{
const int kind = offer_selection_kind();
@@ -6420,6 +6450,17 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
std::vector<const OfferVerb*> bound; // menu id offset -> verb
const int base = wxID_HIGHEST + 4200;
// Header: WHAT the rows below act on, named the way the tree and the cards name it. The rows
// only make sense against the selection, and the selection is not always visible under the
// cursor that opened the menu. Greyed, so it reads as a title and cannot be run.
{
const wxString head = offer_header(kind);
if (!head.empty()) {
menu.Append(wxID_ANY, head)->Enable(false);
menu.AppendSeparator();
}
}
for (int row = 0; row < kOfferRowCount; ++row) {
std::vector<const OfferVerb*> live, family;
for (int i = 0; i < kOfferVerbCount; ++i) {
+1
View File
@@ -755,6 +755,7 @@ private:
// monitor, and the menu would map there — detached from the geometry it is about.
wxPoint offer_anchor() const;
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
wxString offer_header(int kind) const; // the offer menu's title line for that selection
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
// is_sketching() is still false — precisely when the drawing tools must be on offer. The
+223
View File
@@ -0,0 +1,223 @@
#include "DesignTextDialog.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/DialogButtons.hpp"
#include "slic3r/Utils/WxFontUtils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Emboss.hpp"
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/dcbuffer.h>
#include <wx/fontenum.h>
#include <wx/graphics.h>
#include <wx/sizer.h>
#include <wx/spinctrl.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <algorithm>
#include <limits>
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)
: 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.
wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
double height = 10.0;
if (AppConfig* cfg = wxGetApp().app_config) {
const std::string desc = cfg->get(kFontKey);
if (!desc.empty()) {
wxFont f = WxFontUtils::load_wxFont(desc);
if (f.IsOk()) font = f;
}
const std::string h = cfg->get(kHeightKey);
if (!h.empty()) {
try { height = std::clamp(std::stod(h), 0.5, 500.0); } catch (...) {}
}
}
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->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);
// Scalable faces only ('@' faces are the vertical-writing aliases GTK/MSW list twice).
wxArrayString faces = wxFontEnumerator::GetFacenames(wxFONTENCODING_SYSTEM, false);
faces.erase(std::remove_if(faces.begin(), faces.end(), [](const wxString& f) { return f.StartsWith("@"); }),
faces.end());
faces.Sort();
m_face = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, faces);
int sel = m_face->FindString(font.GetFaceName());
m_face->SetSelection(sel != wxNOT_FOUND ? sel : 0);
form->Add(new wxStaticText(this, wxID_ANY, _L("Font")), 0, wxALIGN_CENTER_VERTICAL);
form->Add(m_face, 1, wxEXPAND);
auto* style = new wxBoxSizer(wxHORIZONTAL);
m_bold = new wxCheckBox(this, wxID_ANY, _L("Bold"));
m_italic = new wxCheckBox(this, wxID_ANY, _L("Italic"));
m_bold->SetValue(WxFontUtils::is_bold(font));
m_italic->SetValue(WxFontUtils::is_italic(font));
style->Add(m_bold, 0, wxRIGHT, em);
style->Add(m_italic, 0);
form->AddSpacer(0);
form->Add(style, 0);
m_height = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(10 * em, -1),
wxSP_ARROW_KEYS, 0.5, 500.0, height, 0.5);
m_height->SetDigits(1);
form->Add(new wxStaticText(this, wxID_ANY, _L("Height (mm)")), 0, wxALIGN_CENTER_VERTICAL);
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));
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(); });
m_size = new wxStaticText(this, wxID_ANY, wxEmptyString);
auto* buttons = new DialogButtons(this, {"OK", "Cancel"});
m_ok = buttons->GetOK();
auto* top = new wxBoxSizer(wxVERTICAL);
top->Add(form, 0, wxEXPAND | wxALL, em);
top->Add(m_preview, 1, wxEXPAND | wxLEFT | wxRIGHT, em);
top->Add(m_size, 0, wxLEFT | wxRIGHT | wxTOP, em);
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
});
auto refont = [this](wxCommandEvent&) { load_font(); update_preview(); };
m_face->Bind(wxEVT_CHOICE, refont);
m_bold->Bind(wxEVT_CHECKBOX, refont);
m_italic->Bind(wxEVT_CHECKBOX, refont);
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);
});
wxGetApp().UpdateDlgDarkUI(this);
SetSizerAndFit(top);
CenterOnParent();
load_font();
update_preview();
m_text->SetFocus();
}
wxString DesignTextDialog::text() const { return m_text->GetValue(); }
wxFont DesignTextDialog::current_font() const
{
wxFontInfo info(12);
if (m_face->GetSelection() != wxNOT_FOUND)
info.FaceName(m_face->GetStringSelection());
info.Bold(m_bold->GetValue()).Italic(m_italic->GetValue());
return wxFont(info);
}
void DesignTextDialog::load_font()
{
const wxFont font = current_font();
m_font_file.reset();
if (font.IsOk() && WxFontUtils::can_load(font))
m_font_file = WxFontUtils::create_font_file(font);
}
void DesignTextDialog::update_preview()
{
const std::string utf8(m_text->GetValue().ToUTF8().data());
m_regions.clear();
if (m_font_file && !utf8.empty())
m_regions = text_to_regions(utf8, m_height->GetValue(), m_font_file);
wxString line;
if (!m_font_file)
line = _L("This font cannot be used for text — pick another one");
else if (utf8.empty())
line = _L("Type the text to see its size");
else if (m_regions.empty())
line = _L("This font has no outline for this text");
else {
double lo_x = std::numeric_limits<double>::max(), lo_y = lo_x;
double hi_x = -lo_x, hi_y = -lo_x;
for (const auto& r : m_regions)
for (const auto& c : r)
for (const Vec2d& p : c) {
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
}
line = wxString::Format(_L("Size: %.1f × %.1f mm"), hi_x - lo_x, hi_y - lo_y);
}
m_size->SetLabel(line);
if (m_ok) m_ok->Enable(!m_regions.empty());
m_preview->Refresh();
}
void DesignTextDialog::draw_preview(wxWindow* canvas)
{
wxAutoBufferedPaintDC dc(canvas);
const bool dark = wxGetApp().dark_mode();
dc.SetBackground(wxBrush(dark ? wxColour(0x1e, 0x1e, 0x22) : wxColour(0xf4, 0xf4, 0xf6)));
dc.Clear();
if (m_regions.empty()) return;
double lo_x = std::numeric_limits<double>::max(), lo_y = lo_x, hi_x = -lo_x, hi_y = -lo_x;
for (const auto& r : m_regions)
for (const auto& c : r)
for (const Vec2d& p : c) {
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
}
const wxSize sz = canvas->GetClientSize();
const double pad = 0.08 * std::min(sz.x, sz.y);
const double w = std::max(hi_x - lo_x, 1e-6), h = std::max(hi_y - lo_y, 1e-6);
const double k = std::min((sz.x - 2 * pad) / w, (sz.y - 2 * pad) / h);
const double ox = 0.5 * (sz.x - k * w), oy = 0.5 * (sz.y + k * h); // y up -> screen y down
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (!gc) return;
wxGraphicsPath path = gc->CreatePath();
for (const auto& r : m_regions)
for (const auto& c : r) {
if (c.empty()) continue;
path.MoveToPoint(ox + k * (c[0].x() - lo_x), oy - k * (c[0].y() - lo_y));
for (size_t i = 1; i < c.size(); ++i)
path.AddLineToPoint(ox + k * (c[i].x() - lo_x), oy - k * (c[i].y() - lo_y));
path.CloseSubpath();
}
gc->SetBrush(wxBrush(wxColour(0x00, 0x96, 0x88))); // Orca accent
gc->SetPen(*wxTRANSPARENT_PEN);
gc->FillPath(path, wxODDEVEN_RULE); // holes (the counter of an "o") stay open
}
void DesignTextDialog::on_dpi_changed(const wxRect&)
{
Fit();
Refresh();
}
}} // namespace Slic3r::GUI
+59
View File
@@ -0,0 +1,59 @@
#ifndef slic3r_GUI_DesignTextDialog_hpp_
#define slic3r_GUI_DesignTextDialog_hpp_
#include "slic3r/GUI/GUI_Utils.hpp"
#include "libslic3r/CAD/SketchImport.hpp"
#include <wx/font.h>
#include <memory>
class wxTextCtrl;
class wxChoice;
class wxCheckBox;
class wxSpinCtrlDouble;
class wxStaticText;
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.
class DesignTextDialog : public DPIDialog
{
public:
explicit DesignTextDialog(wxWindow* parent);
// The vectorised text, centred on the origin, in mm. Empty unless the dialog ended wxID_OK.
const ImportRegions& regions() const { return m_regions; }
wxString text() const;
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void load_font(); // m_face/m_bold/m_italic -> m_font_file
void update_preview(); // text/font/height -> m_regions + size label
wxFont current_font() const;
void draw_preview(wxWindow* canvas);
wxTextCtrl* m_text{nullptr};
wxChoice* m_face{nullptr};
wxCheckBox* m_bold{nullptr};
wxCheckBox* m_italic{nullptr};
wxSpinCtrlDouble* m_height{nullptr};
wxWindow* m_preview{nullptr};
wxStaticText* m_size{nullptr};
wxWindow* m_ok{nullptr};
std::shared_ptr<const Emboss::FontFile> m_font_file;
ImportRegions m_regions;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GUI_DesignTextDialog_hpp_