From 6a52ea1818936f4663e21d7bd57cca4020d2c8ba Mon Sep 17 00:00:00 2001 From: GlauTech <33813227+GlauTechCo@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:51:40 +0300 Subject: [PATCH 1/3] Update OrcaSlicer_tr.po (#15119) * Update OrcaSlicer_tr.po * Update OrcaSlicer_tr.po Fixed inaccurate AI-generated text and updated missing translations. * REmoive # AI Translated --------- Co-authored-by: Ian Bassi --- localization/i18n/tr/OrcaSlicer_tr.po | 157 ++++++++++++-------------- 1 file changed, 71 insertions(+), 86 deletions(-) diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 467b3c355b..63d1e5bc75 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-29 17:40-0300\n" -"PO-Revision-Date: 2026-08-01 20:32+0300\n" +"PO-Revision-Date: 2026-08-04 19:36+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" "Language: tr\n" @@ -738,9 +738,8 @@ msgstr "Sabit adım sürükleme" msgid "Context Menu" msgstr "Bağlam Menüsü" -# AI Translated msgid "Toggle Auto-Drop" -msgstr "Otomatik Bırakmayı Aç/Kapat" +msgstr "Otomatik düşürmeyi aç / kapat" msgid "Single sided scaling" msgstr "Tek taraflı ölçekleme" @@ -791,9 +790,8 @@ msgstr "Nesne" msgid "Part" msgstr "Parça" -# AI Translated msgid "Relative" -msgstr "Göreli" +msgstr "Göreceli" # AI Translated msgid "Coordinate system used for transform actions." @@ -2306,7 +2304,7 @@ msgid "new or open project file is not allowed during the slicing process!" msgstr "dilimleme işlemi sırasında yeni veya açık proje dosyasına izin verilmez!" msgid "Open Project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "The version of Orca Slicer is too low and needs to be updated to the latest version before it can be used normally." msgstr "Orca Slicer'ın sürümü çok düşük ve normal şekilde kullanılabilmesi için en son sürüme güncellenmesi gerekiyor." @@ -2734,16 +2732,15 @@ msgstr "Simit" msgid "Orca Cube" msgstr "Orca Küpü" -# AI Translated msgid "OrcaSliced Combo" -msgstr "OrcaSliced Combo" +msgstr "Orca Dilimleme Paketi" # AI Translated msgid "Orca Badge" msgstr "Orca Rozeti" msgid "Orca Tolerance Test" -msgstr "Orca tolerans testi" +msgstr "Orca Tolerans Testi" msgid "3DBenchy" msgstr "3DBenchy" @@ -2807,7 +2804,7 @@ msgid "Set as Individual Objects" msgstr "Bireysel nesneler olarak ayarla" msgid "Fill bed with copies" -msgstr "Yatağı kopyalarla doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with copies of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" @@ -2815,9 +2812,8 @@ msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" msgid "Printable" msgstr "Yazdırılabilir" -# AI Translated msgid "Auto Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" # AI Translated msgid "Automatically drops the selected object to the build plate." @@ -2967,7 +2963,7 @@ msgid "Add Models" msgstr "Model ekle" msgid "Show Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "To Objects" msgstr "Nesnelere" @@ -3009,7 +3005,7 @@ msgid "Select all objects on the current plate" msgstr "Mevcut plakadaki tüm nesneleri seç" msgid "Select All Plates" -msgstr "Tüm Plakaları Seç" +msgstr "Tüm plakaları seç" msgid "Select all objects on all plates" msgstr "Tüm plakalardaki tüm nesneleri seç" @@ -3045,13 +3041,13 @@ msgid "Remove the selected plate" msgstr "Seçilen plakayı kaldır" msgid "Add instance" -msgstr "Örnek ekle" +msgstr "Kopya ekle" msgid "Add one more instance of the selected object" msgstr "Seçilen nesnenin bir örneğini daha ekle" msgid "Remove instance" -msgstr "Örneği kaldır" +msgstr "Kopyayı kaldır" msgid "Remove one instance of the selected object" msgstr "Seçilen nesnenin bir örneğini kaldır" @@ -3060,10 +3056,10 @@ msgid "Set number of instances" msgstr "Örnek sayısını ayarlayın" msgid "Change the number of instances of the selected object" -msgstr "Seçilen nesnenin örnek sayısını değiştirme" +msgstr "Seçilen nesnenin kopya sayısını değiştirme" msgid "Fill bed with instances" -msgstr "Yatağı örneklerle doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with instances of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin örnekleriyle doldurun" @@ -3075,7 +3071,7 @@ msgid "Simplify Model" msgstr "Modeli basitleştir" msgid "Subdivision mesh" -msgstr "Alt bölüm ağı" +msgstr "Poligon artırma" msgid "(Lost color)" msgstr "(Renk kaybı)" @@ -3090,10 +3086,10 @@ msgid "Edit Process Settings" msgstr "İşlem ayarlarını düzenle" msgid "Copy Process Settings" -msgstr "İşlem Ayarlarını Kopyala" +msgstr "İşlem ayarlarını kopyala" msgid "Paste Process Settings" -msgstr "İşlem Ayarlarını Yapıştır" +msgstr "İşlem ayarlarını yapıştır" msgid "Edit print parameters for a single object" msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme" @@ -3478,7 +3474,7 @@ msgid "More" msgstr "Daha" msgid "Open Preferences" -msgstr "Tercihleri Aç" +msgstr "Tercihleri aç" msgid "Open next tip" msgstr "Sonraki ipucunu aç" @@ -5450,10 +5446,10 @@ msgid "Acceleration" msgstr "Hızlanma" msgid "Jerk" -msgstr "Jerk" +msgstr "Sarsıntı" msgid "Fan Speed" -msgstr "Fan hızı" +msgstr "Fan Hızı" msgid "Flow" msgstr "Akış" @@ -5468,7 +5464,7 @@ msgid "Layer Time" msgstr "Katman Süresi" msgid "Layer Time (log)" -msgstr "Katman Süresi (günlük)" +msgstr "Katman Süresi (log)" msgid "Pressure Advance" msgstr "Basınç İlerlemesi" @@ -5477,10 +5473,10 @@ msgid "Noop" msgstr "Hayır" msgid "Retract" -msgstr "Geri Çekme" +msgstr "Geri çekme" msgid "Unretract" -msgstr "İleri İtme" +msgstr "İleri itme" msgid "Seam" msgstr "Dikiş" @@ -5578,7 +5574,7 @@ msgid "Acceleration: " msgstr "İvme: " msgid "Jerk: " -msgstr "Jerk: " +msgstr "Sarsıntı: " msgid "PA: " msgstr "PA: " @@ -5608,7 +5604,7 @@ msgid "Actual speed profile" msgstr "Gerçek hız profili" msgid "Statistics of All Plates" -msgstr "Tüm Plakaların İstatistikleri" +msgstr "Tüm plakaların istatistikleri" msgid "Display" msgstr "Ekran" @@ -5708,7 +5704,7 @@ msgid "Acceleration (mm/s²)" msgstr "İvme (mm/s²)" msgid "Jerk (mm/s)" -msgstr "Jerk (mm/s)" +msgstr "Sarsıntı (mm/s)" msgid "Fan speed (%)" msgstr "Fan hızı (%)" @@ -5759,9 +5755,8 @@ msgstr "Normal mod" msgid "Total Filament" msgstr "Toplam filament" -# AI Translated msgid "Model Filament" -msgstr "Model Filamenti" +msgstr "Model filamenti" msgid "Prepare time" msgstr "Hazırlık süresi" @@ -6282,20 +6277,19 @@ msgid "Setup Wizard" msgstr "Kurulum sihirbazı" msgid "Show Configuration Folder" -msgstr "Yapılandırma Klasörünü Göster" +msgstr "Yapılandırma klasörünü göster" -# AI Translated msgid "Troubleshoot Center" -msgstr "Sorun Giderme Merkezi" +msgstr "Sorun giderme merkezi" msgid "Open Network Test" -msgstr "Ağ Testini Aç" +msgstr "Ağ testini aç" msgid "Show Tip of the Day" -msgstr "Günün İpucunu Göster" +msgstr "Günün ipucunu göster" msgid "Check for Updates" -msgstr "Güncellemeleri Kontrol Et" +msgstr "Güncellemeleri kontrol et" #, c-format, boost-format msgid "&About %s" @@ -6349,7 +6343,7 @@ msgid "Recent files" msgstr "Son dosyalar" msgid "Save Project" -msgstr "Projeyi Kaydet" +msgstr "Projeyi kaydet" msgid "Save current project to file" msgstr "Mevcut projeyi dosyaya kaydet" @@ -6415,13 +6409,13 @@ msgid "Export toolpaths as OBJ" msgstr "Takımyollarını OBJ olarak dışa aktar" msgid "Export Preset Bundle" -msgstr "Ön Ayar Paketini Dışa Aktar" +msgstr "Ön ayar paketini dışa aktar" msgid "Export current configuration to files" msgstr "Geçerli yapılandırmayı dosyalara aktar" msgid "Export" -msgstr "Dışa Aktar" +msgstr "Dışa aktar" msgid "Quit" msgstr "Çıkış" @@ -6478,13 +6472,13 @@ msgid "Deselects all objects" msgstr "Tüm nesnelerin seçimini kaldırır" msgid "Use Perspective View" -msgstr "Perspektif Görünüm" +msgstr "Perspektif görünüm" msgid "Use Orthogonal View" -msgstr "Ortogonal Görünüm" +msgstr "Ortogonal görünüm" msgid "Auto Perspective" -msgstr "Otomatik Perspektif" +msgstr "Otomatik perspektif" msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." msgstr "Üst/Alt/Yan görünümler arasında geçiş yaparken ortografik ve perspektif arasında otomatik olarak geçiş yapın." @@ -6496,37 +6490,37 @@ msgid "Show G-code window in Preview scene." msgstr "Previce sahnesinde G-kodu penceresini göster." msgid "Show 3D Navigator" -msgstr "3D Gezgini Göster" +msgstr "3D gezgini göster" msgid "Show 3D navigator in Prepare and Preview scene." msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster." msgid "Show Gridlines" -msgstr "Kılavuz Çizgilerini Göster" +msgstr "Kılavuz çizgilerini göster" msgid "Show Gridlines on plate" msgstr "Kılavuz Çizgilerini plaka üzerinde göster" msgid "Reset Window Layout" -msgstr "Pencere Düzenini Sıfırla" +msgstr "Pencere düzenini sıfırla" msgid "Reset to default window layout" msgstr "Varsayılan pencere düzenine sıfırla" msgid "Show &Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "Show object labels in 3D scene." msgstr "3B sahnede nesne etiketlerini göster." msgid "Show &Overhang" -msgstr "Çıkıntıyı Göster" +msgstr "Çıkıntıyı göster" msgid "Show object overhang highlight in 3D scene." msgstr "3B sahnede nesne çıkıntısı vurgusunu göster." msgid "Show Selected Outline (beta)" -msgstr "Seçilen Taslağı Göster (Deneysel)" +msgstr "Seçilen taslağı göster (deneysel)" msgid "Show outline around selected object in 3D scene." msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." @@ -6542,13 +6536,11 @@ msgstr "Düzen" msgid "View" msgstr "Görünüm" -# AI Translated msgid "Preset Bundle" -msgstr "Ön Ayar Paketi" +msgstr "Ön ayar paketi" -# AI Translated msgid "Sync Presets" -msgstr "Ön Ayarları Eşitle" +msgstr "Ön ayarları eşitle" # AI Translated msgid "Pull and apply the latest presets from OrcaCloud" @@ -6590,10 +6582,10 @@ msgid "Cornering calibration" msgstr "Viraj kalibrasyonu" msgid "Input Shaping Frequency" -msgstr "Input shaping Frekansı" +msgstr "Input shaping frekansı" msgid "Input Shaping Damping/zeta factor" -msgstr "Input shaping Sönümleme/zeta faktörü" +msgstr "Input shaping sönümleme/zeta faktörü" msgid "Input Shaping" msgstr "Input shaping" @@ -6601,9 +6593,8 @@ msgstr "Input shaping" msgid "VFA" msgstr "VFA" -# AI Translated msgid "Calibration Guide" -msgstr "Kalibrasyon Kılavuzu" +msgstr "Kalibrasyon kılavuzu" msgid "&Open G-code" msgstr "&G kodunu aç" @@ -8219,9 +8210,8 @@ msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yük msgid "An object with multiple parts was detected" msgstr "Birden fazla parçaya sahip nesne algılandı" -# AI Translated msgid "Auto-Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" #, c-format, boost-format msgid "Connected printer is %s. It must match the project preset for printing.\n" @@ -8296,9 +8286,8 @@ msgstr "Seçilen nesne bölünemedi." msgid "Split to Objects" msgstr "Nesnelere Ayır" -# AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" -msgstr "Z konumunu korumak için Otomatik Bırakma devre dışı bırakılsın mı?\n" +msgstr "Z konumunu korumak için Otomatik düşürme devre dışı bırakılsın mı?\n" # AI Translated msgid "Object with floating parts was detected" @@ -8433,7 +8422,7 @@ msgid "Creating a new project" msgstr "Yeni bir proje oluşturma" msgid "Load project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "" "Failed to save the project.\n" @@ -8869,10 +8858,10 @@ msgid "Current Association: " msgstr "Mevcut Bağlantı: " msgid "Current Instance" -msgstr "Mevcut Örnek" +msgstr "Mevcut Kopya" msgid "Current Instance Path: " -msgstr "Mevcut Örnek Yolu: " +msgstr "Mevcut Kopya Yolu: " msgid "General" msgstr "Genel" @@ -10708,7 +10697,7 @@ msgid "Reserved keywords found" msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" -msgstr "Ayarların Üzerine Yazma" +msgstr "Ayarların Üzerine Yaz" msgid "Basic information" msgstr "Temel Bilgiler" @@ -12227,7 +12216,7 @@ msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Elle modda gruplama hatası. Lütfen nozul sayısını denetleyin veya yeniden gruplayın." msgid "Internal Bridge" -msgstr "İç Köprü" +msgstr "İç köprü" msgid "undefined error" msgstr "bilinmeyen hata" @@ -12920,7 +12909,6 @@ msgstr "Çıkıntı bu belirtilen eşiği aştığında, soğutma fanını aşa msgid "External bridge infill direction" msgstr "Dış köprü dolgu yönü" -# AI Translated #, no-c-format, no-boost-format msgid "" "External Bridging angle override.\n" @@ -12937,14 +12925,13 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: ‘Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." msgid "Internal bridge infill direction" msgstr "İç köprü dolgu yönü" -# AI Translated msgid "" "Internal Bridging angle override.\n" "If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" @@ -12960,13 +12947,12 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: 'Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." -# AI Translated msgid "Relative bridge angle" -msgstr "Göreli köprü açısı" +msgstr "Göreceli köprü açısı" # AI Translated msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." @@ -13413,7 +13399,7 @@ msgstr "" "Not: Elde edilen değer ilk katman akış oranından etkilenmez." msgid "Brim follows compensated outline" -msgstr "Kenar telafi edilen taslağı takip ediyor" +msgstr "Kenar toleranslı dış sınırı takip etsin" # AI Translated msgid "" @@ -13724,13 +13710,13 @@ msgid "" msgstr "" "Dikey kabuk kalınlığını garanti etmek için eğimli yüzeylerin yakınına katı dolgu ekleyin (üst + alt katı katmanlar)\n" "Yok: Hiçbir yere katı dolgu eklenmez. Dikkat: Modelinizin eğimli yüzeyleri varsa bu seçeneği dikkatli kullanın.\n" -"Yalnızca kritik: Duvarlar için katı dolgu eklemekten kaçının\n" +"Kritik: Duvarlar için katı dolgu eklemekten kaçının\n" "Orta: Yalnızca çok eğimli yüzeyler için katı dolgu ekleyin\n" "Hepsi: Tüm uygun eğimli yüzeyler için katı dolgu ekleyin\n" "Varsayılan değer Tümü'dür." msgid "Critical Only" -msgstr "Yalnızca kritik" +msgstr "Kritik" msgid "Moderate" msgstr "Orta" @@ -14247,7 +14233,7 @@ msgid "By First filament" msgstr "İlk filamente göre" msgid "By Highest Temp" -msgstr "En Yüksek Sıcaklığa Göre" +msgstr "En yüksek sıcaklığa göre" msgid "Filament diameter is used to calculate extrusion variables in G-code, so it is important that this is accurate and precise." msgstr "Filament çapı, gcode'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır." @@ -14685,10 +14671,10 @@ msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk sett msgstr "Marlin Firmware Köşe Sapması (geleneksel XY Sarsıntı ayarının yerini alır)" msgid "Jerk of outer walls." -msgstr "Dış duvar JERK değeri." +msgstr "Dış duvar sarsıntı değeri." msgid "Jerk of inner walls." -msgstr "İç duvarlar JERK değeri." +msgstr "İç duvarlar sarsıntı değeri." msgid "Jerk for top surface." msgstr "Üst yüzey için JERK değeri." @@ -15721,7 +15707,7 @@ msgid "" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)" msgstr "" "Maksimum bağlantı sapması (M205 J, yalnızca Marlin Aygıt Yazılımı için JD > 0 ise geçerlidir)\n" -"Marlin 2 yazıcınız Classic Jerk kullanıyorsa bu değeri 0 olarak ayarlayın.)" +"Marlin 2 yazıcınız Classic sarsıntı kullanıyorsa bu değeri 0 olarak ayarlayın.)" msgid "Minimum speed for extruding" msgstr "Ekstrüzyon için minimum hız" @@ -16914,7 +16900,7 @@ msgid "This setting only generates supports that begin on the build plate." msgstr "Model yüzeyinde destek oluşturmayın, yalnızca baskı plakasında." msgid "Support critical regions only" -msgstr "Yalnızca kritik bölgeleri destekleyin" +msgstr "Kritik bölgeleri destekleyin" msgid "Only create support for critical regions including sharp tail, cantilever, etc." msgstr "Yalnızca keskin kuyruk, konsol vb. gibi kritik bölgeler için destek oluşturun." @@ -19288,13 +19274,13 @@ msgid "" "To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." msgstr "" "Marlin 2 Kavşak Sapması tespit edildi:\n" -"Classic Jerk'i test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." +"Classic sarsıntıyı test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." msgid "" "Marlin 2 Classic Jerk detected:\n" "To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." msgstr "" -"Marlin 2 Classic Jerk tespit edildi:\n" +"Marlin 2 Classic sarsıntı tespit edildi:\n" "Kavşak Sapmasını test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı > 0 değerine ayarlayın." msgid "" @@ -19538,7 +19524,7 @@ msgid "Start Test Single-Thread" msgstr "Tek İş Parçacığı Testini Başlat" msgid "Export Log" -msgstr "Logu Dışa Aktar" +msgstr "Logu dışa aktar" msgid "OrcaSlicer Version:" msgstr "OrcaSlicer Sürümü:" @@ -20281,9 +20267,8 @@ msgstr "Sistem klasörü silinemedi..." msgid "Failed to determine executable path." msgstr "Yürütülebilir dosya yolu belirlenemedi." -# AI Translated msgid "Failed to launch a new instance." -msgstr "Yeni bir örnek başlatılamadı." +msgstr "Yeni bir kopya başlatılamadı." # AI Translated msgid "log(s)" From 322dc9b6a69932198db0f97fe872102d8827bc7f Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 18 Aug 2026 12:19:27 -0300 Subject: [PATCH 2/3] Smooth more patterns (#15205) --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Fill/Fill.cpp | 6 +- src/libslic3r/Fill/Fill3DHoneycomb.cpp | 4 + src/libslic3r/Fill/FillConcentric.cpp | 23 +- src/libslic3r/Fill/FillCornerSmoothing.cpp | 226 ++++++++++++++ src/libslic3r/Fill/FillCornerSmoothing.hpp | 108 +++++++ src/libslic3r/Fill/FillCrossHatch.cpp | 4 + src/libslic3r/Fill/FillHoneycomb.cpp | 4 + src/libslic3r/Fill/FillLightning.cpp | 14 + src/libslic3r/Fill/FillPlanePath.cpp | 206 ++++--------- src/libslic3r/Fill/FillPlanePath.hpp | 6 +- src/libslic3r/Fill/FillRectilinear.cpp | 5 + src/libslic3r/PrintConfig.cpp | 5 +- src/libslic3r/PrintConfig.hpp | 23 ++ src/slic3r/GUI/ConfigManipulation.cpp | 2 +- tests/fff_print/test_fill.cpp | 287 ++++++++++++++++++ tests/libslic3r/CMakeLists.txt | 1 + .../libslic3r/test_fill_corner_smoothing.cpp | 173 +++++++++++ tests/libslic3r/test_fill_plane_path.cpp | 54 ++++ 19 files changed, 996 insertions(+), 157 deletions(-) create mode 100644 src/libslic3r/Fill/FillCornerSmoothing.cpp create mode 100644 src/libslic3r/Fill/FillCornerSmoothing.hpp create mode 100644 tests/libslic3r/test_fill_corner_smoothing.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 812d28e088..f7b4de6e25 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -149,6 +149,8 @@ set(lisbslic3r_sources Fill/FillConcentric.hpp Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.hpp + Fill/FillCornerSmoothing.cpp + Fill/FillCornerSmoothing.hpp Fill/Fill.cpp Fill/FillCrossHatch.cpp Fill/FillCrossHatch.hpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 88d87ddb26..0888e1bb55 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -970,9 +970,9 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p region_config.sparse_infill_rotate_template.value); params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty(); - // Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill. - // FillHilbertCurve::generate clamps and validates the value itself. - if (params.pattern == ipHilbertCurve) + // Orca: the smoothing factor only applies to the sparse infill patterns that + // implement it. The fills clamp and validate the value themselves. + if (is_smoothable_infill_pattern(params.pattern, params.multiline)) params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value; } else { const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.; diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.cpp b/src/libslic3r/Fill/Fill3DHoneycomb.cpp index 5908f854de..ad5f8918fd 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.cpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "Fill3DHoneycomb.hpp" namespace Slic3r { @@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single( for (Polyline &pl : polylines){ pl.translate(bb.min); pl.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a + // straight line have no corner to round. + smooth_polyline_corners(pl, params.smooth_factor, scaled(params.resolution)); } // Apply multiline offset if needed diff --git a/src/libslic3r/Fill/FillConcentric.cpp b/src/libslic3r/Fill/FillConcentric.cpp index a75d2ed7d3..1882f7a656 100644 --- a/src/libslic3r/Fill/FillConcentric.cpp +++ b/src/libslic3r/Fill/FillConcentric.cpp @@ -5,6 +5,7 @@ #include "Arachne/WallToolPaths.hpp" #include "FillConcentric.hpp" +#include "FillCornerSmoothing.hpp" #include namespace Slic3r { @@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single( Polygons loops = to_polygons(contracted); - ExPolygons last { std::move(contracted) }; + ExPolygons last { contracted }; while (! last.empty()) { last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2); append(loops, to_polygons(last)); } + // Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the + // fill region - they are its offsets - so a corner may only be rounded where the curve replacing it + // stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave + // feature or across a thin region is outside the fill and would put the extrusion over a wall. + // The reach is capped at half the distance between two loops as well: a loop is as long as the + // object, and a corner cut by half of its side would swallow the neighbouring loops. + auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) { + // The straight chord between the ends of the curve is the deepest the curve can cut. + for (const double t : { 0.25, 0.5, 0.75 }) { + const Vec2d sample = from + t * (to - from); + const Point point(coord_t(sample.x()), coord_t(sample.y())); + if (std::none_of(contracted.begin(), contracted.end(), + [&point](const ExPolygon ®ion) { return region.contains(point); })) + return false; + } + return true; + }; + smooth_polygons_corners(loops, params.smooth_factor, scaled(params.resolution), 0.5 * distance, + corner_stays_inside); + // generate paths from the outermost to the innermost, to avoid // adhesion problems of the first central tiny loops loops = union_pt_chained_outside_in(loops); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp new file mode 100644 index 0000000000..2af9f6bb9c --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -0,0 +1,226 @@ +#include + +#include "FillCornerSmoothing.hpp" + +namespace Slic3r { + +// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly +// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin. +static constexpr const double min_smoothed_turn_cosine = -0.9; + +// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not +// orthonormal for turns other than a right angle. +using QuinticBezier = std::array; + +static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation) +{ + // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every + // control point within a deviation-wide strip around the endpoint chord conservatively bounds the + // flattening error. The cross product is the perpendicular distance scaled by the chord length; + // comparing squared values avoids a square root. + auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; }; + const Vec2d chord = in_plane(curve.back() - curve.front()); + const double chord_length_sq = chord.squaredNorm(); + const double max_cross_sq = deviation * deviation * chord_length_sq; + + for (size_t i = 1; i + 1 < curve.size(); ++i) { + const Vec2d offset = in_plane(curve[i] - curve.front()); + const double cross = chord.x() * offset.y() - chord.y() * offset.x(); + if (cross * cross > max_cross_sq) + return false; + } + return true; +} + +static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) +{ + // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one + // control point to the left half and one to the right half; the latter is filled backwards to keep + // both resulting control polygons in their original parameter direction. + QuinticBezier subdivision = curve; + left.front() = subdivision.front(); + right.back() = subdivision.back(); + for (size_t level = 1; level < curve.size(); ++level) { + for (size_t i = 0; i + level < curve.size(); ++i) + subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); + left[level] = subdivision.front(); + right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; + } +} + +static void flatten_bezier( + const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector &output) +{ + // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. + // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, + // avoiding abrupt segment-length jumps at adaptive-depth boundaries. + static constexpr size_t max_depth = 16; + + std::vector subcurves(2); + subdivide_bezier(curve, subcurves[0], subcurves[1]); + + for (size_t depth = 1; depth < max_depth; ++depth) { + bool all_flat = true; + for (const QuinticBezier &c : subcurves) + if (!is_bezier_flat(c, incoming, outgoing, deviation)) { + all_flat = false; + break; + } + if (all_flat) + break; + std::vector finer(subcurves.size() * 2); + for (size_t i = 0; i < subcurves.size(); ++i) + subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); + subcurves = std::move(finer); + } + + // The curve start is deliberately omitted so it can be shared with the straight leg feeding into it. + output.clear(); + output.reserve(subcurves.size()); + for (const QuinticBezier &c : subcurves) + output.emplace_back(c.back()); +} + +const std::vector& CornerSmoother::curve_coefficients( + const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing) +{ + const double cosine = incoming.dot(outgoing); + // Corners of the same size and turn angle are congruent, so they flatten identically. An infill + // path walks over the very same corner over and over again, the Hilbert curve over a single one. + if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine) + return m_cached_coefficients; + + // One canonical corner running from -corner_distance along the incoming leg to corner_distance + // along the outgoing one. At each end, the first three control points are collinear and equally + // spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The + // endpoint curvature is therefore zero, giving G2 joins to both legs. + const double d = corner_distance; + const QuinticBezier corner_curve {{ + {-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d} + }}; + // Retain a finite positive tolerance if the smoother was set up with an invalid one. + const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON; + flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients); + + m_cached_distance = corner_distance; + m_cached_cosine = cosine; + m_has_cached_coefficients = true; + return m_cached_coefficients; +} + +void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next) +{ + m_corner_points.clear(); + + const Vec2d incoming_leg = corner - previous; + const Vec2d outgoing_leg = next - corner; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + if (incoming_length < EPSILON || outgoing_length < EPSILON) { + m_corner_points.emplace_back(corner); + return; + } + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); + // A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above. + if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) { + m_corner_points.emplace_back(corner); + return; + } + + // Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart. + double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length); + if (m_max_corner_distance > 0.) + corner_distance = std::min(corner_distance, m_max_corner_distance); + + const Vec2d curve_start = corner - corner_distance * incoming; + const Vec2d curve_end = corner + corner_distance * outgoing; + if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) { + m_corner_points.emplace_back(corner); + return; + } + + const std::vector &coefficients = curve_coefficients(corner_distance, incoming, outgoing); + m_corner_points.reserve(coefficients.size() + 1); + m_corner_points.emplace_back(curve_start); + for (const Vec2d &coefficient : coefficients) + m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing); +} + +// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices +// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a +// path returning to where it started retraces its way back and is not a loop. +static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother) +{ + // A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes + // feeding the smoother the last vertex first, whose own output point is then dropped again. + size_t skip = polygon ? 1 : 0; + + Points smoothed; + smoothed.reserve(2 * points.size()); + auto emit = [&smoothed, &skip](const Vec2d &point) { + if (skip > 0) { + --skip; + return; + } + smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5))); + }; + + if (polygon) + smoother.push(points.back().cast(), emit); + for (const Point &point : points) + smoother.push(point.cast(), emit); + if (polygon) + // Wrap the first vertex around, so that the last one is a corner as well. + smoother.push(points.front().cast(), emit); + smoother.flush(emit); + + if (polygon) + // The flushed point is the wrapped first vertex, which a polygon does not store. + smoothed.pop_back(); + return smoothed; +} + +void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled() || polyline.size() < 3) + return; + + polyline.points = smooth_corners(polyline.points, false, smoother); + // Rounding back to the integer grid may collapse neighbouring samples of a curve. + polyline.remove_duplicate_points(); +} + +void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + if (sanitize_smooth_factor(smooth_factor) == 0.) + return; + for (Polyline &polyline : polylines) + smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter); +} + +void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled()) + return; + + for (Polygon &polygon : polygons) { + if (polygon.size() < 3) + continue; + polygon.points = smooth_corners(polygon.points, true, smoother); + polygon.remove_duplicate_points(); + // The curves of the first and of the last corner may have met on the segment they share. A + // polygon closes implicitly, so it must not repeat its first vertex at the end. + if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back()) + polygon.points.pop_back(); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp new file mode 100644 index 0000000000..1852fc4c67 --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Polyline.hpp" + +namespace Slic3r { + +// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>. +inline double sanitize_smooth_factor(double smooth_factor) +{ + return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.; +} + +// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it +// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the +// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it. +using CornerFilter = std::function; + +// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight +// legs with a continuous curvature, so the toolhead does not have to stop in every corner. +// Points are pushed one by one, because the plane path fills produce their path on the fly, and +// every point of the smoothed path is handed over to the caller supplied emit callback. +// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted +// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such +// duplicates is left to the caller, which is the only one knowing that grid. +class CornerSmoother +{ +public: + // tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed + // points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it + // bounds how far a rounded corner moves away from the original path, which matters where the legs + // are much longer than the spacing of the pattern. Zero leaves the reach uncapped. + CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0., + CornerFilter corner_filter = {}) + : m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance), + m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter)) + {} + + bool enabled() const { return m_corner_distance_ratio > 0.; } + + template void push(const Vec2d &point, Emit &emit) + { + if (m_pending == 0) { + emit(point); + m_previous = point; + } else if (m_pending > 1) { + round_corner(m_previous, m_corner, point); + for (const Vec2d &corner_point : m_corner_points) + emit(corner_point); + m_previous = m_corner; + } + m_corner = point; + m_pending = std::min(m_pending + 1, 2); + } + + // Emits the last point of the path and prepares the smoother for a new one. + template void flush(Emit &emit) + { + if (m_pending > 1) + emit(m_corner); + m_pending = 0; + } + +private: + // Fills m_corner_points with the points replacing the corner vertex. + void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next); + // Flattens the canonical corner curve of the given size and turn into coordinates of the + // (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner. + const std::vector& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing); + + // Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment + // is the maximum, otherwise the curves of two adjacent corners would overlap. + const double m_corner_distance_ratio; + const double m_tolerance; + const double m_max_corner_distance; + const CornerFilter m_corner_filter; + std::vector m_corner_points; + // Cached flattening of the last corner, valid for corners of the same size and turn angle. + std::vector m_cached_coefficients; + double m_cached_distance { 0. }; + double m_cached_cosine { 0. }; + bool m_has_cached_coefficients { false }; + + Vec2d m_previous { Vec2d::Zero() }; + Vec2d m_corner { Vec2d::Zero() }; + // Number of points held back: none, the first point of a path, or a corner candidate. + int m_pending { 0 }; +}; + +// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone. +// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its +// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance. +void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +// Polygons close implicitly, so every one of their vertices is a corner. +void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCrossHatch.cpp b/src/libslic3r/Fill/FillCrossHatch.cpp index 571095eca4..98be5ef46b 100644 --- a/src/libslic3r/Fill/FillCrossHatch.cpp +++ b/src/libslic3r/Fill/FillCrossHatch.cpp @@ -3,6 +3,7 @@ #include "../Surface.hpp" #include #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillCrossHatch.hpp" namespace Slic3r { @@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single( // shift the pattern to the actual space for (Polyline &pl : polylines) { pl.translate(bb.min); } + // Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillHoneycomb.cpp b/src/libslic3r/Fill/FillHoneycomb.cpp index a595cdb664..82679541da 100644 --- a/src/libslic3r/Fill/FillHoneycomb.cpp +++ b/src/libslic3r/Fill/FillHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillHoneycomb.hpp" namespace Slic3r { @@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single( } p.rotate(-direction.first, m.hex_center); p.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the honeycomb cells. Done before the clipping, so that the + // curves are cut by the region boundary just like the sharp path would be. + smooth_polyline_corners(p, params.smooth_factor, scaled(params.resolution)); all_polylines.push_back(p); } } diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 7937b9d129..77031b42e0 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -2,6 +2,7 @@ #include "../Print.hpp" #include "../ShortestPath.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillLightning.hpp" #include "Lightning/Generator.hpp" @@ -17,6 +18,19 @@ void Filler::_fill_surface_single( const Layer &layer = generator->getTreesForLayer(this->layer_id); Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled(0.5 * this->spacing - this->overlap)); + // Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and + // the reach is capped: cutting a corner moves the branch, and a branch is as long as the object + // rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour + // instead of rounding the turn between them. Half the distance between two branches keeps them + // apart. With more than one line per infill wall the branches are printed as outlines drawn around + // them, and the outlines of branches that run into each other merge into a single one; moving a + // branch by more than a fraction of its printed width breaks such an outline up into separate + // loops, so that width bounds the reach as well. + const double branch_width = scaled(this->spacing) * params.multiline; + const double branch_spacing = branch_width / std::max(double(params.density), EPSILON); + const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing); + smooth_polylines_corners(fill_lines, params.smooth_factor, scaled(params.resolution), max_reach); + // Apply multiline offset if needed multiline_fill(fill_lines, params, spacing); diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index 7c4f285ac6..577aef0600 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillPlanePath.hpp" namespace Slic3r { @@ -288,145 +289,60 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x, } } -using QuinticBezier = std::array; - -static bool is_bezier_flat(const QuinticBezier &curve, const double deviation) -{ - // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every - // control point within a deviation-wide strip around the endpoint chord conservatively bounds the - // flattening error. The cross product is the perpendicular distance scaled by the chord length; - // comparing squared values avoids a square root. - const Vec2d chord = curve.back() - curve.front(); - const double chord_length_sq = chord.squaredNorm(); - const double max_cross_sq = deviation * deviation * chord_length_sq; - - for (size_t i = 1; i + 1 < curve.size(); ++i) { - const Vec2d offset = curve[i] - curve.front(); - const double cross = chord.x() * offset.y() - chord.y() * offset.x(); - if (cross * cross > max_cross_sq) - return false; - } - return true; -} - -static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) -{ - // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one - // control point to the left half and one to the right half; the latter is filled backwards to keep - // both resulting control polygons in their original parameter direction. - QuinticBezier subdivision = curve; - left.front() = subdivision.front(); - right.back() = subdivision.back(); - for (size_t level = 1; level < curve.size(); ++level) { - for (size_t i = 0; i + level < curve.size(); ++i) - subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); - left[level] = subdivision.front(); - right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; - } -} - -static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector &output) -{ - // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. - // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, - // avoiding abrupt segment-length jumps at adaptive-depth boundaries. - static constexpr size_t max_depth = 16; - - std::vector subcurves(2); - subdivide_bezier(curve, subcurves[0], subcurves[1]); - - for (size_t depth = 1; depth < max_depth; ++depth) { - bool all_flat = true; - for (const QuinticBezier &c : subcurves) - if (!is_bezier_flat(c, deviation)) { - all_flat = false; - break; - } - if (all_flat) - break; - std::vector finer(subcurves.size() * 2); - for (size_t i = 0; i < subcurves.size(); ++i) - subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); - subcurves = std::move(finer); - } - - // The curve start is deliberately omitted so consecutive curve pieces can share it without duplication. - output.reserve(output.size() + subcurves.size()); - for (const QuinticBezier &c : subcurves) - output.emplace_back(c.back()); -} - +// Rounds the corners of the generated path on its way to the infill output. template -static void generate_smooth_hilbert_curve( - coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, - const double corner_distance, Output &output) +class SmoothingPolylineOutput { - // A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed - // generator, expand the larger requested dimension to the next valid Hilbert grid size. The output - // clipper or the later region intersection removes the padded part of the traversal. - size_t sz = 2; - const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y); - while (sz < sz0) - sz <<= 1; +public: + SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance) + : m_output(output), m_smoother(smooth_factor, tolerance) {} - const size_t point_count = sz * sz; - output.reserve(point_count); + void reserve(size_t n) { m_output.reserve(n); } + void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); } + // The smoother holds back the last point of the path until it knows there is no corner left to round. + void finish() { auto emit = emitter(); m_smoother.flush(emit); } - // The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance - // if this helper is invoked with an invalid resolution. - const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON; - // Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance). - // At each end, the first three control points are collinear and equally spaced: the tangent follows - // the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore - // zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten - // it only once to the requested chordal-deviation tolerance. - const QuinticBezier corner_curve {{ - {-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.}, - {0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance} - }}; - std::vector curve_coefficients; - flatten_bezier(corner_curve, deviation, curve_coefficients); - - auto translated_point = [min_x, min_y](size_t idx) { - Point p = hilbert_n_to_xy(idx); - return Point(p.x() + min_x, p.y() + min_y); - }; - auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); }; - bool has_last_output = false; - Vec2d last_output; - // Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates - // to avoid emitting zero-length extrusion segments. - auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) { - if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) { - output.add_point(point); - last_output = point; - has_last_output = true; - } - }; - - Vec2d previous = to_vec2d(translated_point(0)); - Vec2d corner = to_vec2d(translated_point(1)); - add_point(previous); - // Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of - // its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline. - for (size_t i = 1; i + 1 < point_count; ++i) { - const Vec2d next = to_vec2d(translated_point(i + 1)); - const Vec2d incoming = (corner - previous).normalized(); - const Vec2d outgoing = (next - corner).normalized(); - const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); - - if (std::abs(cross) < EPSILON) { - add_point(corner); - } else { - add_point(corner - corner_distance * incoming); - for (const Vec2d &coefficient : curve_coefficients) - add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing); - } - - previous = corner; - corner = next; +private: + // The curves of two adjacent corners meet at the midpoint of the segment they share, where they + // may round to the very same output point. Drop those, they would be zero length extrusions. + auto emitter() + { + return [this](const Vec2d &pt) { + const Point snapped = m_output.scaled(pt); + if (m_has_last_snapped && snapped == m_last_snapped) + return; + m_last_snapped = snapped; + m_has_last_snapped = true; + m_output.add_point(pt); + }; } - add_point(corner); + + Output &m_output; + CornerSmoother m_smoother; + Point m_last_snapped { Point::Zero() }; + bool m_has_last_snapped { false }; +}; + +// Runs the path generator against the concrete output type, optionally through the corner smoother. +// The outputs do not share a virtual add_point(), so the type has to be resolved here. +template +static void generate_path(InfillPolylineOutput &output, const FillParams ¶ms, const double resolution, GenerateFn generate) +{ + const double smooth_factor = sanitize_smooth_factor(params.smooth_factor); + auto run = [smooth_factor, resolution, &generate](auto &out) { + if (smooth_factor == 0.) { + generate(out); + } else { + SmoothingPolylineOutput> smoothing(out, smooth_factor, resolution); + generate(smoothing); + smoothing.finish(); + } + }; + + if (output.clips()) + run(static_cast(output)); + else + run(output); } void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output) @@ -440,19 +356,8 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, const FillParams ¶ms, InfillPolylineOutput &output) { - const double smooth_factor = std::isfinite(params.smooth_factor) ? - std::clamp(params.smooth_factor, 0., 1.) : 0.; - if (smooth_factor == 0.) { - this->generate(min_x, min_y, max_x, max_y, resolution, output); - return; - } - - const double corner_distance = 0.5 * smooth_factor; - if (output.clips()) - generate_smooth_hilbert_curve( - min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast(output)); - else - generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output); + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); }); } template @@ -495,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c generate_octagram_spiral(min_x, min_y, max_x, max_y, output); } +void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) +{ + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); }); +} + } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillPlanePath.hpp b/src/libslic3r/Fill/FillPlanePath.hpp index b4b25b73ae..a1e9068ca9 100644 --- a/src/libslic3r/Fill/FillPlanePath.hpp +++ b/src/libslic3r/Fill/FillPlanePath.hpp @@ -21,10 +21,10 @@ public: void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); } Points&& result() { return std::move(m_out); } virtual bool clips() const { return false; } - -protected: + // The output grid the generated points are snapped to. const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; } +protected: // Output polyline. Points m_out; @@ -93,6 +93,8 @@ public: protected: bool centered() const override { return true; } void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override; + void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) override; }; } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 8b40b8753c..0c82354b38 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -18,6 +18,7 @@ #include "../ShortestPath.hpp" #include "../VariableWidth.hpp" +#include "FillCornerSmoothing.hpp" #include "FillRectilinear.hpp" // #define SLIC3R_DEBUG @@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal( for (Polyline &pl : polylines) pl.translate(rotate_vector.second); + // Orca: round the corners of the trapezoids. The straight base lines of the triangular family + // have no corner to round. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline fill multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f9d895332a..8083da954e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3469,9 +3469,8 @@ void PrintConfigDef::init_fff_params() def = this->add("sparse_infill_smooth_factor", coPercent); def->label = L("Sparse infill smooth factor"); def->category = L("Strength"); - def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, " - "while 100% produces the largest possible curves between adjacent infill lines. " - "Currently applies only to the Hilbert Curve."); + def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, " + "while 100% produces the largest possible curves between adjacent infill lines."); def->sidetext = "%"; def->min = 0; def->max = 100; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f51c1c6411..26a708b78d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern) } } +// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. +// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more +// than one line per infill wall; a single line makes them plain crossing lines with nothing to round. +inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1) +{ + switch (pattern) { + case ipHilbertCurve: + case ipOctagramSpiral: + case ipLightning: + case ipHoneycomb: + case ip3DHoneycomb: + case ipConcentric: + case ipCrossHatch: + return true; + case ipGrid: + case ipTriangles: + case ipStars: + return multiline > 1; + default: + return false; + } +} + enum class IroningType { NoIroning, TopSurfaces, diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 3885a391b8..de94bb6b4b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -752,7 +752,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_top_shell = has_top_shell_layers && config->option("top_surface_density")->value > 0; bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0; bool has_solid_infill = has_top_shell_layers || has_bottom_shell; - toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve); + toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline"))); toggle_field("top_surface_pattern", has_top_shell); toggle_field("bottom_surface_pattern", has_bottom_shell); toggle_field("top_surface_density", has_top_shell_layers); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 5fbce5a342..07460d3990 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -698,3 +698,290 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set", CHECK(delta == 30); } } + +TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]") +{ + // A cell whose sides are several times the line width, so that the corners have room to be rounded. + const double spacing = 0.45; + const double density = 0.1; + auto fill = [spacing, density](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("honeycomb")); + filler->spacing = spacing; + + FillParams params; + params.density = float(density); + params.dont_adjust = true; + // Keep the fragments apart, so that only the turns of the pattern itself are measured. + params.anchor_length_max = 0.f; + params.smooth_factor = smooth_factor; + + Slic3r::ExPolygon square{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } }; + Slic3r::Surface surface(stInternal, square); + return filler->fill_surface(&surface, params); + }; + + // Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all. + auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) { + double sharpest = 1.; + for (const Polyline &polyline : polylines) + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + + REQUIRE(!sharp.empty()); + REQUIRE(smooth.size() == sharp.size()); + REQUIRE(point_count(smooth) > point_count(sharp)); + // The cell corners turn by 60 degrees; smoothing replaces them by gentle curves. + REQUIRE(sharpest_turn_cosine(sharp) < 0.6); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); +} + +// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print. +// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones. +struct SparseInfillShape { + size_t point_count { 0 }; + size_t sharp_turns { 0 }; + size_t path_count { 0 }; + double length { 0. }; +}; + +static SparseInfillShape sparse_infill_shape(const Print &print) +{ + SparseInfillShape shape; + + auto account = [&shape](const ExtrusionPath &path) { + if (!sparse_role(path.role())) + return; + const Points3 &pts = path.polyline.points; + ++shape.path_count; + shape.point_count += pts.size(); + for (size_t i = 1; i < pts.size(); ++i) + shape.length += (pts[i] - pts[i - 1]).head<2>().cast().norm(); + for (size_t i = 1; i + 1 < pts.size(); ++i) { + const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast(); + const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast(); + if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. && + incoming.normalized().dot(outgoing.normalized()) < 0.9) + ++shape.sharp_turns; + } + }; + + for (const Layer *layer : print.objects().front()->layers()) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (auto *path = dynamic_cast(entity)) + account(*path); + else if (auto *multi = dynamic_cast(entity)) + for (const ExtrusionPath &p : multi->paths) + account(p); + else if (auto *loop = dynamic_cast(entity)) + for (const ExtrusionPath &p : loop->paths) + account(p); + } + return shape; +} + +TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + // The branch turns are replaced by curves, which cut the corners off and take more points to + // describe. The turns where two branches are joined into one path stay sharp. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "concentric"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "crosshatch"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]") +{ + auto shape_for = [](int multiline, const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "grid"}, + {"sparse_infill_density", "20%"}, + {"fill_multiline", multiline}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for(2, "0%"); + const SparseInfillShape smooth = shape_for(2, "100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); + + // A single line per infill wall is the plain crossing line grid, which has no corner of its own. + const SparseInfillShape single_sharp = shape_for(1, "0%"); + const SparseInfillShape single_smooth = shape_for(1, "100%"); + REQUIRE(single_sharp.point_count > 0); + REQUIRE(single_smooth.point_count == single_sharp.point_count); + REQUIRE(single_smooth.length == single_sharp.length); +} + +TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "3dhoneycomb"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]") +{ + // The concentric loops are offsets of the fill region and are never clipped to it, so a corner + // rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of + // the turn, which leaves the region at every corner of a hole, and in a region thinner than the + // curve even at a corner turning inwards. + const bool thin_region = GENERATE(false, true); + ExPolygon region; + if (thin_region) { + // An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them. + region = ExPolygon{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2), + Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } }; + } else { + region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.), + Point::new_scale(50., 50.), Point::new_scale(0., 50.) }, + Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.), + Point::new_scale(20., 30.), Point::new_scale(20., 20.) } }; + } + CAPTURE(thin_region); + + auto fill = [®ion](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("concentric")); + filler->spacing = 0.45; + + FillParams params; + params.density = 0.1f; + params.dont_adjust = true; + params.smooth_factor = smooth_factor; + + Slic3r::Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + REQUIRE(!sharp.empty()); + + // Nothing leaves the fill region, which the unrounded loops already touch from the inside. + const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON)); + REQUIRE(diff_pl(sharp, bounds).empty()); + REQUIRE(diff_pl(smooth, bounds).empty()); + // The corners that the region has room for are still rounded. + if (!thin_region) + REQUIRE(point_count(smooth) > point_count(sharp)); +} + +TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]") +{ + // With more than one line per infill wall, the branches are printed as outlines drawn around them, + // and the outlines of branches that run close to each other merge into one. Rounding the branches + // before those outlines are built moves them apart, which breaks the merged outlines up into + // separate loops - many more of them, each needing its own travel move. + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"fill_multiline", 2}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.path_count > 0); + REQUIRE(smooth.path_count <= sharp.path_count); + // The outlines are still rounded. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 1ad299473c..bc10bb4f73 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests test_preset_setting_id.cpp test_preset_diff.cpp test_elephant_foot_compensation.cpp + test_fill_corner_smoothing.cpp test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp new file mode 100644 index 0000000000..f2c25e816d --- /dev/null +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include + +#include "libslic3r/Fill/FillCornerSmoothing.hpp" +#include "libslic3r/Polyline.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; + +namespace { + +// A right angle turn, with the outgoing leg ten times longer than the incoming one. +Polyline asymmetric_corner() +{ + return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) }; +} + +double max_turn_cosine(const Polyline &polyline) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + +bool contains(const Polyline &polyline, const Point &point) +{ + return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end(); +} + +const double tolerance = scaled(0.0125); + +} // namespace + +TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + Polyline smooth = sharp; + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The right angle is gone, every remaining turn is a gentle one. + REQUIRE(max_turn_cosine(sharp) < 0.1); + REQUIRE(max_turn_cosine(smooth) > 0.9); + REQUIRE(smooth.length() < sharp.length()); +} + +TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + + Polyline none = sharp; + smooth_polyline_corners(none, 0., tolerance); + REQUIRE(none.points == sharp.points); + + Polyline invalid = sharp; + smooth_polyline_corners(invalid, std::numeric_limits::quiet_NaN(), tolerance); + REQUIRE(invalid.points == sharp.points); +} + +TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]") +{ + // The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of + // two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is + // left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long. + Polyline smooth = asymmetric_corner(); + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(contains(smooth, Point::new_scale(5., 0.))); + REQUIRE(contains(smooth, Point::new_scale(10., 5.))); + // A Bezier curve stays within the convex hull of its control points, so the rounded path stays + // inside the box spanned by the two legs. + for (const Point &point : smooth.points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 100.).y()); + } +} + +TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]") +{ + Polyline half = asymmetric_corner(); + smooth_polyline_corners(half, 0.5, tolerance); + Polyline full = asymmetric_corner(); + smooth_polyline_corners(full, 1., tolerance); + + // Half of the factor leaves the 10mm leg half as far from the corner. + REQUIRE(contains(half, Point::new_scale(7.5, 0.))); + REQUIRE(contains(full, Point::new_scale(5., 0.))); + // A larger factor rounds a wider portion of the legs, cutting more of the corner off. + REQUIRE(full.length() < half.length()); +} + +TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]") +{ + // Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin + // into a degenerate loop instead of a tip. + Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) }; + const Polyline sharp = hairpin; + smooth_polyline_corners(hairpin, 1., tolerance); + REQUIRE(hairpin == sharp); +} + +TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]") +{ + Polyline coarse = asymmetric_corner(); + smooth_polyline_corners(coarse, 1., scaled(0.2)); + Polyline fine = asymmetric_corner(); + smooth_polyline_corners(fine, 1., scaled(0.001)); + + REQUIRE(fine.size() > coarse.size()); + REQUIRE(fine.front() == coarse.front()); + REQUIRE(fine.back() == coarse.back()); +} + +TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]") +{ + // Fully smoothed adjacent corners meet at the midpoint of the segment they share. + Polyline zigzag; + for (int i = 0; i < 8; ++i) + zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.)); + smooth_polyline_corners(zigzag, 1., tolerance); + + for (size_t i = 1; i < zigzag.size(); ++i) + REQUIRE((zigzag[i] - zigzag[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]") +{ + // A polygon closes implicitly, so none of its corners may stay sharp, not even the first one. + const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(0., 10.) }; + Polygons smooth{ square }; + smooth_polygons_corners(smooth, 1., tolerance); + const Polyline rounded = smooth.front().split_at_first_point(); + + REQUIRE(smooth.front().size() > square.size()); + REQUIRE(max_turn_cosine(rounded) > 0.9); + // The turn from the closing segment back into the first one must be gentle as well. + const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast().normalized(); + const Vec2d outgoing = (rounded[1] - rounded[0]).cast().normalized(); + REQUIRE(incoming.dot(outgoing) > 0.9); + // None of the corners is cut by more than half of a 10mm side. + for (const Point &point : smooth.front().points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 10.).y()); + } +} + +TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]") +{ + // A branch of a lightning tree walks out and retraces its way back, ending where it started. Its + // ends are two free ends that happen to coincide, and joining them would close it into a loop. + Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(5., 10.), Point::new_scale(0., 0.) }; + const Polyline sharp = retrace; + smooth_polyline_corners(retrace, 1., tolerance); + + REQUIRE(retrace.size() > sharp.size()); + REQUIRE(retrace.front() == sharp.front()); + REQUIRE(retrace.back() == sharp.back()); +} diff --git a/tests/libslic3r/test_fill_plane_path.cpp b/tests/libslic3r/test_fill_plane_path.cpp index bbb75dce58..7fc7f4a6b0 100644 --- a/tests/libslic3r/test_fill_plane_path.cpp +++ b/tests/libslic3r/test_fill_plane_path.cpp @@ -27,6 +27,31 @@ public: } }; +class TestableOctagramSpiral : public FillOctagramSpiral +{ +public: + Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7) + { + InfillPolylineOutput output(output_scale); + FillParams params; + params.smooth_factor = smooth_factor; + FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output); + return std::move(output.result()); + } +}; + +// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all. +double sharpest_turn_cosine(const Points &points) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < points.size(); ++i) { + const Vec2d incoming = (points[i] - points[i - 1]).cast().normalized(); + const Vec2d outgoing = (points[i + 1] - points[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + double path_length(const Points &points) { double length = 0.; @@ -146,6 +171,35 @@ TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature); } +TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones. + REQUIRE(sharpest_turn_cosine(sharp) < -0.7); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); + + for (size_t i = 1; i < smooth.size(); ++i) + REQUIRE((smooth[i] - smooth[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5); + const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + const Points invalid_factor = TestableOctagramSpiral().generate_points( + 0.005, std::numeric_limits::quiet_NaN()); + + REQUIRE(path_length(full_smooth) < path_length(half_smooth)); + REQUIRE(path_length(half_smooth) < path_length(sharp)); + REQUIRE(invalid_factor == sharp); +} + TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]") { const Points sharp = TestableHilbertCurve().generate_points(0.005); From 4fd7fdb3faa24bdb903d5c89bb5c83801c63345c Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 18 Aug 2026 12:25:59 -0300 Subject: [PATCH 3/3] Move smooth factor wiki link (#15287) --- src/slic3r/GUI/Tab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a31355d0e..0ea685a02c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2790,7 +2790,7 @@ void TabPrint::build() optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); - optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor"); + optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");