Compare commits

..
Author SHA1 Message Date
Rodrigo Faselli 13d4a0d922 Avoid center plug for internal solid infill (spiral inset) (#15705)
avoid center plug for internal solid infill
2026-10-06 11:41:25 -03:00
Ian Bassi c74ac8a6c3 Keep FPS/timings overlay below the toolbar on narrow canvases (#16215) 2026-10-06 11:36:47 -03:00
Ian Bassi 2d90345d1e Move fill-pattern-tops wiki under 3D Honeycomb (#16214) 2026-10-06 10:34:28 -03:00
Ioannis Giannakas 5a8eb3f14e Fix one wall on top dropping inner walls of narrow Arachne walls (#16174)
* Fix one wall on top dropping inner walls of narrow Arachne walls

* Fix clang tidy errors on the test suite
2026-10-06 14:07:01 +01:00
Kris Austin e7d6f5e6c0 fix: crash in LAN mode when the printer type is not known yet (#16191)
* fix: crash in LAN mode when the printer type is not known yet

InputIpAddressDialog::set_machine_obj() built the help image name from
the printer config with no fallback. If the printer type is empty or
unknown, for example before the first push_all arrives on a flaky LAN
link, the lookup returns "" and create_scaled_bitmap("_en") throws.
The dialog is opened by the "LAN Connection Failed" handlers in
MediaPlayCtrl and MediaFilePanel, where nothing catches the exception,
so the app crashes.

Use input_access_code_x1 when there is no image, and the _cn image for
zh_CN, the same as ConnectPrinterDialog::init_bitmap().

Ported from Bambu Studio 52ca2ec5d1.

* fix: return an empty bitmap for an empty icon name

create_scaled_bitmap() threw when a caller passed an empty name. That
happens when a printer config lookup has no entry, for example in
AMSSetting::update_ams_img() for a printer type with no AMS image.
Log an error and return wxNullBitmap instead.

Ported from Bambu Studio 52ca2ec5d1.
2026-10-06 09:55:25 -03:00
Kris Austin e098c933f0 perf: write post-processed G-code without per-line copies to speed up export by up to 6% (#16167)
* perf: write post-processed G-code without a per-line copy

* perf: size the post-process line map from the first pass

* test: line ends of the exported G-code

* test: include the headers the line-ends test and gcode() helper use
2026-10-06 07:50:08 -03:00
HanifKoh d1afb1fed6 Use a System clang-tidy When Available and Make --fix Converge in One Pass (#16199)
* Use a System clang-tidy When Available and Make --fix Converge in One Pass

scripts/run_clang_tidy.sh only looked at CLANG_TIDY and the venv it creates,
so a clang-tidy already on the system was never used. It is now the first
choice: the pinned version outright, another version after a prompt that
says results may differ slightly from CI, which -y and an existing pinned
venv skip.

Two problems in clang_tidy_diff.py made --fix need several runs and still
leave the plain check failing:

- A deleted #include orphans uses on unchanged lines. The plain check runs
  such a file whole and reports them, but --fix kept the line filter to the
  changed lines, so they were never fixed. Fix mode now runs the file whole
  first and then fixes exactly the changed lines plus the lines that run
  found wanting, so unrelated lines are still never rewritten.

- clang-tidy exits non-zero for the findings it just fixed, so every fixed
  file was reported as failed and the user ran --fix again to see what was
  left. A file --fix changed is now checked again and the fixed files are
  listed separately from what --fix could not add.

CI runs the script without --fix and is unchanged.

* Keep the a/ b/ Diff Prefixes Whatever the User's Git Config Says

parse_diff recognises a changed file by its +++ b/ header. With
diff.noprefix or diff.mnemonicPrefix set, git prints +++ src/x.cpp or
+++ w/src/x.cpp instead, every file was dropped, and the local check
reported no changed C++ lines. The diff is now asked for the a/ and b/
prefixes outright, which overrides both settings.

* Warn When No Remote Points at OrcaSlicer/OrcaSlicer

Without one, run_clang_tidy.sh compares against origin/main. When origin
is a fork whose main already holds the commits, the check finds nothing
and says so, without hinting at why. The script now names the base it
fell back to and how to point it at the upstream repository.
2026-10-06 17:16:09 +08:00
17 changed files with 555 additions and 173 deletions
+45 -14
View File
@@ -8,6 +8,9 @@ include) fails wherever the error is. Headers are compiled on their own, and
fail only on errors in their changed lines. Deleting an #include also fails
on every use, changed or not, that now lacks the header it provided.
With -- --fix, clang-tidy adds the missing includes, on those lines only, and
each file it changed is checked again so that only what remains is reported.
The checks come from .clang-tidy at the repository root. The compile database
must come from a configure with SLIC3R_PCH=OFF, or the precompiled header hides
missing includes.
@@ -102,9 +105,10 @@ def is_checked(path):
def changed_files(merge_base):
# Against the working tree, so a local run covers uncommitted edits too.
# core.quotePath=false keeps a non-ASCII path unquoted, so parse_diff sees its b/ prefix.
# core.quotePath=false keeps a non-ASCII path unquoted, and the explicit prefixes
# override diff.noprefix and diff.mnemonicPrefix, so parse_diff sees its b/ prefix.
diff = subprocess.run(["git", "-c", "core.quotePath=false", "diff", "-U0", "--no-color", "--no-ext-diff",
"--diff-filter=AMR", merge_base],
"--src-prefix=a/", "--dst-prefix=b/", "--diff-filter=AMR", merge_base],
check=True, capture_output=True, **UTF8).stdout
return {path: change for path, change in parse_diff(diff).items() if is_checked(path)}
@@ -201,13 +205,19 @@ def errors_alone_at(revision, clang_tidy, build_dir, path):
def check_file(clang_tidy, build_dir, merge_base, path, change, extra_args):
"""Run clang-tidy on one file and return (failed, output, failing diagnostics)."""
"""Run clang-tidy on one file and return (failed, output, failing diagnostics, fixed)."""
fixing = any(arg.startswith("--fix") for arg in extra_args)
if fixing:
with open(path, "rb") as f:
before = f.read()
# A deleted include can orphan uses on unchanged lines, so such a file is
# checked whole and the findings narrowed here. --fix keeps the line filter
# so it never rewrites unrelated code.
whole = bool(change.removed_includes) and not extra_args
# checked whole and the findings narrowed here. --fix keeps a line filter so
# it never rewrites unrelated code, which for such a file means a second,
# fixing run limited to the lines the first one found wanting.
whole = bool(change.removed_includes)
returncode, output, diagnostics = run_clang_tidy(clang_tidy, build_dir, path,
None if whole else change.lines, extra_args)
None if whole else change.lines,
[] if whole else extra_args)
real = os.path.realpath(path)
def introduced(d):
@@ -223,14 +233,25 @@ def check_file(clang_tidy, build_dir, merge_base, path, change, extra_args):
if errors and change.removed_includes:
with open(path, encoding="utf-8") as f:
text = f.read()
before = errors_alone_at(merge_base, clang_tidy, build_dir, path)
before_sites = errors_alone_at(merge_base, clang_tidy, build_dir, path)
failing += [d for d in errors if d not in failing
and (error_sites([d], text) - before)]
return bool(failing), output, failing
if whole:
and (error_sites([d], text) - before_sites)]
failed = bool(failing)
elif whole:
failing = [d for d in diagnostics if d.is_compile_error or introduced(d)]
return bool(failing), output, failing
return returncode != 0, output, diagnostics
failed = bool(failing)
else:
failing, failed = diagnostics, returncode != 0
if not fixing:
return failed, output, failing, False
if whole and failing:
lines = change.lines + [[d.line, d.line] for d in failing if os.path.realpath(d.file) == real]
run_clang_tidy(clang_tidy, build_dir, path, lines, extra_args)
with open(path, "rb") as f:
if f.read() == before:
return failed, output, failing, False
# Checked again, so what is reported is what the fixes left.
return check_file(clang_tidy, build_dir, merge_base, path, change, [])[:3] + (True,)
def main():
@@ -263,11 +284,14 @@ def main():
annotate = os.environ.get("GITHUB_ACTIONS") == "true"
root = os.getcwd() + os.sep
failed = []
fixed = []
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
jobs = {path: pool.submit(check_file, args.clang_tidy, args.build_dir, merge_base, path, change, args.extra_args)
for path, change in todo}
for path, job in jobs.items():
file_failed, output, diagnostics = job.result()
file_failed, output, diagnostics, file_fixed = job.result()
if file_fixed:
fixed.append(path)
if not file_failed:
continue
failed.append(path)
@@ -282,6 +306,13 @@ def main():
if len(diagnostics) > MAX_REPORTED:
print(f"... and {len(diagnostics) - MAX_REPORTED} more")
if fixed:
print(f"\nAdded includes to {len(fixed)} file(s):")
for path in fixed:
print(f" {path}")
if failed and fixed:
print(f"\nclang-tidy still fails on {len(failed)} file(s); the findings above are what --fix could not add.")
return 1
if failed:
print(f"\nclang-tidy failed on {len(failed)} file(s). Add the includes it names, or apply its "
"suggestions locally with scripts/run_clang_tidy.sh --fix (scripts\\run_clang_tidy.ps1 -Fix on Windows).")
-5
View File
@@ -4,10 +4,5 @@
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- The Bambu network plug-in's code protector rewrites one of its own signed code pages
after loading. Under the hardened runtime macOS kills the process when that page is
paged back in; this lets it run, as Bambu Studio's signature does. -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
+40 -10
View File
@@ -6,8 +6,9 @@
# scripts/run_clang_tidy.sh --fix also add the missing includes it names
#
# It configures a separate build directory (build-tidy) without the precompiled
# header, installs the pinned clang-tidy into a virtual environment inside it, and
# runs scripts/clang_tidy_diff.py the way CI does. Uncommitted changes are checked too.
# header, uses the clang-tidy on your system or installs the pinned one into a
# virtual environment inside it, and runs scripts/clang_tidy_diff.py the way CI
# does. Uncommitted changes are checked too.
set -euo pipefail
@@ -23,7 +24,8 @@ Usage: scripts/run_clang_tidy.sh [options]
deps/build/<arch> on macOS)
-j, --jobs N parallel clang-tidy runs (default: all cores)
--fix apply clang-tidy's fixes (adds the missing includes)
-y, --yes install missing tools without asking
-y, --yes install missing tools without asking; another clang-tidy
version found on the system is then not offered
-h, --help show this help
EOF
}
@@ -136,12 +138,38 @@ REQUIREMENTS="$ROOT/scripts/clang_tidy_requirements.txt"
PINNED=$(sed -n 's/^clang-tidy==//p' "$REQUIREMENTS")
VENV="$BUILD_DIR/clang-tidy-venv"
if [ -n "${CLANG_TIDY:-}" ]; then
is_pinned() {
[ -x "$1" ] && "$1" --version 2>/dev/null | grep -q "version $PINNED"
}
CLANG_TIDY="${CLANG_TIDY:-}"
if [ -n "$CLANG_TIDY" ]; then
# Set by the caller: use it as is.
:
is_pinned "$CLANG_TIDY" || echo "Warning: $CLANG_TIDY is not clang-tidy $PINNED, so results may differ from CI." >&2
else
# One already on the system comes first: the pinned version outright, another
# version if the user accepts the difference. The pinned version is installed
# into a virtual environment otherwise.
INSTALLED=""
for candidate in $(command -v clang-tidy "clang-tidy-${PINNED%%.*}" || true) \
"/usr/lib/llvm-${PINNED%%.*}/bin/clang-tidy" \
"$(brew --prefix llvm 2>/dev/null || true)/bin/clang-tidy"; do
if is_pinned "$candidate"; then
CLANG_TIDY="$candidate"
break
fi
[ -z "$INSTALLED" ] && [ -x "$candidate" ] && INSTALLED="$candidate"
done
if [ -z "$CLANG_TIDY" ] && [ -n "$INSTALLED" ] && [ "$YES" = 0 ] && ! is_pinned "$VENV/bin/clang-tidy"; then
echo "Found $INSTALLED, which is $("$INSTALLED" --version | sed -n 's/.*version \([0-9.]*\).*/\1/p' | head -n 1), not the $PINNED CI uses, so results may differ slightly."
if ask "Use it anyway?"; then
CLANG_TIDY="$INSTALLED"
fi
fi
fi
if [ -z "$CLANG_TIDY" ]; then
CLANG_TIDY="$VENV/bin/clang-tidy"
if [ ! -x "$CLANG_TIDY" ] || ! "$CLANG_TIDY" --version | grep -q "version $PINNED"; then
if ! is_pinned "$CLANG_TIDY"; then
if ask "clang-tidy $PINNED (the version CI uses) is not installed. Install it into $VENV?"; then
mkdir -p "$BUILD_DIR"
if ! python3 -m venv "$VENV"; then
@@ -158,9 +186,6 @@ else
fi
fi
fi
if ! "$CLANG_TIDY" --version | grep -q "version $PINNED"; then
echo "Warning: $CLANG_TIDY is not clang-tidy $PINNED, so results may differ from CI." >&2
fi
# --- Dependencies -------------------------------------------------------------
@@ -217,7 +242,12 @@ cmake --build "$BUILD_DIR" --target git_commit_hash_header >/dev/null
if [ -z "$BASE" ]; then
REMOTE=$(git remote -v | awk '/github\.com[:\/]OrcaSlicer\/OrcaSlicer(\.git)? \(fetch\)/ { print $1; exit }')
REMOTE="${REMOTE:-origin}"
if [ -z "$REMOTE" ]; then
# Against a fork's main that already has the commits, nothing is checked.
echo "Warning: no remote points at github.com/OrcaSlicer/OrcaSlicer, so this compares against origin/main." >&2
echo "If origin is your fork, add the upstream remote (git remote add upstream https://github.com/OrcaSlicer/OrcaSlicer.git) or pass --base." >&2
REMOTE=origin
fi
if [ "$FETCH" = 1 ]; then
git fetch --quiet "$REMOTE" main
fi
+57
View File
@@ -155,6 +155,8 @@ class TestSubprocessCalls(unittest.TestCase):
diff = "+++ b/src/libslic3r/Über.cpp\n@@ -1,0 +2 @@\n+// 打印\n"
files, call, _ = self.run_patched(clang_tidy_diff.changed_files, "base", stdout=diff)
self.assertIn("core.quotePath=false", call.args[0])
# Whatever diff.noprefix or diff.mnemonicPrefix a user has set.
self.assertIn("--dst-prefix=b/", call.args[0])
self.assertEqual(call.kwargs["encoding"], "utf-8")
self.assertEqual(files["src/libslic3r/Über.cpp"].lines, [[2, 2]])
@@ -167,5 +169,60 @@ class TestSubprocessCalls(unittest.TestCase):
self.assertEqual(call.kwargs["encoding"], "utf-8")
class TestCheckFileFix(unittest.TestCase):
"""check_file with -- --fix: what clang-tidy is run on, and what is reported afterwards."""
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.addCleanup(self.dir.cleanup)
self.path = os.path.join(self.dir.name, "Color.cpp")
with open(self.path, "w") as f:
f.write("int x;\n")
def finding(self, line, include=""):
return clang_tidy_diff.Diagnostic(self.path, line, 1, "error",
'no header providing "x" is directly included [misc-include-cleaner]', include)
def check(self, change, results, fix_writes=None):
"""Run check_file with run_clang_tidy answering from `results` in turn; the --fix run
rewrites the file with `fix_writes` when given. Returns (result, calls)."""
calls = []
def run(clang_tidy, build_dir, path, lines, extra_args):
calls.append((lines, extra_args))
if "--fix" in extra_args and fix_writes is not None:
with open(path, "w") as f:
f.write(fix_writes)
return results[len(calls) - 1]
with mock.patch.object(clang_tidy_diff, "run_clang_tidy", side_effect=run):
result = clang_tidy_diff.check_file("clang-tidy", "build", "base", self.path, change, ["--fix"])
return result, calls
def test_a_deleted_include_is_fixed_on_the_lines_it_orphaned_only(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]], removed_includes={"libslic3r/Point.hpp"})
orphaned = self.finding(50, "<libslic3r/Point.hpp>")
unrelated = self.finding(60, "<vector>")
(failed, _, failing, fixed), calls = self.check(
change, [(1, "", [orphaned, unrelated]), (1, "", [orphaned]), (0, "", [])], fix_writes="#include <libslic3r/Point.hpp>\n")
self.assertEqual(calls, [(None, []), ([[4, 4], [50, 50]], ["--fix"]), (None, [])])
self.assertEqual((failed, failing, fixed), (False, [], True))
def test_a_file_the_fix_did_not_change_keeps_its_findings(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]])
error = clang_tidy_diff.Diagnostic(self.path, 4, 1, "error", "unknown type name 'Foo' [clang-diagnostic-error]")
(failed, _, failing, fixed), calls = self.check(change, [(1, "", [error])])
self.assertEqual(calls, [([[4, 4]], ["--fix"])])
self.assertEqual((failed, failing, fixed), (True, [error], False))
def test_a_changed_file_is_checked_again_and_reports_what_is_left(self):
change = clang_tidy_diff.FileChange(lines=[[4, 4]])
error = clang_tidy_diff.Diagnostic(self.path, 4, 1, "error", "unknown type name 'Foo' [clang-diagnostic-error]")
(failed, _, failing, fixed), calls = self.check(
change, [(1, "", [self.finding(4, "<vector>"), error]), (1, "", [error])], fix_writes="#include <vector>\n")
self.assertEqual(calls, [([[4, 4]], ["--fix"]), ([[4, 4]], [])])
self.assertEqual((failed, failing, fixed), (True, [error], True))
if __name__ == "__main__":
unittest.main()
@@ -5,10 +5,5 @@
<!-- for dynamic loading of libraries without signature validation. Used for 3dconnection drivers.-->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- The Bambu network plug-in's code protector rewrites one of its own signed code pages
after loading. Under the hardened runtime macOS kills the process when that page is
paged back in; this lets it run, as Bambu Studio's signature does. -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
+10
View File
@@ -352,6 +352,16 @@ void FillSpiralInset::_fill_surface_single(const FillParams& params,
assert(params.use_arachne);
assert(this->print_config != nullptr && this->print_object_config != nullptr);
// Internal solid infill must not add Arachne's standalone thin walls: one of those walls can
// become the extra centre point after the spiral has finished. Top and bottom surfaces keep
// Arachne, including the centre plug handled by generate_spiral_insets().
if (params.extrusion_role == erSolidInfill) {
Polylines polylines;
this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled<coord_t>(this->spacing)));
return;
}
// Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops.
if (params.density <= 0.9999f || params.dont_adjust) {
Polylines polylines;
+83 -105
View File
@@ -751,6 +751,50 @@ template<typename T>
}
}
namespace {
// Writes G-code to a file in blocks and records in lines_ends the file offset after every '\n'
class GCodeFileWriter
{
public:
GCodeFileWriter(FilePtr& out, const std::string& out_path, std::vector<size_t>& lines_ends, const char* error_message)
: m_out(out), m_out_path(out_path), m_lines_ends(lines_ends), m_error_message(error_message)
{}
~GCodeFileWriter() { assert(m_buffer.empty() || std::uncaught_exceptions() > 0); }
void append(std::string_view text)
{
const size_t text_pos = m_file_pos + m_buffer.size();
for (size_t i = text.find('\n'); i != std::string_view::npos; i = text.find('\n', i + 1))
m_lines_ends.emplace_back(text_pos + i + 1);
m_buffer += text;
if (m_buffer.size() >= GCodeProcessor::Output_Block_Size)
flush();
}
void flush()
{
if (m_buffer.empty())
return;
fwrite(m_buffer.data(), 1, m_buffer.size(), m_out.f);
if (ferror(m_out.f)) {
m_out.close();
boost::nowide::remove(m_out_path.c_str());
throw Slic3r::RuntimeError(m_error_message);
}
m_file_pos += m_buffer.size();
m_buffer.clear();
}
private:
FilePtr& m_out;
const std::string& m_out_path;
std::vector<size_t>& m_lines_ends;
const char* m_error_message;
std::string m_buffer;
size_t m_file_pos{0};
};
} // namespace
// Helper class to modify and export gcode to file
class ExportLines
{
@@ -765,16 +809,6 @@ public:
enum class EWriteType { BySize, ByTime };
private:
static void update_lines_ends_and_out_file_pos(const std::string& out_string, std::vector<size_t>& lines_ends, size_t* out_file_pos)
{
for (size_t i = 0; i < out_string.size(); ++i) {
if (out_string[i] == '\n')
lines_ends.emplace_back((out_file_pos != nullptr) ? *out_file_pos + i + 1 : i + 1);
}
if (out_file_pos != nullptr)
*out_file_pos += out_string.size();
}
struct LineData
{
std::string line;
@@ -814,12 +848,14 @@ private:
EWriteType m_write_type{EWriteType::BySize};
// Time machines containing g1 times cache
const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& m_machines;
// Output file writer
GCodeFileWriter& m_writer;
// Current time
std::array<float, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)> m_times{0.0f, 0.0f};
// Current size in bytes
// Current size of the cache in bytes
size_t m_size{0};
// gcode lines cache
// gcode lines cache, used only when writing by time
std::deque<LineData> m_lines;
size_t m_added_lines_counter{0};
// map of gcode line ids from original to final
@@ -827,16 +863,16 @@ private:
std::vector<std::pair<size_t, size_t>> m_gcode_lines_map;
size_t m_times_cache_id{0};
size_t m_out_file_pos{0};
public:
ExportLines(EWriteType type, const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& machines)
ExportLines(EWriteType type, const std::array<GCodeProcessor::TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& machines, GCodeFileWriter& writer)
#ifndef NDEBUG
: m_statistics(*this)
, m_write_type(type)
, m_machines(machines){}
, m_machines(machines)
, m_writer(writer){}
#else
: m_write_type(type), m_machines(machines)
: m_write_type(type), m_machines(machines), m_writer(writer)
{}
#endif // NDEBUG
@@ -890,11 +926,14 @@ public:
if (line.empty())
return;
m_lines.push_back({line, m_times});
if (m_write_type == EWriteType::ByTime) {
m_lines.push_back({line, m_times});
#ifndef NDEBUG
m_statistics.add_line(line.length());
m_statistics.add_line(line.length());
#endif // NDEBUG
m_size += line.length();
m_size += line.length();
} else
m_writer.append(line);
++m_added_lines_counter;
if (!ignore_from_move) {
assert(!m_gcode_lines_map.empty());
@@ -960,65 +999,35 @@ public:
}
}
// write to file:
// m_write_type == EWriteType::ByTime - all lines older than m_time - backtrace_time
// m_write_type == EWriteType::BySize - all lines if current size is greater than 65535 bytes
void write(FilePtr& out, float backtrace_time, GCodeProcessorResult& result, const std::string& out_path)
// when writing by time, pass the cached lines older than m_times[Normal] - backtrace_time to the writer
void write(float backtrace_time)
{
if (m_lines.empty())
if (m_write_type != EWriteType::ByTime)
return;
// collect lines to write into a single string
std::string out_string;
if (!m_lines.empty()) {
if (m_write_type == EWriteType::ByTime) {
while (m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) {
const LineData& data = m_lines.front();
out_string += data.line;
m_size -= data.line.length();
m_lines.pop_front();
while (!m_lines.empty() && m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) {
const LineData& data = m_lines.front();
m_writer.append(data.line);
m_size -= data.line.length();
m_lines.pop_front();
#ifndef NDEBUG
m_statistics.remove_line();
m_statistics.remove_line();
#endif // NDEBUG
}
} else {
if (m_size > 65535) {
while (!m_lines.empty()) {
out_string += m_lines.front().line;
m_lines.pop_front();
}
m_size = 0;
#ifndef NDEBUG
m_statistics.remove_all_lines();
#endif // NDEBUG
}
}
}
{
write_to_file(out, out_string, result, out_path);
update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos);
}
}
// flush the current content of the cache to file
void flush(FilePtr& out, GCodeProcessorResult& result, const std::string& out_path)
// flush the current content of the cache and the writer to file
void flush()
{
// collect lines to flush into a single string
std::string out_string;
while (!m_lines.empty()) {
out_string += m_lines.front().line;
m_writer.append(m_lines.front().line);
m_lines.pop_front();
}
m_size = 0;
#ifndef NDEBUG
m_statistics.remove_all_lines();
#endif // NDEBUG
{
write_to_file(out, out_string, result, out_path);
update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos);
}
m_writer.flush();
}
void synchronize_moves(GCodeProcessorResult& result) const
@@ -1051,20 +1060,7 @@ public:
size_t get_size() const { return m_size; }
private:
void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path)
{
if (!out_string.empty()) {
if (true) {
fwrite((const void*) out_string.c_str(), 1, out_string.length(), out.f);
if (ferror(out.f)) {
out.close();
boost::nowide::remove(out_path.c_str());
throw Slic3r::RuntimeError("GCode processor post process export failed.\nIs the disk full?");
}
}
}
}
void reserve(size_t lines_count) { m_gcode_lines_map.reserve(lines_count); }
};
void GCodeProcessor::run_post_process()
@@ -1160,8 +1156,13 @@ void GCodeProcessor::run_post_process()
last_exported_stop[i] = time_in_minutes(m_time_processor.machines[i].time);
}
m_result.lines_ends.clear();
// m_result.lines_ends.emplace_back(std::vector<size_t>());
GCodeFileWriter writer(out, out_path, m_result.lines_ends, "GCode processor post process export failed.\nIs the disk full?");
ExportLines export_line(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize,
m_time_processor.machines);
m_time_processor.machines, writer);
// The line map holds an entry for each line of the file, and the first pass counted them
export_line.reserve(m_line_id);
// replace placeholder lines with the proper final value
// gcode_line is in/out parameter, to reduce expensive memory allocation
@@ -1535,9 +1536,6 @@ void GCodeProcessor::run_post_process()
}
};
m_result.lines_ends.clear();
// m_result.lines_ends.emplace_back(std::vector<size_t>());
// Orca: freshly collect SKIPPABLE ranges each post-process pass. The ranges are stored on the
// member (rather than a local) so the injection pass can consume them, hence the clear here to
// avoid stale ranges on re-invocation.
@@ -1802,7 +1800,7 @@ void GCodeProcessor::run_post_process()
if (!gcode_line.empty())
export_line.append_line(gcode_line);
export_line.write(out, 1.1f * max_backtrace_time, m_result, out_path);
export_line.write(1.1f * max_backtrace_time);
gcode_line.clear();
}
}
@@ -1844,7 +1842,7 @@ void GCodeProcessor::run_post_process()
}
}
export_line.flush(out, m_result, out_path);
export_line.flush();
out.close();
in.close();
@@ -1992,31 +1990,13 @@ void GCodeProcessor::run_second_pass_injection()
// The rewrite may shift byte positions (once the injector inserts lines), so rebuild lines_ends from scratch.
// With an empty map the scanned '\n' offsets reproduce the current lines_ends exactly.
m_result.lines_ends.clear();
size_t out_file_pos = 0;
auto write_out = [&out, &out_path, this, &out_file_pos](std::string& str) {
if (str.empty())
return;
fwrite((const void*) str.c_str(), 1, str.length(), out.f);
if (ferror(out.f)) {
out.close();
boost::nowide::remove(out_path.c_str());
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nIs the disk full?\n"));
}
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == '\n')
m_result.lines_ends.emplace_back(out_file_pos + i + 1);
}
out_file_pos += str.size();
str.clear();
};
GCodeFileWriter writer(out, out_path, m_result.lines_ends, "GCode processor pre-heat injection pass failed.\nIs the disk full?\n");
// Orca: read/split lines with EOL-preserving semantics (keep the original \r and \n bytes, and
// synthesize no trailing newline). This is required for the empty-map identity: normalizing every
// line ending to "\n" would not be byte-identical if the finished file used \r\n or lacked a
// final newline.
std::string gcode_line;
std::string export_buffer;
unsigned int line_id = 0;
auto op_it = inserted_operation_lines.begin();
std::vector<char> buffer(65536 * 10, 0);
@@ -2058,16 +2038,14 @@ void GCodeProcessor::run_second_pass_injection()
}
++op_it;
}
export_buffer += gcode_line;
writer.append(gcode_line);
gcode_line.clear();
if (export_buffer.length() >= 65536)
write_out(export_buffer);
}
}
if (eof)
break;
}
write_out(export_buffer);
writer.flush();
out.close();
in.close();
+3
View File
@@ -491,6 +491,9 @@ class Print;
static const std::string VFlush_End_Tag;
static const std::string External_Purge_Tag;
public:
// Size of the blocks the post-processing passes write the G-code in
static constexpr size_t Output_Block_Size = 65536;
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
// path) can reference them single-sourced.
+125 -26
View File
@@ -759,6 +759,59 @@ static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &
}
}
// ORCA: only_one_wall_top - widest bead of the given walls.
static coord_t widest_bead(const std::vector<Arachne::VariableWidthLines> &walls)
{
coord_t widest = 0;
for (const Arachne::VariableWidthLines &group : walls)
for (const Arachne::ExtrusionLine &el : group)
for (const Arachne::ExtrusionJunction &j : el.junctions)
widest = std::max(widest, j.w);
return widest;
}
// ORCA: only_one_wall_top - length of the walls running further than tolerance from the reference walls, outside the
// excluded area.
static double length_off_reference(const std::vector<Arachne::VariableWidthLines> &walls, const Arachne::VariableWidthLines &reference,
const ExPolygons &excluded, coord_t tolerance)
{
auto append_centerlines = [](const Arachne::VariableWidthLines &lines, Polylines &out) {
for (const Arachne::ExtrusionLine &el : lines) {
if (el.junctions.size() < 2)
continue;
Polyline &centerline = out.emplace_back();
centerline.points.reserve(el.junctions.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
centerline.points.emplace_back(j.p);
}
};
Polylines wall_centerlines;
Polylines reference_centerlines;
for (const Arachne::VariableWidthLines &group : walls)
append_centerlines(group, wall_centerlines);
append_centerlines(reference, reference_centerlines);
Polylines off_reference = diff_pl(wall_centerlines, offset(reference_centerlines, float(tolerance)));
if (! excluded.empty())
off_reference = diff_pl(off_reference, excluded);
return total_length(off_reference);
}
// ORCA: only_one_wall_top - area covered by the given walls at their local widths.
static Polygons walls_footprint(const Arachne::VariableWidthLines &walls)
{
Polygons footprint;
for (const Arachne::ExtrusionLine &el : walls)
for (size_t i = 1; i < el.junctions.size(); ++ i) {
const Arachne::ExtrusionJunction &a = el.junctions[i - 1];
const Arachne::ExtrusionJunction &b = el.junctions[i];
const coord_t width = std::max(a.w, b.w);
if (width > 0)
append(footprint, offset(Polyline(a.p, b.p), float(width) / 2.f));
}
return union_(footprint);
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
ExPolygons &non_top_polygons, ExPolygons &fill_clip) const {
// other perimeters
@@ -2546,44 +2599,90 @@ void PerimeterGenerator::process_arachne()
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Get top ExPolygons from current infill contour.
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
top = diff_ex(top, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
return intersection_ex(top, contour);
};
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
std::vector<Arachne::VariableWidthLines> full_perimeters;
Polygons full_inner_contour;
bool full_perimeters_generated = false;
auto generate_full_perimeters = [&]() {
if (full_perimeters_generated)
return;
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
full_perimeters = full_tool_paths.getToolPaths();
full_inner_contour = full_tool_paths.getInnerContour();
full_perimeters_generated = true;
};
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
// the width added, and only beads widened by more than twice the tolerance are looked for.
const coord_t outer_wall_tolerance = bead_width_0 / 10;
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
bool nominal_uncovered = false;
// Grown by an outer wall width to take in the outer walls bordering the top surface.
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
if (nominal_uncovered) {
generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
}
}
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
@@ -2612,11 +2711,11 @@ void PerimeterGenerator::process_arachne()
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
}
}
//PS
+7
View File
@@ -7970,9 +7970,16 @@ void GLCanvas3D::_render_fps_overlay(int fps) const
const float margin = 10.0f * get_scale();
const ImVec2 display_size = ImGui::GetIO().DisplaySize;
ImVec2 pos(display_size.x - margin, margin);
// Last frame's size; zero until the overlay has been shown once.
const ImGuiWindow* self = ImGui::FindWindowByName("###fps_overlay");
const float left = pos.x - (self != nullptr ? self->Size.x : 0.0f);
// The Preview legend takes the top-right corner.
if (const ImGuiWindow* legend = ImGui::FindWindowByName("Legend"); m_canvas_type == ECanvasType::CanvasPreview && legend != nullptr && legend->Active)
pos = ImVec2(legend->Pos.x - margin, legend->Pos.y);
// The toolbar row can reach the corner on a narrow canvas; stack the overlay below it then.
else if (m_main_toolbar.is_enabled() &&
get_main_toolbar_offset() + m_main_toolbar.get_width() + m_separator_toolbar.get_width() + m_gizmos.get_scaled_total_width() + m_assemble_view_toolbar.get_width() > left)
pos.y = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + margin;
ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.35f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * get_scale());
+4 -1
View File
@@ -1816,7 +1816,10 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
m_input_printer_name->GetTextCtrl()->SetLabelText(m_obj->get_dev_name());
std::string img_str = DevPrinterConfigUtil::get_printer_connect_help_img(m_obj->printer_type);
auto diagram_bmp = create_scaled_bitmap(img_str + "_en", this, 198);
if (img_str.empty()) { img_str = "input_access_code_x1"; }
std::string language = wxGetApp().app_config->get("language");
auto diagram_bmp = create_scaled_bitmap(img_str + (language == "zh_CN" ? "_cn" : "_en"), this, 198);
m_img_help->SetBitmap(diagram_bmp);
+1 -1
View File
@@ -2928,7 +2928,7 @@ void TabPrint::build()
optgroup->append_single_option_line("skin_infill_line_width", "strength_settings_patterns#locked-zag");
optgroup->append_single_option_line("skeleton_infill_line_width", "strength_settings_patterns#locked-zag");
optgroup->append_single_option_line("symmetric_infill_y_axis", "strength_settings_infill#symmetric-infill-y-axis");
optgroup->append_single_option_line("infill_complete_top", "strength_settings_infill#infill-complete-top");
optgroup->append_single_option_line("infill_complete_top", "strength_settings_patterns#fill-pattern-tops");
optgroup->append_single_option_line("infill_shift_step", "strength_settings_patterns#cross-hatch");
optgroup->append_single_option_line("lateral_lattice_angle_1", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("lateral_lattice_angle_2", "strength_settings_patterns#lateral-lattice");
+7
View File
@@ -456,6 +456,13 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in,
const vector<std::string>& array_new_color/* = vector<std::string>*/)//used for semi transparent material)
{
static Slic3r::GUI::BitmapCache cache;
// An empty name means the caller's icon lookup failed
if (bmp_name_in.empty() || bmp_name_in == ".png") {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": empty bitmap name";
return wxNullBitmap;
}
if (bitmap2) {
return create_scaled_bitmap2(bmp_name_in, cache, win, px_cnt, grayscale, resize, array_new_color);
}
+44
View File
@@ -1,15 +1,20 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Utils.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include "libslic3r/PrintConfig.hpp"
@@ -177,3 +182,42 @@ TEST_CASE("A seam takes the actual speed of the move it follows", "[GCodeProcess
}
REQUIRE(seams > 0);
}
TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[GCodeProcessor]")
{
struct Case
{
const char* name;
bool preheat_backtrace;
bool pre_heating;
};
const auto test_case = GENERATE(values<Case>({
{ "written by size", false, false },
{ "written by time for the preheat backtrace", true, false },
{ "rewritten by the pre-heating pass", false, true },
}));
INFO(test_case.name);
DynamicPrintConfig config = Test::multifilament_config(2, {
{ "single_extruder_multi_material", 0 },
{ "ooze_prevention", test_case.preheat_backtrace },
{ "preheat_time", 30 },
{ "enable_pre_heating", test_case.pre_heating },
});
Print print;
Model model;
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { { "extruder", 1 } }, { { "extruder", 2 } } };
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config, &overrides);
GCodeProcessorResult result;
const std::string gcode = Test::gcode(print, &result);
REQUIRE((gcode.find("preheat T") != std::string::npos) == test_case.preheat_backtrace);
REQUIRE((gcode.find(GCodeProcessor::Machine_Start_GCode_End_Tag) != std::string::npos) == test_case.pre_heating);
REQUIRE(gcode.size() > GCodeProcessor::Output_Block_Size);
std::vector<size_t> newline_ends;
for (size_t i = gcode.find('\n'); i != std::string::npos; i = gcode.find('\n', i + 1))
newline_ends.push_back(i + 1);
REQUIRE(result.lines_ends.size() == newline_ends.size());
const auto difference = std::mismatch(result.lines_ends.begin(), result.lines_ends.end(), newline_ends.begin());
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
CHECK(difference.first == result.lines_ends.end());
}
+4 -3
View File
@@ -14,6 +14,7 @@
#include <initializer_list>
#include "libslic3r/Point.hpp"
#include <fstream>
#include <ios>
#include <iterator>
#include <set>
#include <string>
@@ -330,13 +331,13 @@ void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::
print.process();
}
std::string gcode(Print & print)
std::string gcode(Print & print, GCodeProcessorResult* result)
{
ScopedTemporaryFile temp(".gcode");
print.set_status_silent();
print.process();
print.export_gcode(temp.string(), nullptr, nullptr);
std::ifstream t(temp.string());
print.export_gcode(temp.string(), result, nullptr);
std::ifstream t(temp.string(), std::ios::binary);
std::string str((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
return str;
}
+5 -3
View File
@@ -17,7 +17,9 @@
#include <unordered_map>
#include <vector>
namespace Slic3r { namespace Test {
namespace Slic3r {
struct GCodeProcessorResult;
namespace Test {
constexpr double MM_PER_MIN = 60.0;
@@ -92,8 +94,8 @@ void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// Process `print` and return its exported G-code.
std::string gcode(Print& print);
// Process `print` and return its exported G-code, filling `result` when one is given.
std::string gcode(Print& print, GCodeProcessorResult* result = nullptr);
// Build, slice, and return the G-code for `meshes` under the given config.
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config);
+120
View File
@@ -19,8 +19,10 @@
#include <cstddef>
#include "libslic3r/libslic3r.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include <limits>
#include <string>
#include <utility>
@@ -275,6 +277,124 @@ TEST_CASE("Only one wall on the first layer needs a bottom shell", "[Perimeters]
namespace {
// The last layer of the tab, whose top surface shares an island with the tube walls rising past it.
const double tab_top_z = 5.0;
// With the widths below the tube walls are 1.10mm wide once the precise outer wall offset (0.043mm a side) is
// taken off. That is narrower than 3 outer wall spacings (3 x 0.377 = 1.131mm), so an Arachne pass limited to a
// single wall fills it by widening its 2 beads, yet wide enough for the full 2 wall pass to add a middle wall
// (from 1.062mm).
const double narrow_wall = 1.186;
// A 20x30x10 tube with narrow_wall thick walls, and a 20x8x5 tab against its -Y side.
Print &tube_with_tab(Print &print, Model &model, const DynamicPrintConfig &config)
{
ModelObject *object = model.add_object();
object->name = "tube_with_tab.stl";
object->add_volume(make_cube(20., 30., 10.), ModelVolumeType::MODEL_PART, false);
// Overlaps the tube wall by 0.5mm so the two parts slice as one island.
TriangleMesh tab = make_cube(20., 8.5, 5.);
tab.translate(0.f, -8.f, 0.f);
object->add_volume(std::move(tab), ModelVolumeType::MODEL_PART, false);
TriangleMesh bore = make_cube(20. - 2. * narrow_wall, 30. - 2. * narrow_wall, 12.);
bore.translate(float(narrow_wall), float(narrow_wall), -1.f);
object->add_volume(std::move(bore), ModelVolumeType::NEGATIVE_VOLUME, false);
object->add_instance();
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
return print;
}
// Every width the narrow_wall arithmetic depends on, so none of them rests on a default.
DynamicPrintConfig narrow_wall_config(bool only_one_wall_top, double top_surface_expansion)
{
DynamicPrintConfig config = base_config("arachne");
config.set_deserialize_strict({
{ "wall_loops", 2 },
{ "nozzle_diameter", "0.4" },
{ "line_width", 0.42 },
{ "outer_wall_line_width", 0.42 },
{ "inner_wall_line_width", 0.45 },
{ "min_bead_width", "85%" },
{ "precise_outer_wall", true },
{ "wall_sequence", "inner wall/outer wall" },
{ "only_one_wall_top", only_one_wall_top },
{ "top_surface_expansion", top_surface_expansion },
});
return config;
}
// Inner wall length the layer at print_z extrudes within 3mm of its +Y edge: the tube wall facing away from the tab.
double far_wall_inner_wall_length(const Print &print, double print_z)
{
for (const Layer *layer : print.objects().front()->layers()) {
if (std::abs(layer->print_z - print_z) > EPSILON)
continue;
BoundingBox band = get_extents(layer->lslices);
band.min.y() = band.max.y() - scaled<coord_t>(3.);
Polylines inner_walls;
auto collect = [&inner_walls](const ExtrusionPaths &paths) {
for (const ExtrusionPath &path : paths)
if (path.role() == erPerimeter)
inner_walls.emplace_back(path.as_polyline());
};
for (const LayerRegion *region : layer->regions()) {
const ExtrusionEntityCollection walls = region->perimeters.flatten();
for (const ExtrusionEntity *entity : walls.entities) {
if (const auto *loop = dynamic_cast<const ExtrusionLoop*>(entity))
collect(loop->paths);
else if (const auto *multi_path = dynamic_cast<const ExtrusionMultiPath*>(entity))
collect(multi_path->paths);
else if (const auto *path = dynamic_cast<const ExtrusionPath*>(entity))
collect({ *path });
}
}
return unscaled<double>(total_length(intersection_pl(inner_walls, band.polygon())));
}
return 0.;
}
} // namespace
// only_one_wall_top first lays out an island with a single Arachne wall and generates the inner walls inside it.
// On a wall narrower than 3 outer wall spacings that single wall pass widens its 2 beads to fill the wall and leaves
// no room for the middle wall, which is only intended over the top surface. The tube walls away from the tab are not
// under the tab's top surface, so on the tab's last layer they keep the inner wall they get with the option off.
TEST_CASE("Only one wall on top surfaces keeps the inner walls of narrow walls away from the top surface", "[Perimeters]")
{
// 0 re-onions the region beside the top surface, 2 clips the inner walls over it.
const double top_surface_expansion = GENERATE(0.0, 2.0);
CAPTURE(top_surface_expansion);
struct TabTopLayer {
double perimeters;
double far_wall_inner_walls;
};
auto tab_top_layer_for = [top_surface_expansion](bool only_one_wall_top) {
Print print;
Model model;
tube_with_tab(print, model, narrow_wall_config(only_one_wall_top, top_surface_expansion));
print.process();
REQUIRE_FALSE(print.objects().empty());
return TabTopLayer{ perimeter_length_at(print, tab_top_z), far_wall_inner_wall_length(print, tab_top_z) };
};
const TabTopLayer plain = tab_top_layer_for(false);
const TabTopLayer one_wall = tab_top_layer_for(true);
// The option acts on this layer: the inner walls under the tab's top surface are gone.
REQUIRE(plain.far_wall_inner_walls > 10.);
CHECK(one_wall.perimeters < plain.perimeters);
CHECK_THAT(one_wall.far_wall_inner_walls, Catch::Matchers::WithinAbs(plain.far_wall_inner_walls, 1.0));
}
namespace {
// The layer that closes the cavity of box_over_cavity(), the first one printed over air.
const double cavity_ceiling_z = 6.2;