Skip to content

Negative volumes in a third-party 2.x-style 3MF are printed solid — exit 0, no message (3.0.0-alpha11) #15694

Description

@Neunzehnneunzig

Description of the bug

A 3MF that carries its volume types in Metadata/Slic3r_PE_model.config (the PrusaSlicer 2.x project layout) is loaded as plain geometry unless its value is PrusaSlicer- followed by a remainder that parses as a Semver and is ≤ 2.99.1. Every volume then becomes a ModelPart, so a NegativeVolume is sliced as solid geometry — with exit code 0, the normal Slicing result exported to … line and empty stderr.

One archive, sliced five times with the same profiles, with only that metadata value changed between runs:

filament used [mm] last ;Z: objects_info
ThirdPartyTool-1.0 8397.98 55 file stem, 40 × 40 mm
PrusaSlicer-3.0.0 8397.98 55 file stem, 40 × 40 mm
PrusaSlicer-notaversion 8397.98 55 file stem, 40 × 40 mm
PrusaSlicer-2.99.1 1955.53 15 CubeObj, 30 × 30 mm
PrusaSlicer-2.9.6 1955.53 15 CubeObj, 30 × 30 mm

The last two rows are the intended result: the 40 mm NegativeVolume removes the top of the 30 mm body. The other three slice the cutter as solid geometry — 4.3× the filament of the correct part, and 2.5× the body on its own (solid.3mf: 3417.63 mm, last ;Z:30). The object also keeps the file stem instead of the name from the model config.

PrusaSlicer 2.9.6 slices the ThirdPartyTool-1.0 and PrusaSlicer-2.9.6 files to the same G-code (2328.01 mm, last ;Z:15.05, both 135 924 bytes; diff reports exactly one differing line, the generation timestamp in the header). It applies the volume types regardless of the producer string.

Source, tag version_3.0.0-alpha11
  • src/slic3r-shared/src/Slic3r/Biz/Format/3mf/Model3mf.cpp:916 is_old_stored_version() requires all three: the Application metadata starts with "PrusaSlicer-" (:921), the remainder parses as a Semver (:923), and that version is ≤ last_old_stored_version (2.99.1, :150) (:926). :943-:949 then records Read3mfIssueType::legacy_loader_required (:944) and stops the XML parser (:949).
  • src/slic3r-shared/src/Slic3r/Biz/Format/3mf.cpp:671-672 throws on that issue, and FileLoadingLogic.cpp:668 → :671 reaches load_legacy_project() (:579) only through that exception. Both CLI branches arrive there: the geometry path calls load_from_project(input_file_path, std::nullopt) at FileLoadingLogic.cpp:721, and src/slic3r-app-cli/src/Slic3r/App/CLI/LoadPrintData.cpp:210-223 routes to that geometry path whenever profile options are given (has_full_config_from_profiles(), comment: "We have a full bunch of options from profiles set, so just load a geometry.").
  • The new loader never opens Metadata/Slic3r_PE_model.config; that name exists only in the legacy reader (src/slic3r-shared/src/Slic3r/Biz/Config/Legacy/3mf_legacy.cpp:128). In the new loader the file merely becomes Read3mfIssueType::unprocessed_file_in_3mf (Format/3mf.cpp:745), and src/slic3r-app-cli/ contains no reference to the collected issues, so nothing is printed.

Expected: the volume types are applied for any producer (2.9.6 does this), or the file is refused with a message. The alpha11 release notes say "loading projects using custom printers or projects from other slicers is not fully supported yet. You may encounter errors in these cases." — here there is no error at all, only a different part. The same section states the principle directly for the reverse direction: "If you are running PrusaSlicer 2.9.1 or later, you will be notified about this. In older PrusaSlicer versions, the config will be skipped silently." Silently skipping the config is treated there as the old behaviour to be avoided.

Possible fix: choose the legacy reader by archive content (Metadata/Slic3r_PE_model.config present, Metadata/PrusaSlicer3_project.json absent) instead of by the Application string. The file also already announces itself with a key the new loader parses: slic3rpe:Version3mf is mapped to ModelMetadataNames::Slic3r_version at Format/3mf/Metadata.cpp:24 and read back through read_name() (:66), but every use of that value is commented out (Metadata.cpp:50, Model3mf.cpp:1893, :1907). Gating on either signal would not depend on the producer name at all. If neither is wanted, at least surface the ignored file on the CLI rather than dropping the unprocessed_file_in_3mf issue.

Worth knowing before suggesting the obvious workaround: stamping the file PrusaSlicer-2.9.6 routes it to the legacy loader, which then depends on the printer config being resolvable. With the three profile options used below, --info succeeds (exit 0). Without them it fails — "%PS3%" --datadir "%DD%" --info neg_prusaslicer.3mf gives exit 1 and neg_prusaslicer.3mf: Loading file failed: Cannot find compatible printer HW config, while the same command on neg_thirdparty.3mf gives exit 0 and reports the cutter as geometry (number_of_facets = 24, number_of_parts = 2, size_z = 55).

Related, not duplicates: #15662 (third-party 3MF metadata, object names only) and #15644 (2.x project crashes on preset save).

Zipped project file & How to reproduce

Nothing attached — the three 3MFs are 1.4–1.6 kB and are written by the script at the bottom.

1. Unpack PrusaSlicer-3.0.0-alpha11.zip. Point --datadir at a directory a GUI run has already initialised and that carries the MK4 presets used below. With an empty --datadir the alpha crashes while populating the vendor bundle (exit -1073740791 / 0xC0000409, last log line [BundleLoader.cpp:43] Populate vendor prusa-research-fff/PrusaResearch, nothing on stderr) — unrelated to this report, but it will stop you before you get here.

2. In an empty folder, save the script below as make_3mf.py and run it:

python make_3mf.py
wrote solid.3mf 1450 bytes
wrote neg_thirdparty.3mf 1582 bytes
wrote neg_prusaslicer.3mf 1581 bytes

solid.3mf is a 30 mm cube. neg_thirdparty.3mf is that cube plus a 40 mm box (triangles 12..23) declared volume_type=NegativeVolume in Metadata/Slic3r_PE_model.config. neg_prusaslicer.3mf is the same archive with changed from ThirdPartyTool-1.0 to PrusaSlicer-2.9.6; every other byte of every part is identical.

3. Slice all three from that folder:

set PS3=\PrusaSlicer.exe
set DD=
set PROF=--printer-profile "Prusa MK4 0.4" --print-profile "0.20mm SPEED @MK4 0.4" --material-profile "Prusament PLA @MK4 0.4"

"%PS3%" --datadir "%DD%" %PROF% -g -o a_solid.gcode solid.3mf
"%PS3%" --datadir "%DD%" %PROF% -g -o a_third.gcode neg_thirdparty.3mf
"%PS3%" --datadir "%DD%" %PROF% -g -o a_prusa.gcode neg_prusaslicer.3mf

Each run: exit 0, stderr empty (0 bytes). The only non-log line on stdout is Slicing result exported to a_*.gcode; spdlog output also goes to stdout and, depending on the data directory, may include unrelated [error] lines from preset loading — 107 stdout lines, 85 of them [error], in my run, identical for all three files (e.g. Invalid key top_one_perimeter_type). Nothing in that output refers to the 3MF, the ignored config file, or the volume types.

3b. The profile options are not what causes this. LoadPrintData.cpp:210-223 sends the file to load_geometry_project whenever profiles are given, so repeat without them:

"%PS3%" --datadir "%DD%" -g -o n_third.gcode neg_thirdparty.3mf
"%PS3%" --datadir "%DD%" -g -o n_prusa.gcode neg_prusaslicer.3mf

neg_thirdparty.3mf: exit 0, ; filament used [mm] = 8395.98, last ;Z:55 — still solid. neg_prusaslicer.3mf: exit 1, neg_prusaslicer.3mf: Loading file failed: Cannot find compatible printer HW config. Both CLI branches reach the same gate: the geometry path calls load_from_project at FileLoadingLogic.cpp:721.

4. Compare:

findstr /b /c:"; filament used [mm]" /c:"; objects_info" a_solid.gcode a_third.gcode a_prusa.gcode
a_solid.gcode:; objects_info = {"objects":[{"name":"solid","polygon":[[143.000,143.000],[113.000,143.000],[113.000,113.000],[143.000,113.000]]}]}
a_solid.gcode:; filament used [mm] = 3417.63
a_third.gcode:; objects_info = {"objects":[{"name":"neg_thirdparty","polygon":[[148.000,148.000],[108.000,148.000],[108.000,108.000],[148.000,108.000]]}]}
a_third.gcode:; filament used [mm] = 8397.98
a_prusa.gcode:; objects_info = {"objects":[{"name":"CubeObj","polygon":[[113.000,113.000],[143.000,113.000],[143.000,143.000],[113.000,143.000]]}]}
a_prusa.gcode:; filament used [mm] = 1955.53

The top layer is the last ;Z: line of each file: ;Z:30 for a_solid, ;Z:55 for a_third, ;Z:15 for a_prusa. The footprint in objects_info is 40 × 40 mm for a_third and 30 × 30 mm for a_prusa.

4b. To see that the gate is the parsed version and not the PrusaSlicer- prefix, copy neg_thirdparty.3mf three more times, replacing ThirdPartyTool-1.0 in 3D/3dmodel.model with PrusaSlicer-3.0.0, PrusaSlicer-notaversion and PrusaSlicer-2.99.1, then slice as in step 3. The first two give 8397.98 mm and last ;Z:55; only PrusaSlicer-2.99.1 gives 1955.53 mm and last ;Z:15.

5. Optional 2.9.6 comparison, built-in defaults, fresh data directory so no installed preset interferes:

set PS296=C:\Program Files\Prusa3D\PrusaSlicer\prusa-slicer-console.exe

"%PS296%" --datadir "%TEMP%\ps296tmp" -g -o p_third.gcode neg_thirdparty.3mf
"%PS296%" --datadir "%TEMP%\ps296tmp" -g -o p_prusa.gcode neg_prusaslicer.3mf
findstr /b /c:"; filament used [mm]" p_third.gcode p_prusa.gcode
p_third.gcode:; filament used [mm] = 2328.01
p_prusa.gcode:; filament used [mm] = 2328.01

Both 135 924 bytes, last ;Z:15.05, and identical apart from the timestamp in the first line — 2.9.6 subtracts the cutter for both producers.

make_3mf.py — python3, standard library only
# Writes three 2.x-style 3MF projects (stdlib only, python3 make_3mf.py).
# Body: 30 mm cube, triangles 0..11, z 0..30 after the build transform.
# Cutter: 40 mm box, triangles 12..23, z 15..55, declared NegativeVolume in
# Metadata/Slic3r_PE_model.config -> expected: top of the body removed, max Z 15.
# neg_thirdparty.3mf and neg_prusaslicer.3mf differ ONLY in .
import zipfile, os

def box(z0, z1, half):
    v = [(-half,-half,z1),(half,-half,z1),(half,half,z1),(-half,half,z1),
         (-half,-half,z0),(half,-half,z0),(half,half,z0),(-half,half,z0)]
    t = [(0,1,2),(0,2,3),(5,4,7),(5,7,6),(4,0,3),(4,3,7),
         (1,5,6),(1,6,2),(4,5,1),(4,1,0),(3,2,6),(3,6,7)]
    return v, t

def model(app, cutter):
    verts, tris = box(-15, 15, 15)                      # body
    if cutter:
        v2, t2 = box(0, 40, 20)                         # cutter, ids 12..23
        n = len(verts); verts += v2; tris += [(a+n,b+n,c+n) for a,b,c in t2]
    vx = "".join('' % p for p in verts)
    tx = "".join('' % p for p in tris)
    return ('\n'
      '
      ' xmlns:slic3rpe="http://schemas.slic3r.org/3mf/2017/06"'
      ' xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">'
      '1'
      '%s'
      ''
      '%s%s'
      ''
      ''
      '\n') % (app, vx, tx)

def cfg(cutter):
    extra = (''
             ''
             ''
             '') if cutter else ''
    return ('\n'
      ''
      ''
      ''
      ''
      ''
      '%s\n') % extra

CT = ('\n'package/2006/content-types">'openxmlformats-package.relationships+xml"/>\n')
RELS = ('\n'openxmlformats.org/package/2006/relationships">
  ' Id="rel-1" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>'
  '\n')

def write(name, app, cutter):
    with zipfile.ZipFile(name, "w", zipfile.ZIP_DEFLATED) as z:
        z.writestr("[Content_Types].xml", CT)
        z.writestr("_rels/.rels", RELS)
        z.writestr("3D/3dmodel.model", model(app, cutter))
        z.writestr("Metadata/Slic3r_PE_model.config", cfg(cutter))
    print("wrote", name, os.path.getsize(name), "bytes")

write("solid.3mf",            "ThirdPartyTool-1.0", False)
write("neg_thirdparty.3mf",   "ThirdPartyTool-1.0", True)
write("neg_prusaslicer.3mf",  "PrusaSlicer-2.9.6",  True)

Context: this was found by a CLI automation tool that writes 2.x-style projects. Modifier, support-blocker and support-enforcer volumes are declared in the same file and pass through the same gate; only NegativeVolume was measured here.

Version of PrusaSlicer

3.0.0-alpha11 (Windows zip, PrusaSlicer.exe), compared against 2.9.6

Operating system

Windows

Operating system version

11 Pro 26200 (x64)

Printer model

Original Prusa MK4

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions