Skip to content

Commit 99a364f

Browse files
rhys100cursoragent
andcommitted
Add auto-wrap/shrink and multiline surface curve following.
Extend Text Shape with wrap-to-width, optional height box with auto-shrink, and per-line surface contour sampling so multiline text follows the mesh instead of a flat stamp. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c6c6628 commit 99a364f

8 files changed

Lines changed: 420 additions & 30 deletions

File tree

src/libslic3r/Emboss.cpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,6 +1702,172 @@ double Emboss::get_text_shape_scale(const FontProp &fp, const FontFile &ff)
17021702
return scale * SHAPE_SCALE;
17031703
}
17041704

1705+
namespace {
1706+
double measure_line_width_mm(FontFileWithCache &font_with_cache, const FontProp &font_prop, const std::wstring &line)
1707+
{
1708+
if (!font_with_cache.has_value() || line.empty())
1709+
return 0.;
1710+
const double scale = get_text_shape_scale(font_prop, *font_with_cache.font_file);
1711+
fontinfo_opt font_info_cache;
1712+
Point cursor(0, 0);
1713+
for (wchar_t letter : line) {
1714+
if (letter == '\n' || letter == '\r')
1715+
continue;
1716+
letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache);
1717+
}
1718+
return cursor.x() * scale;
1719+
}
1720+
1721+
double measure_text_height_mm(FontFileWithCache &font_with_cache, const FontProp &font_prop, const std::string &text)
1722+
{
1723+
if (!font_with_cache.has_value())
1724+
return 0.;
1725+
const unsigned lines = std::max(1u, get_count_lines(text));
1726+
const double scale = get_text_shape_scale(font_prop, *font_with_cache.font_file);
1727+
return get_line_height(*font_with_cache.font_file, font_prop) * scale * lines;
1728+
}
1729+
} // namespace
1730+
1731+
std::string Emboss::apply_text_wrap(FontFileWithCache &font_with_cache, const std::string &text, const FontProp &font_prop, double wrap_width_mm)
1732+
{
1733+
if (!font_with_cache.has_value() || wrap_width_mm <= 1e-3 || text.empty())
1734+
return text;
1735+
1736+
const std::wstring src = boost::nowide::widen(text);
1737+
std::wstring out;
1738+
out.reserve(src.size() + 8);
1739+
1740+
size_t para_start = 0;
1741+
while (para_start <= src.size()) {
1742+
size_t para_end = src.find('\n', para_start);
1743+
if (para_end == std::wstring::npos)
1744+
para_end = src.size();
1745+
const std::wstring paragraph = src.substr(para_start, para_end - para_start);
1746+
1747+
std::wstring line;
1748+
auto flush_line = [&]() {
1749+
if (line.empty())
1750+
return;
1751+
if (!out.empty())
1752+
out.push_back('\n');
1753+
out += line;
1754+
line.clear();
1755+
};
1756+
1757+
size_t i = 0;
1758+
while (i < paragraph.size()) {
1759+
size_t start = i;
1760+
if (paragraph[i] == ' ' || paragraph[i] == '\t') {
1761+
while (i < paragraph.size() && (paragraph[i] == ' ' || paragraph[i] == '\t'))
1762+
++i;
1763+
} else {
1764+
while (i < paragraph.size() && paragraph[i] != ' ' && paragraph[i] != '\t')
1765+
++i;
1766+
}
1767+
const std::wstring token = paragraph.substr(start, i - start);
1768+
const std::wstring trial = line + token;
1769+
if (line.empty() || measure_line_width_mm(font_with_cache, font_prop, trial) <= wrap_width_mm) {
1770+
line = trial;
1771+
continue;
1772+
}
1773+
flush_line();
1774+
if (measure_line_width_mm(font_with_cache, font_prop, token) <= wrap_width_mm) {
1775+
line = token;
1776+
} else {
1777+
for (wchar_t ch : token) {
1778+
std::wstring next = line;
1779+
next.push_back(ch);
1780+
if (!line.empty() && measure_line_width_mm(font_with_cache, font_prop, next) > wrap_width_mm) {
1781+
flush_line();
1782+
line.assign(1, ch);
1783+
} else {
1784+
line.swap(next);
1785+
}
1786+
}
1787+
}
1788+
}
1789+
flush_line();
1790+
1791+
// Preserve blank lines from consecutive Enter presses
1792+
if (para_end < src.size() && paragraph.empty()) {
1793+
if (!out.empty())
1794+
out.push_back('\n');
1795+
}
1796+
1797+
if (para_end >= src.size())
1798+
break;
1799+
para_start = para_end + 1;
1800+
}
1801+
1802+
return boost::nowide::narrow(out);
1803+
}
1804+
1805+
std::string Emboss::fit_text_to_box(FontFileWithCache &font_with_cache,
1806+
FontProp & font_prop,
1807+
const std::string &text,
1808+
double wrap_width_mm,
1809+
double wrap_height_mm,
1810+
bool do_wrap,
1811+
bool do_shrink)
1812+
{
1813+
if (!font_with_cache.has_value() || text.empty())
1814+
return text;
1815+
1816+
const float original_size = font_prop.size_in_mm;
1817+
std::string working = text;
1818+
// Work on a private glyph cache so fitting does not wipe the caller's style-manager cache.
1819+
FontFileWithCache local_font = font_with_cache;
1820+
local_font.cache = std::make_shared<Glyphs>();
1821+
1822+
auto rebuild = [&](float size) {
1823+
font_prop.size_in_mm = size;
1824+
local_font.cache = std::make_shared<Glyphs>();
1825+
working = do_wrap && wrap_width_mm > 1e-3 ? apply_text_wrap(local_font, text, font_prop, wrap_width_mm) : text;
1826+
};
1827+
1828+
rebuild(original_size);
1829+
1830+
if (!do_shrink || wrap_width_mm <= 1e-3)
1831+
return working;
1832+
1833+
auto fits = [&]() {
1834+
// Width: every line must fit (wrap already enforces when enabled; still check for shrink-only)
1835+
std::wstring ws = boost::nowide::widen(working);
1836+
size_t start = 0;
1837+
while (start <= ws.size()) {
1838+
size_t end = ws.find('\n', start);
1839+
if (end == std::wstring::npos)
1840+
end = ws.size();
1841+
if (measure_line_width_mm(local_font, font_prop, ws.substr(start, end - start)) > wrap_width_mm + 1e-3)
1842+
return false;
1843+
if (end >= ws.size())
1844+
break;
1845+
start = end + 1;
1846+
}
1847+
if (wrap_height_mm > 1e-3 && measure_text_height_mm(local_font, font_prop, working) > wrap_height_mm + 1e-3)
1848+
return false;
1849+
return true;
1850+
};
1851+
1852+
if (fits())
1853+
return working;
1854+
1855+
float lo = 1.f;
1856+
float hi = original_size;
1857+
for (int iter = 0; iter < 16; ++iter) {
1858+
float mid = 0.5f * (lo + hi);
1859+
rebuild(mid);
1860+
if (fits())
1861+
lo = mid;
1862+
else
1863+
hi = mid;
1864+
}
1865+
rebuild(lo);
1866+
if (font_prop.size_in_mm < 1.f)
1867+
font_prop.size_in_mm = 1.f;
1868+
return working;
1869+
}
1870+
17051871
namespace {
17061872

17071873
void add_quad(uint32_t i1,

src/libslic3r/Emboss.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,25 @@ namespace Emboss
169169
/// <returns>Conversion to mm</returns>
170170
double get_text_shape_scale(const FontProp &fp, const FontFile &ff);
171171

172+
/// <summary>
173+
/// Insert line breaks so each line fits wrap_width_mm. Preserves existing '\n'.
174+
/// Word-wraps on spaces; breaks long words by character when needed.
175+
/// </summary>
176+
std::string apply_text_wrap(FontFileWithCache &font_with_cache, const std::string &text, const FontProp &font_prop, double wrap_width_mm);
177+
178+
/// <summary>
179+
/// Optionally wrap text to width and/or shrink FontProp::size_in_mm so the layout fits a box.
180+
/// When wrap_height_mm &lt;= 0, only width is enforced.
181+
/// </summary>
182+
/// <returns>Text used for shape generation (may include inserted '\n')</returns>
183+
std::string fit_text_to_box(FontFileWithCache &font_with_cache,
184+
FontProp & font_prop,
185+
const std::string &text,
186+
double wrap_width_mm,
187+
double wrap_height_mm,
188+
bool do_wrap,
189+
bool do_shrink);
190+
172191
/// <summary>
173192
/// getter of font info by collection defined in prop
174193
/// </summary>

src/libslic3r/Format/bbs_3mf.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ static constexpr const char* THICKNESS_ATTR = "thickness";
249249
static constexpr const char* EMBEDED_DEPTH_ATTR = "embeded_depth";
250250
static constexpr const char* ROTATE_ANGLE_ATTR = "rotate_angle";
251251
static constexpr const char* TEXT_GAP_ATTR = "text_gap";
252+
static constexpr const char* WRAP_TEXT_ATTR = "wrap_text";
253+
static constexpr const char* WRAP_WIDTH_ATTR = "wrap_width";
254+
static constexpr const char* WRAP_HEIGHT_ATTR = "wrap_height";
255+
static constexpr const char* AUTO_SHRINK_ATTR = "auto_shrink";
252256
static constexpr const char* BOLD_ATTR = "bold";
253257
static constexpr const char* ITALIC_ATTR = "italic";
254258
static constexpr const char *SURFACE_TYPE = "surface_type";
@@ -4990,6 +4994,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
49904994
text_info.m_embeded_depth = bbs_get_attribute_value_float(attributes, num_attributes, EMBEDED_DEPTH_ATTR);
49914995
text_info.m_rotate_angle = bbs_get_attribute_value_float(attributes, num_attributes, ROTATE_ANGLE_ATTR);
49924996
text_info.m_text_gap = bbs_get_attribute_value_float(attributes, num_attributes, TEXT_GAP_ATTR);
4997+
if (bbs_has_attribute_value_int(attributes, num_attributes, WRAP_TEXT_ATTR))
4998+
text_info.m_wrap_text = bbs_get_attribute_value_int(attributes, num_attributes, WRAP_TEXT_ATTR) != 0;
4999+
if (bbs_has_attribute_value_int(attributes, num_attributes, WRAP_WIDTH_ATTR))
5000+
text_info.m_wrap_width_mm = bbs_get_attribute_value_float(attributes, num_attributes, WRAP_WIDTH_ATTR);
5001+
if (bbs_has_attribute_value_int(attributes, num_attributes, WRAP_HEIGHT_ATTR))
5002+
text_info.m_wrap_height_mm = bbs_get_attribute_value_float(attributes, num_attributes, WRAP_HEIGHT_ATTR);
5003+
if (bbs_has_attribute_value_int(attributes, num_attributes, AUTO_SHRINK_ATTR))
5004+
text_info.m_auto_shrink = bbs_get_attribute_value_int(attributes, num_attributes, AUTO_SHRINK_ATTR) != 0;
49935005

49945006
text_info.m_bold = bbs_get_attribute_value_int(attributes, num_attributes, BOLD_ATTR);
49955007
text_info.m_italic = bbs_get_attribute_value_int(attributes, num_attributes, ITALIC_ATTR);
@@ -8038,6 +8050,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
80388050
stream << EMBEDED_DEPTH_ATTR << "=\"" << text_info.m_embeded_depth << "\" ";
80398051
stream << ROTATE_ANGLE_ATTR << "=\"" << text_info.m_rotate_angle << "\" ";
80408052
stream << TEXT_GAP_ATTR << "=\"" << text_info.m_text_gap << "\" ";
8053+
stream << WRAP_TEXT_ATTR << "=\"" << (text_info.m_wrap_text ? 1 : 0) << "\" ";
8054+
stream << WRAP_WIDTH_ATTR << "=\"" << text_info.m_wrap_width_mm << "\" ";
8055+
stream << WRAP_HEIGHT_ATTR << "=\"" << text_info.m_wrap_height_mm << "\" ";
8056+
stream << AUTO_SHRINK_ATTR << "=\"" << (text_info.m_auto_shrink ? 1 : 0) << "\" ";
80418057

80428058
stream << BOLD_ATTR << "=\"" << (text_info.m_bold ? 1 : 0) << "\" ";
80438059
stream << ITALIC_ATTR << "=\"" << (text_info.m_italic ? 1 : 0) << "\" ";

src/libslic3r/Model.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,10 @@ struct TextInfo
854854
float m_embeded_depth = 0.f;
855855
float m_rotate_angle = 0;
856856
float m_text_gap = 0.f;
857+
bool m_wrap_text = false;
858+
float m_wrap_width_mm = 40.f;
859+
float m_wrap_height_mm = 0.f; // 0 = no height limit
860+
bool m_auto_shrink = false;
857861
bool m_is_surface_text = false;//for old
858862
bool m_keep_horizontal = false;//for old
859863
enum TextType {
@@ -867,8 +871,8 @@ struct TextInfo
867871

868872
RaycastResult m_rr;
869873
template<typename Archive> void serialize(Archive &ar) {
870-
ar(m_font_name, m_font_version, m_font_size, m_curr_font_idx, m_bold, m_italic, m_thickness, m_embeded_depth, m_rotate_angle, m_text_gap, m_surface_type, m_text,
871-
m_rr,text_configuration);
874+
ar(m_font_name, m_font_version, m_font_size, m_curr_font_idx, m_bold, m_italic, m_thickness, m_embeded_depth, m_rotate_angle, m_text_gap, m_wrap_text,
875+
m_wrap_width_mm, m_wrap_height_mm, m_auto_shrink, m_surface_type, m_text, m_rr, text_configuration);
872876
}
873877
TextConfiguration text_configuration;
874878
};

src/slic3r/GUI/Gizmos/GLGizmoText.cpp

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,15 +1351,6 @@ std::unique_ptr<Emboss::DataBase> GLGizmoText::create_emboss_data_base(
13511351
assert(style_manager.get_wx_font().IsOk());
13521352
assert(style.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0);
13531353

1354-
// Multiline placement uses the Emboss 2D layout, so character spacing must live in FontProp.
1355-
// Single-line text still applies m_text_gap in GenerateTextJob::calc_position_points.
1356-
if (get_count_lines(text) > 1) {
1357-
if (std::abs(m_text_gap) > 1e-4f)
1358-
style.prop.char_gap = static_cast<int>(std::lround(m_text_gap));
1359-
else
1360-
style.prop.char_gap.reset();
1361-
}
1362-
13631354
bool is_outside = (type == ModelVolumeType::MODEL_PART);
13641355

13651356
// Cancel previous Job, when it is in process
@@ -1378,7 +1369,28 @@ std::unique_ptr<Emboss::DataBase> GLGizmoText::create_emboss_data_base(
13781369
base.from_surface = style.distance;
13791370

13801371
FontFileWithCache &font = style_manager.get_font_file_with_cache();
1381-
TextConfiguration tc{static_cast<EmbossStyle>(style), text};
1372+
1373+
// Apply auto-wrap / shrink into the shape string; keep UI m_text as the editable source.
1374+
std::string shape_text = text;
1375+
if ((m_wrap_text || m_auto_shrink) && font.has_value()) {
1376+
FontProp fit_prop = style.prop;
1377+
fit_prop.size_in_mm = m_font_size;
1378+
shape_text = fit_text_to_box(font, fit_prop, text, m_wrap_width_mm, m_wrap_height_mm, m_wrap_text, m_auto_shrink);
1379+
style.prop.size_in_mm = fit_prop.size_in_mm;
1380+
m_font_size = fit_prop.size_in_mm;
1381+
m_style_manager.get_font_prop().size_in_mm = fit_prop.size_in_mm;
1382+
}
1383+
1384+
// Multiline placement uses the Emboss 2D layout, so character spacing must live in FontProp.
1385+
// Single-line text still applies m_text_gap in GenerateTextJob::calc_position_points.
1386+
if (get_count_lines(shape_text) > 1) {
1387+
if (std::abs(m_text_gap) > 1e-4f)
1388+
style.prop.char_gap = static_cast<int>(std::lround(m_text_gap));
1389+
else
1390+
style.prop.char_gap.reset();
1391+
}
1392+
1393+
TextConfiguration tc{static_cast<EmbossStyle>(style), shape_text};
13821394
auto td_ptr = std::make_unique<TextDataBase>(std::move(base), font, std::move(tc), style.projection);
13831395
return td_ptr;
13841396
}
@@ -2366,6 +2378,38 @@ void GLGizmoText::on_render_input_window(float x, float y, float bottom_limit)
23662378

23672379

23682380

2381+
ImGui::AlignTextToFramePadding();
2382+
bool wrap_changed = ImGui::Checkbox((_L("Auto wrap")).c_str(), &m_wrap_text);
2383+
ImGui::SameLine();
2384+
bool shrink_changed = ImGui::Checkbox((_L("Auto shrink")).c_str(), &m_auto_shrink);
2385+
if (wrap_changed || shrink_changed)
2386+
m_need_update_text = true;
2387+
if (m_wrap_text || m_auto_shrink) {
2388+
ImGui::AlignTextToFramePadding();
2389+
m_imgui->text(_L("Wrap width"));
2390+
ImGui::SameLine(caption_size);
2391+
ImGui::PushItemWidth(temp_input_width);
2392+
float old_wrap_w = m_wrap_width_mm;
2393+
if (ImGui::InputFloat("###text_wrap_width", &m_wrap_width_mm, 0.0f, 0.0f, "%.2f")) {
2394+
m_wrap_width_mm = ImClamp(m_wrap_width_mm, 1.f, 1000.f);
2395+
if (old_wrap_w != m_wrap_width_mm)
2396+
m_need_update_text = true;
2397+
}
2398+
ImGui::SameLine();
2399+
m_imgui->text(_L("Wrap height"));
2400+
ImGui::SameLine();
2401+
ImGui::PushItemWidth(temp_input_width);
2402+
float old_wrap_h = m_wrap_height_mm;
2403+
if (ImGui::InputFloat("###text_wrap_height", &m_wrap_height_mm, 0.0f, 0.0f, "%.2f")) {
2404+
m_wrap_height_mm = ImClamp(m_wrap_height_mm, 0.f, 1000.f);
2405+
if (old_wrap_h != m_wrap_height_mm)
2406+
m_need_update_text = true;
2407+
}
2408+
if (ImGui::IsItemHovered())
2409+
m_imgui->tooltip(_u8L("Optional max height in mm. 0 means no height limit. With Auto shrink, font size is reduced to fit the box."),
2410+
m_gui_cfg->max_tooltip_width);
2411+
}
2412+
23692413
ImGui::AlignTextToFramePadding();
23702414
m_imgui->text(_L("Text Gap"));
23712415
ImGui::SameLine(caption_size);
@@ -3096,6 +3140,10 @@ void GLGizmoText::reset_text_info()
30963140
m_embeded_depth = m_style_manager.get_style().projection.embeded_depth;
30973141
m_rotate_angle = get_angle_from_current_style();
30983142
m_text_gap = m_style_manager.get_style().prop.char_gap.value_or(0);
3143+
m_wrap_text = false;
3144+
m_wrap_width_mm = 40.f;
3145+
m_wrap_height_mm = 0.f;
3146+
m_auto_shrink = false;
30993147
m_surface_type = TextInfo::TextType::SURFACE;
31003148
m_rr = RaycastResult();
31013149
m_last_text_mv = nullptr;
@@ -3399,6 +3447,11 @@ bool GLGizmoText::generate_text_volume()
33993447
input_info.m_thickness = m_thickness;
34003448
input_info.m_cut_plane_dir_in_world = m_cut_plane_dir_in_world;
34013449
input_info.use_surface = m_surface_type == TextInfo::TextType::SURFACE_CHAR ? true : false;
3450+
if (m_style_manager.is_active_font() && m_style_manager.get_font_file_with_cache().has_value()) {
3451+
input_info.font_file = m_style_manager.get_font_file_with_cache().font_file;
3452+
input_info.font_prop = m_style_manager.get_font_prop();
3453+
input_info.font_prop.size_in_mm = m_font_size;
3454+
}
34023455
TextInfo text_info = get_text_info();
34033456

34043457
ModelObject *model_object = selection.get_model()->objects[m_object_idx];
@@ -3445,6 +3498,10 @@ TextInfo GLGizmoText::get_text_info()
34453498
text_info.m_rr.mesh_id = m_rr.mesh_id;
34463499
text_info.m_rotate_angle = m_rotate_angle;
34473500
text_info.m_text_gap = m_text_gap;
3501+
text_info.m_wrap_text = m_wrap_text;
3502+
text_info.m_wrap_width_mm = m_wrap_width_mm;
3503+
text_info.m_wrap_height_mm = m_wrap_height_mm;
3504+
text_info.m_auto_shrink = m_auto_shrink;
34483505
text_info.m_surface_type = m_surface_type;
34493506
text_info.text_configuration = m_ui_text_configuration;
34503507
text_info.text_configuration.style.prop.line_gap = m_style_manager.get_font_prop().line_gap;
@@ -3472,6 +3529,10 @@ void GLGizmoText::load_from_text_info(const TextInfo &text_info)
34723529
m_rotate_angle = (float) Geometry::rad2deg(limit_angle);
34733530

34743531
m_text_gap = text_info.m_text_gap;
3532+
m_wrap_text = text_info.m_wrap_text;
3533+
m_wrap_width_mm = text_info.m_wrap_width_mm;
3534+
m_wrap_height_mm = text_info.m_wrap_height_mm;
3535+
m_auto_shrink = text_info.m_auto_shrink;
34753536
m_surface_type = (TextInfo::TextType) text_info.m_surface_type;
34763537

34773538
if (is_old_text_info(text_info)) { // compatible with older versions

0 commit comments

Comments
 (0)