Skip to content

Commit c6c6628

Browse files
rhys100cursoragent
andcommitted
Add multiline support for Text Shape.
Finish Enter/newline layout in GenerateTextJob, persist newlines and line gap in 3MF, and expose Line gap in the text tool UI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ba049f6 commit c6c6628

6 files changed

Lines changed: 144 additions & 39 deletions

File tree

src/libslic3r/Emboss.cpp

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,9 +1475,16 @@ void Slic3r::Emboss::text2vshapes(EmbossShape & emboss_shape,
14751475
text_scales.emplace_back(real_scale);
14761476
}
14771477
cur_x = cursor.x() * standard_scale;
1478-
text_cursors.emplace_back(cur_x - last_x);
1479-
text_absolute_cursors.emplace_back(cur_x);
1480-
last_x = cur_x;
1478+
// After a line break cursor.x is reset; do not treat that as a negative advance into the previous line.
1479+
if (letter == '\n' || letter == '\r') {
1480+
text_cursors.emplace_back(0.f);
1481+
text_absolute_cursors.emplace_back(cur_x);
1482+
last_x = 0.f;
1483+
} else {
1484+
text_cursors.emplace_back(cur_x - last_x);
1485+
text_absolute_cursors.emplace_back(cur_x);
1486+
last_x = cur_x;
1487+
}
14811488
}
14821489
std::vector<Point> offset_xy;
14831490
if (bfc_fn) {// support_backup_fonts
@@ -1487,11 +1494,13 @@ void Slic3r::Emboss::text2vshapes(EmbossShape & emboss_shape,
14871494
}
14881495
std::vector<Vec2f> text_align_offsets;
14891496
text_align_offsets.resize(text_scales.size());
1490-
for (int i = 0; i < text_scales.size(); i++) {
1491-
float x = (float) offset_xy[i][0] * standard_scale;
1492-
float y = (float) offset_xy[i][1] * standard_scale;
1493-
text_align_offsets[i][0] = x;
1494-
text_align_offsets[i][1] = y;
1497+
for (int i = 0; i < (int) text_scales.size(); i++) {
1498+
if (i < (int) offset_xy.size()) {
1499+
text_align_offsets[i][0] = (float) offset_xy[i][0] * standard_scale;
1500+
text_align_offsets[i][1] = (float) offset_xy[i][1] * standard_scale;
1501+
} else {
1502+
text_align_offsets[i] = Vec2f::Zero();
1503+
}
14951504
}
14961505
emboss_shape.text_align_offsets = text_align_offsets;
14971506
std::vector<float> no_use_text_scales;
@@ -2193,8 +2202,8 @@ void align_shape(ExPolygonsWithIds &shapes, std::vector<Point> &offset_xy, const
21932202

21942203
// Speed up for left aligned text
21952204
if (prop.align.first == FontProp::HorizontalAlign::left){
2196-
// already horizontaly aligned
2197-
offset_xy.emplace_back(Point(0, y_offset));
2205+
// already horizontaly aligned — still emit one offset per character for callers
2206+
offset_xy.assign(shapes.size(), Point(0, y_offset));
21982207
for (ExPolygonsWithId& shape : shapes)
21992208
for (ExPolygon &s : shape.expoly)
22002209
s.translate(Point(0, y_offset));
@@ -2216,9 +2225,11 @@ void align_shape(ExPolygonsWithIds &shapes, std::vector<Point> &offset_xy, const
22162225
Point offset(
22172226
get_align_x_offset(prop.align.first, shape_bb, get_line_bb(0)),
22182227
y_offset);
2228+
offset_xy.reserve(shapes.size());
22192229
for (size_t i = 0; i < shapes.size(); ++i) {
22202230
wchar_t letter = text[i];
22212231
if (letter == '\n'){
2232+
offset_xy.emplace_back(offset); // keep 1:1 with shapes/text for multiline
22222233
offset.x() = get_align_x_offset(prop.align.first, shape_bb, get_line_bb(i + 1));
22232234
continue;
22242235
}
@@ -2272,9 +2283,11 @@ void align_shape(ExPolygonsWithIds & shapes,
22722283

22732284
// Align x line by line
22742285
Point main_offset(get_align_x_offset(prop.align.first, shape_bb, get_line_bb(0)), main_y_offset);
2286+
offset_xy.reserve(shapes.size());
22752287
for (size_t i = 0; i < shapes.size(); ++i) {
22762288
wchar_t letter = text[i];
22772289
if (letter == '\n') {//Enter the next line of text
2290+
offset_xy.emplace_back(main_offset); // keep 1:1 with shapes/text for multiline
22782291
main_offset.x() = get_align_x_offset(prop.align.first, shape_bb, get_line_bb(i + 1));
22792292
continue;
22802293
}

src/libslic3r/Format/bbs_3mf.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4979,6 +4979,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
49794979
text_info.text_configuration.style.prop.boldness = bbs_get_attribute_value_float(attributes, num_attributes, BOLDNESS_ATTR);
49804980
text_info.text_configuration.style.prop.skew = bbs_get_attribute_value_float(attributes, num_attributes, SKEW_ATTR);
49814981
}
4982+
if (bbs_has_attribute_value_int(attributes, num_attributes, LINE_GAP_ATTR))
4983+
text_info.text_configuration.style.prop.line_gap = bbs_get_attribute_value_int(attributes, num_attributes, LINE_GAP_ATTR);
4984+
if (bbs_has_attribute_value_int(attributes, num_attributes, CHAR_GAP_ATTR))
4985+
text_info.text_configuration.style.prop.char_gap = bbs_get_attribute_value_int(attributes, num_attributes, CHAR_GAP_ATTR);
49824986
text_info.m_curr_font_idx = bbs_get_attribute_value_int(attributes, num_attributes, FONT_INDEX_ATTR);
49834987

49844988
text_info.m_font_size = bbs_get_attribute_value_float(attributes, num_attributes, FONT_SIZE_ATTR);
@@ -8015,12 +8019,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
80158019
void _add_text_info_to_archive(std::stringstream& stream, const TextInfo& text_info) {
80168020
stream << " <" << TEXT_INFO_TAG << " ";
80178021

8018-
stream << TEXT_ATTR << "=\"" << xml_escape(text_info.m_text) << "\" ";
8022+
// Preserve newlines/tabs in text attributes (XML attribute whitespace normalisation).
8023+
stream << TEXT_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(text_info.m_text) << "\" ";
80198024
stream << FONT_NAME_ATTR << "=\"" << xml_escape(text_info.m_font_name) << "\" ";
80208025
stream << FONT_VERSION_ATTR << "=\"" << text_info.m_font_version << "\" ";
80218026
stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(text_info.text_configuration.style.name) << "\" ";
80228027
stream << BOLDNESS_ATTR << "=\"" << text_info.text_configuration.style.prop.boldness.value_or(0) << "\" ";
80238028
stream << SKEW_ATTR << "=\"" << text_info.text_configuration.style.prop.skew.value_or(0) << "\" ";
8029+
if (text_info.text_configuration.style.prop.line_gap.has_value())
8030+
stream << LINE_GAP_ATTR << "=\"" << *text_info.text_configuration.style.prop.line_gap << "\" ";
8031+
if (text_info.text_configuration.style.prop.char_gap.has_value())
8032+
stream << CHAR_GAP_ATTR << "=\"" << *text_info.text_configuration.style.prop.char_gap << "\" ";
80248033

80258034
stream << FONT_INDEX_ATTR << "=\"" << text_info.m_curr_font_idx << "\" ";
80268035

src/slic3r/GUI/Gizmos/GLGizmoText.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,15 @@ 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+
13541363
bool is_outside = (type == ModelVolumeType::MODEL_PART);
13551364

13561365
// Cancel previous Job, when it is in process
@@ -2522,7 +2531,8 @@ bool GLGizmoText::set_height()
25222531

25232532
void GLGizmoText::draw_text_input(int caption_width)
25242533
{
2525-
bool allow_multi_line = false;
2534+
// Enter creates a new line; Emboss::text2vshapes + GenerateTextJob baked layout consume '\n'.
2535+
bool allow_multi_line = true;
25262536
bool support_backup_fonts = GUI::wxGetApp().app_config->get_bool("support_backup_fonts");
25272537
auto create_range_text_prep = [&mng = m_style_manager, &text = m_text, &exist_unknown = m_text_contain_unknown_glyph, support_backup_fonts]() {
25282538
if (text.empty()) { return std::string(); }
@@ -2983,6 +2993,34 @@ void GLGizmoText::draw_advanced(float caption_size, float slider_width, float sl
29832993
m_need_update_text = true;
29842994
}
29852995
}
2996+
2997+
// Line gap (font points) — used by Emboss::get_line_height for multiline text.
2998+
std::optional<int> &line_gap = m_style_manager.get_font_prop().line_gap;
2999+
float line_gap_value = static_cast<float>(line_gap.value_or(0));
3000+
const float min_line_gap = static_cast<float>(limits.line_gap.min);
3001+
const float max_line_gap = static_cast<float>(limits.line_gap.max);
3002+
ImGui::AlignTextToFramePadding();
3003+
m_imgui->text(_L("Line gap"));
3004+
ImGui::SameLine(caption_size + ad_space_size);
3005+
ImGui::PushItemWidth(slider_width);
3006+
if (m_imgui->bbl_slider_float_style("##text_line_gap", &line_gap_value, min_line_gap, max_line_gap, "%.0f", 1.0f, true)) {
3007+
if (std::abs(line_gap_value) < 0.5f)
3008+
line_gap.reset();
3009+
else
3010+
line_gap = static_cast<int>(std::lround(line_gap_value));
3011+
}
3012+
is_stop_sliding = m_imgui->get_last_slider_status().deactivated_after_edit;
3013+
if (is_stop_sliding)
3014+
m_need_update_text = true;
3015+
ImGui::SameLine(drag_left_width + ad_space_size);
3016+
ImGui::PushItemWidth(1.5 * slider_icon_width);
3017+
if (ImGui::BBLDragFloat("##text_line_gap_input", &line_gap_value, 1, min_line_gap, max_line_gap, "%.0f")) {
3018+
if (std::abs(line_gap_value) < 0.5f)
3019+
line_gap.reset();
3020+
else
3021+
line_gap = static_cast<int>(std::lround(line_gap_value));
3022+
m_need_update_text = true;
3023+
}
29863024
}
29873025

29883026
void GLGizmoText::init_font_name_texture()
@@ -3409,6 +3447,8 @@ TextInfo GLGizmoText::get_text_info()
34093447
text_info.m_text_gap = m_text_gap;
34103448
text_info.m_surface_type = m_surface_type;
34113449
text_info.text_configuration = m_ui_text_configuration;
3450+
text_info.text_configuration.style.prop.line_gap = m_style_manager.get_font_prop().line_gap;
3451+
text_info.text_configuration.style.prop.char_gap = m_style_manager.get_font_prop().char_gap;
34123452
text_info.m_font_version = CUR_FONT_VERSION;
34133453
return text_info;
34143454
}
@@ -3446,6 +3486,8 @@ void GLGizmoText::load_from_text_info(const TextInfo &text_info)
34463486
}
34473487
m_custom_boldness = text_info.text_configuration.style.prop.boldness.value_or(0.f);
34483488
m_custom_skew = text_info.text_configuration.style.prop.skew.value_or(0.f);
3489+
m_style_manager.get_font_prop().line_gap = text_info.text_configuration.style.prop.line_gap;
3490+
m_style_manager.get_font_prop().char_gap = text_info.text_configuration.style.prop.char_gap;
34493491
if (is_text_changed) {
34503492
process(true,std::nullopt,false);
34513493
}

src/slic3r/GUI/Gizmos/GLGizmoText.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ enum class SLAGizmoEventType : unsigned char;
2727
//2.2 mean v2.2 bambu version(20250815) fix char gap of text
2828
// 2.3 mean v2.3 bambu version(20250826) add custom boldness and skew
2929
//2.5 mean v2.3 bambu version(20251130) fix text more accurate layout
30+
//2.6 mean multiline Text Shape (Enter / line breaks + line gap persistence)
3031
const std::string NEW_FONT_BEGIN_VERSION = "2.0";
31-
const std::string CUR_FONT_VERSION = "2.5";
32+
const std::string CUR_FONT_VERSION = "2.6";
3233
class GLGizmoText : public GLGizmoBase
3334
{
3435
private:

0 commit comments

Comments
 (0)