Merge Belt Printing Into Upstream (#14394)

# Belt printing for OrcaSlicer

This merges the `belt-printer` branch into `main`. It adds support for
conveyor-belt ("infinite Z") printers like the BabyBelt Pro, the
IdeaFormer IR3 V2 and the CR-30 family, along with the things that had
to grow around it: tilted-plane slicing, supports that terminate on the
belt, belt-aware brims, a purge tower that works without a flat bed,
multi-colour belt prints, a preview that shows the part the way it was
designed, and starter profiles.

It has been a long road (the first attempt was #12733 back in March, the
pipeline has been rebuilt twice since) and a lot of people have put work
into it. Credits are at the bottom; please tell me if I've missed
anyone.

Closes #2628, closes #11344, closes #6885, closes #9004, closes #14188.

---

## How belt printing works

Slicers assume the bed sits on the XY plane and layers stack along Z. A
belt printer breaks that in two ways at once: the belt is tilted
(usually 45°) and the axis that advances the belt is the printer's Z, so
the "bed" lives on the XZ plane and is, in principle, infinitely long.

<img width="6360" height="2702" alt="A flat bed vs a tilted belt"
src="https://github.com/user-attachments/assets/3f5542f2-6dab-42bf-9233-f96d863b40c8"
/>

Rather than teach every part of the slicer about tilted beds, the branch
leaves the slicing engine alone and transforms what goes in and what
comes out. The pipeline has five steps.

![printer
pipeline](https://github.com/user-attachments/assets/4d2f9965-f768-4b22-afbe-b3fda05354c4)

<details>
<summary><b>The five steps in detail</b></summary>

### 1. Pre-slice rotation
The mesh is rotated by the belt angle before slicing
(`BeltSliceStrategy`, `BeltTransform`), so that the ordinary horizontal
slicer produces layers that are actually tilted planes relative to the
part. Rotation is about X by default (belt along Y); Y rotation and a Z
lift are supported too. A "global" mode rotates every object about one
common origin, which is what keeps several parts on one belt consistent
with each other.

### 2. Slice
Nothing special. The mesh is rotated, the slicer does what it always
does. This is the reason most of Orca's features (walls, infill, seams,
ironing, painting, …) just work on a belt without belt-specific code.

### 3. Supports
Supports are generated after slicing against a virtual floor: the belt
surface, expressed in the rotated frame (`BeltFloorContext`). Normal,
tree and organic supports all terminate on that plane instead of on Z=0,
and nothing may be generated below it. Painted supports and seams are
transformed with the same `trafo_sliced()` as the layers.

![adding
supports](https://github.com/user-attachments/assets/39c1a858-23e2-48ee-8eb3-7b1abb9be172)

### 4. G-code back-transform
The G-code is rotated back into the model's Cartesian frame
(`BeltBackTransform`). A nice side effect: with step 5 switched off you
can slice at a non-standard angle and print the result on a normal
printer. The first belt-sliced Benchy was printed exactly that way, on a
Sovol SV08.

### 5. Machine frame
The printer doesn't know its bed is tilted. To make straight walls come
out straight, the axes are remapped (the default mapping is X → reversed
X, Y → Z, Z → Y) and the result is sheared and scaled:

```math
\begin{bmatrix} X \\ Y \\ Z \end{bmatrix}
\longrightarrow
\begin{bmatrix} X \\ \dfrac{Y}{\cos\alpha} \\ Z + Y\cdot\tan\alpha \end{bmatrix}
```

This lives in `GCodeWriter` behind a small `MachineKinematics` strategy
(`BeltKinematics` on belts, identity otherwise), so the writer itself
has one code path. The slicing angle and the machine angle can differ if
you want to, e.g. slice at 30° on a 45° machine — mind your nozzle
clearance if you do.

</details>

A short recording of the back-transform from the original PR:


https://github.com/user-attachments/assets/cdb9cc83-711d-48b7-9d86-a014a32c5e8e

---

## What had to change to make it work

Belt mode is gated on the `belt_printer` printer setting; with it off,
every code path below is the old one.

<details>
<summary><b>Slicing and geometry</b></summary>

- `PrintObjectSlice` / `PrintObject`: the rotation, Z lift and
per-object layer-grid shift, plus the invalidation that goes with them.
Modifiers and painted volumes are transformed with the same matrix as
the model.
- `FirstLayerPlane`: "the first layer" on a belt is a band along the
belt, not the first slicing layer. First-layer speed, line width and the
fan band are measured against it.
- `Print::validate`: clearance checks against the gantry instead of the
printable height; skirt, raft, draft shield, the classic prime tower,
arc fitting, spiral vase with brim, and scarf-joint seams are refused or
disabled on belts because each of them either doesn't exist on a belt or
moves the belt the wrong way (a scarf seam starts one layer low, which
on a belt is a 0.28 mm back-step into the previous layer at every seam).
- Z-hop defaults to 0 on belt profiles; a lift on a belt is a belt move.

</details>

<details>
<summary><b>Supports</b></summary>

- `SupportMaterial`, `TreeSupport`, `TreeSupport3D`, `TreeModelVolumes`:
a shared `BeltFloorContext` provides the belt plane; supports clip to
it, extension layers are numbered sequentially, and the first-layer
flange that used to be stamped under everything is gone.
- `build_plate_tilt_x/y` (from #12733) is now derived from the slicing
rotation in `Print::apply`, so GUI and CLI agree; it's capped below 90°.
- Organic supports that reach the belt no longer produce negative flow.

</details>

<details>
<summary><b>Brims (#15155)</b></summary>

A belt first layer is effectively a single line of contact, so a brim
matters more than usual. `BeltBrim` generates per-layer bands along the
belt plus an "apron" ahead of the part, in the object's brim filament.
Two belt-specific settings came with it: **Leading edge brim length**
(more lines on the side printed first) and **Extra brim width** (across
the belt), plus a **Leading edge only** brim type.

<img width="1849" height="1043" alt="belt brim"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>

</details>

<details>
<summary><b>Multi-colour: the belt purge tower</b></summary>

The classic wipe tower can't exist on a belt (its G-code bypasses the
transform and it needs a flat bed to stand on). Instead the GUI
generates a long, thin "purge prism" beside the parts, flush with the
far edge of the belt, one per plate (`BeltPurgeTower.cpp`). It is a real
model object, so it is sliced like everything else, and
`Print::_plan_belt_purge` routes every filament change into it via
`flush_into_objects`. It's sized from the flush matrix, split into one
island per simultaneous tool change, snapped onto the parts' layer grid,
cut off after the last colour change and stripped of infill no change
claimed, so what prints is a good deal smaller than the model you see in
Prepare.

</details>

<details>
<summary><b>G-code generation and cooling</b></summary>

- `GCodeWriter` + `MachineKinematics`/`BeltKinematics`: the
back-transform, axis remap, shear and scale, lifts that are belt moves,
and the first-layer travel speed.
- `GCode.cpp` / `BeltGCode`: a belt header (slicing rotation, remaps,
machine tilt) that the preview reads back; it is written outside the
optional header block so printers with a BTT TFT thumbnail still get it.
Exclude-object outlines are emitted in the plate frame.
- `CoolingBuffer`: the "first layers" the fan stays off for are a band
above the belt, marked per extrusion segment by the generator
(`;_BELT_BAND_START/END`) and honoured on every layer.
- `GCodeProcessor`: belt header parsing, start-G-code Z handling, and
height checks that don't compare belt travel against the printable
height.
- `ToolOrdering` / `BeltPurge`: filament changes are detected by
scanning the ordering (an apron layer never carries the first-layer
flag).

</details>

<details>
<summary><b>GUI</b></summary>

- Printer settings tab: the belt group (slicing rotation, angle, global
mode, infinite Y, purge tower, floor settings). The axis remap and
pre-slice remap options are Develop-mode only.
- `ConfigManipulation`: everything that doesn't apply on a belt is
greyed out (skirt, raft, draft shield, the wipe tower group, scarf
seams, …).
- Preview: a "designed view" that back-transforms the toolpaths onto the
model so you see the part upright, with `B` toggling the raw
machine-frame G-code (`GCodeViewer`, `Shortcuts`). The tilt comes from
the belt header, so imported G-code behaves.
- Arrange: parts are packed from the end of the belt that prints first,
colours are grouped into runs so each filament change happens once, the
purge tower's strip and the brim width are reserved, and piles aimed at
an off-centre `best_object_pos` are clamped to the bed (`Arrange.cpp`,
`ArrangeJob.cpp`, one clamp in `libnest2d`).
- `PartPlate`: plate icons stay in the gap between plates on a long,
narrow bed; the plate is open along Y for containment tests on an
infinite-Y belt.
- Calibration: a belt temperature tower (overhang variant) that slices
correctly on a tilt.
- The old tilted-bed rendering in Prepare was dormant and has been
removed; the bed is shown as the slicing pipeline treats it.

</details>

<details>
<summary><b>Config options</b></summary>

Printer: `belt_printer`, `belt_printer_infinite_y`,
`belt_slice_rotation`, `belt_slice_rotation_angle`,
`belt_slice_rotation_global`, `belt_preslice_global`,
`belt_frame_tilt_decouple`, `belt_frame_tilt_angle`,
`belt_support_floor_mode`, `belt_support_floor_offset`,
`belt_support_z_offset_mode`, `enable_belt_purge_tower`,
`first_layer_plane`, `first_layer_plane_offset`,
`first_layer_plane_thickness`, `build_plate_tilt_x/y`,
`gcode_back_transform`, `gcode_remap_x/y/z`, `preslice_remap_x/y/z`,
`preslice_remap_global`.
Process: `belt_purge_tower_width`, `leading_brim_length`,
`extra_brim_width`, brim type `leading_edge_only`. Object:
`belt_purge_tower_object`.

All of them have defaults that leave non-belt printers untouched, and
old `belt_support_floor_mode` values map to `none`.

</details>

<details>
<summary><b>Tests</b></summary>

`tests/libslic3r/test_belt_brim.cpp` and `test_arrange.cpp`, and belt
cases in `fff_print` (`test_print.cpp`, `test_skirt_brim.cpp`,
`test_gcodewriter.cpp`, `test_gcode_processor.cpp`): scarf gate, fan
band, gantry clearance, organic supports on the belt, brim with and
without the purge tower, apron widths, machine mapping at non-45°
angles, first-travel lift, start-G-code Z, arrange clamp and colour
grouping. All three suites pass on Linux, and the tree-wide profile
check passes.

</details>

---

## Starter profiles

Three vendors ship belt profiles. Belt mode needs the printer settings
at **Advanced** or above to show its group.

<img alt="advanced mode"
src="https://github.com/user-attachments/assets/7a519ce5-b3b5-400c-a914-4f208bb577b0"
/>

| Printer | Vendor bundle | Nozzles | Processes | Filaments |
|---|---|---|---|---|
| **Generic Belt Printer** (`MyBeltPrinter`) | Custom | 0.2, 0.4, 0.6,
0.8 | 0.20mm Standard, 0.12mm Fine | library |
| **BabyBelt Pro** (Printcepts) | Printcepts | 0.4 | 0.20mm Standard |
Generic PLA, Generic PETG, eSUN PLA |
| **IdeaFormer IR3 V2** | IdeaFormer | 0.4 | 0.20mm Standard | Generic
PLA, Generic PETG, eSUN PLA |

To set up a printer that isn't listed: pick **Generic Belt Printer**,
set the belt width and length, save the profile, then copy in your
machine's start/end G-code and limits and tune from there.

<img alt="generic belt printer"
src="https://github.com/user-attachments/assets/90134d55-d6fe-4dba-8695-5ea44e78ec2b"
/>

<details>
<summary>BabyBelt Pro</summary>
<img width="2467" height="1392" alt="BabyBelt Pro"
src="https://github.com/user-attachments/assets/2b448cee-339e-43c9-964b-1ee9044044c7"
/>
</details>

---

## Contributions

### @HarrierPigeon
***Majority of design & implementation***

I did most of the work here by myself with AI tools (primarily Claude,
some Codex.)
PRs #12733, #12998, #14385, #15155, #15361, #15156, #15526, #16127

### The @Unlayered3D Team
***Rotation-Mode Pipeline***

The initial version of this sheared the model in the pre-slice pipeline.
Talking with them convinced me to switch to the current
rotate->slice->unrotate-> remap & shear method, which had significant
immediate improvements. Working with them has been a blast.


### @tommasobbianchi
***IdeaFormer IR3V2 Profile, eSUN PLA Tuning, G-Code Render / Preview***

TommyB came in at the perfect time to help keep me motivated and
contributed several things I hadn't had the werewithal to implemement
yet. Without their contributions and encouragement, we wouldn't be here
yet.


### The BabyBelt Community

**BabyBelt Pro** — @rexit1982 for the profile, and @RobMink of
Printcepts for the printer and a lot of patient testing.

**Field reports** — Many members of the BabyBelt community helped,
testing on their machines, providing G-Code and examples of issues, and
encouraging me to keep working on it. Among them:
- @RobMink - creator of the BabyBelt
- @rexit1982 - initial BabyBelt Pro profile, bug hunter
- @shubhracc - found a *lot* of technical bugs
- @NeoDLC - bug hunter
and BabyBelt Discord members who found bugs & gave feedback in no
particular order:
- @horatio42 - also provided a build machine while mine was down
- @HotCubCar - requested belt printer brim support
- Swap_File
- @Nyctelios
- Sup
- @matschi140
- @Rise-Run
- @shooby-dooby

### OrcaSlicer Maintainers
**Generic belt printer** — @SoftFever
**Keeping on top of upstream** @RF47 & @HanifKoh
**Review and fixes** — @HanifKoh (#15685 and the review on this PR).
Among them: plates after the first printed off the bed on belts; painted
supports and seams ignored the belt transform; support generation failed
at a 90° tilt; the object table crashed on the Support column on every
printer because tilt keys were in the per-object tables; apron brims
printed in the wrong filament; `build_plate_tilt` went stale outside the
GUI; the CLI reserved a wipe tower on belts; every G-code file was
treated as belt G-code because the config block carries the angle; tests
didn't compile on Clang/MSVC; plus a long list of smaller clean-ups and
the review questions that led to the clearance check, the header length
fix, the retired floor modes and the removal of the diagnostic logging.

### Additional Thanks

A special thanks to LDO Motors, who provided equipment for validating
multicolor, and my wife, who not only put up with with this obsession,
and the addition of three belt printers to our home, but has encouraged
me to keep going ever since I started this project six months ago.

---

## Known Issues

- The purge prism's first tool is chosen by the shared `ToolOrdering`
logic; on some layouts the print opens on the wrong filament and makes
one extra change at the thin tip of the prism (the "cannot absorb the
full purge volume" warning at a low height).
- Colour grouping in arrange is a soft cost: when the belt is too short
for clean runs, colours overlap rather than spill onto another plate.
- Three OrcaFilamentLibrary filaments still carry `filament_z_hop` 0.4;
belt profiles override it to 0.

Also: slicing at an angle other than the machine's (decoupled frame
tilt) is supported but not something the starter profiles exercise.
This commit is contained in:
SoftFever
2026-10-09 15:24:47 +08:00
committed by GitHub
167 changed files with 12665 additions and 430 deletions
+14
View File
@@ -74,12 +74,26 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# The validator above is the nightly build of main, so it cannot slice profiles that use
# settings a PR adds to the engine: it reports their placeholders as undefined. A PR that
# changes src/ also runs Build all, whose Slice check runs this same sweep with the
# validator built from the PR, so the sweep below only runs for the other PRs.
- name: Detect engine changes
id: engine_changes
if: ${{ github.event_name == 'pull_request' }}
run: |
base=${{ github.event.pull_request.base.sha }}
if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator."
fi
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
if: ${{ steps.engine_changes.outputs.changed != 'true' }}
continue-on-error: true
run: |
set +e
@@ -88,6 +88,18 @@ struct NfpPConfig {
*/
bool explore_holes = false;
/**
* @brief Keep the final pile on the bin.
*
* The final alignment centres the pile on the alignment target. A target
* near an edge (a belt printer starts its parts at the leading end of the
* belt) would push part of a pile that is larger than the room around that
* point off the bed; with this set the pile stops at the edge instead, and a
* pile that does not fit along an axis is centred on it. Off by default, so
* the alignment of every other printer is unchanged.
*/
bool clamp_to_bin = false;
/**
* @brief If true, use all CPUs available. Run on a single core otherwise.
*/
@@ -1111,7 +1123,24 @@ private:
default: ; // DONT_ALIGN
}
auto d = cb - ci;
auto d = cb - ci;
// Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry
// their inflation, which is the margin left at the edge.
if (config_.clamp_to_bin) {
auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
if (hi - lo >= bin_hi - bin_lo)
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
if (lo + shift < bin_lo)
shift = bin_lo - lo;
if (hi + shift > bin_hi)
shift = bin_hi - hi;
return shift;
};
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
cb = ci + d;
}
// BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging?
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Belt temperature-tower asset generator (discrete-provini design).
A vertical temperature tower cannot be sliced on a belt printer, so lay a row of
DISCRETE provini (one per temperature) along the belt (designed Y) with a fixed
surface gap. Each provino is the chevron+arc unit (belt_temp_provino_unit.stl,
keel-first); its temperature is ENGRAVED upright into the 50 mm face — a raised
number would be an unsupported overhang on the belt. The C++ calib_temp belt branch
(Plater.cpp) injects one M104 per zone 70 layers INTO provino i:
print_z[i] = i * PITCH * cos(theta) + 70 * layer_height (theta = 45)
inside the body, not in the empty inter-provino gap (which has no sliced layers for
the event to attach to). PITCH below is the shared geometry contract with that code —
keep them in sync.
Generates one STL per filament temp range used by Temp_Calibration_Dlg.
"""
import numpy as np, trimesh, os
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties
from shapely.geometry import Polygon as ShPoly
from shapely.ops import unary_union
HERE = os.path.dirname(os.path.abspath(__file__))
UNIT = os.path.join(HERE, 'belt_temp_provino_unit.stl') # single provino, keel-first
SURF_GAP = 25.0 # surface-to-surface gap between provini (mm) — user spec
TEXT_H = 9.0
TEXT_DEPTH = 0.8 # engraving depth (numbers are CUT into the face, not raised:
# a raised number is an unsupported Y-overhang on the belt)
TEXT_OVERSHOOT = 0.6 # extra height poking out of the face for a clean boolean cut
# Temperature ranges (start, end) per filament family, 5 C step. File name encodes them.
RANGES = [(230,190),(270,230),(250,230),(280,240),(240,210),(320,280)]
unit = trimesh.load(UNIT)
dY = unit.bounds[1,1] - unit.bounds[0,1]
PITCH = dY + SURF_GAP # designed-Y pitch == C++ contract constant
print(f"unit dY={dY:.2f} PITCH={PITCH:.3f} (C++ contract: print_z[i]=i*{PITCH:.3f}*cos45)")
# 50 mm face normal (0,-1,1)/sqrt2 ; UPRIGHT basis u=+X det(+1) (verified non-mirrored)
n = np.array([0,-1,1.])/np.sqrt(2)
u = np.array([1,0,0.]); v = np.array([0,1,1.])/np.sqrt(2)
R = np.column_stack([u,v,n])
fn = unit.face_normals; fc = unit.triangles_center; fa = unit.area_faces
sel = (fn@n) > 0.9
face_c = (fc[sel]*fa[sel,None]).sum(0)/fa[sel].sum()
def text_mesh(s):
tp = TextPath((0,0), s, size=TEXT_H, prop=FontProperties(family='DejaVu Sans'))
rings = [ShPoly(p) for p in tp.to_polygons() if len(p)>=3]
rings.sort(key=lambda r:r.area, reverse=True)
used=[False]*len(rings); parts=[]
for i,o in enumerate(rings):
if used[i]: continue
holes=[]
for j in range(i+1,len(rings)):
if not used[j] and o.contains(rings[j]): holes.append(rings[j].exterior.coords); used[j]=True
parts.append(ShPoly(o.exterior.coords,holes)); used[i]=True
poly = unary_union(parts)
geoms = list(poly.geoms) if poly.geom_type=='MultiPolygon' else [poly]
m = trimesh.util.concatenate([trimesh.creation.extrude_polygon(g,height=TEXT_DEPTH+TEXT_OVERSHOOT) for g in geoms])
c = m.bounds.mean(axis=0); m.apply_translation([-c[0],-c[1],0]); return m
for t_start, t_end in RANGES:
temps = list(range(t_start, t_end-1, -5))
parts=[]
for i,T in enumerate(temps):
c = unit.copy(); c.apply_translation([0, i*PITCH, 0])
t = text_mesh(str(T)); M=np.eye(4); M[:3,:3]=R; t.apply_transform(M)
# place the text spanning from TEXT_DEPTH inside the face to TEXT_OVERSHOOT outside,
# then CUT it out of the provino (engrave) — no raised material, no Y-overhang.
t.apply_translation(face_c - n*TEXT_DEPTH + np.array([0,i*PITCH,0]))
c = trimesh.boolean.difference([c, t], engine='manifold')
parts.append(c)
asset = trimesh.util.concatenate(parts)
out = os.path.join(HERE, f"belt_temp_tower_{t_start}_{t_end}.stl")
asset.export(out)
dims = np.round(asset.bounds[1]-asset.bounds[0],1)
wt = all(p.is_watertight for p in parts)
print(f" {t_start}->{t_end}: {len(temps)} zones bbox={dims} watertight={wt} -> {os.path.basename(out)}")
+33 -1
View File
@@ -1,9 +1,13 @@
{
"name": "Custom Printer",
"version": "02.04.00.07",
"version": "02.04.00.08",
"force_update": "0",
"description": "My configurations",
"machine_model_list": [
{
"name": "Generic Belt Printer",
"sub_path": "machine/MyBeltPrinter.json"
},
{
"name": "Generic Klipper Printer",
"sub_path": "machine/MyKlipper.json"
@@ -50,6 +54,10 @@
"name": "0.08mm Extra Fine @MyKlipper",
"sub_path": "process/0.08mm Extra Fine @MyKlipper.json"
},
{
"name": "0.12mm Fine @MyBeltPrinter",
"sub_path": "process/0.12mm Fine @MyBeltPrinter.json"
},
{
"name": "0.12mm Fine @MyKlipper",
"sub_path": "process/0.12mm Fine @MyKlipper.json"
@@ -62,6 +70,10 @@
"name": "0.16mm Optimal @MyKlipper",
"sub_path": "process/0.16mm Optimal @MyKlipper.json"
},
{
"name": "0.20mm Standard @MyBeltPrinter",
"sub_path": "process/0.20mm Standard @MyBeltPrinter.json"
},
{
"name": "0.20mm Standard @MyKlipper",
"sub_path": "process/0.20mm Standard @MyKlipper.json"
@@ -262,6 +274,10 @@
"name": "MyKlipper 0.8 nozzle",
"sub_path": "machine/MyKlipper 0.8 nozzle.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "fdm_toolchanger_common",
"sub_path": "machine/fdm_toolchanger_common.json"
@@ -274,6 +290,22 @@
"name": "MyRRF 0.4 nozzle",
"sub_path": "machine/MyRRF 0.4 nozzle.json"
},
{
"name": "MyBeltPrinter 0.2 nozzle",
"sub_path": "machine/MyBeltPrinter 0.2 nozzle.json"
},
{
"name": "MyBeltPrinter 0.4 nozzle",
"sub_path": "machine/MyBeltPrinter 0.4 nozzle.json"
},
{
"name": "MyBeltPrinter 0.6 nozzle",
"sub_path": "machine/MyBeltPrinter 0.6 nozzle.json"
},
{
"name": "MyBeltPrinter 0.8 nozzle",
"sub_path": "machine/MyBeltPrinter 0.8 nozzle.json"
},
{
"name": "MyToolChanger 0.2 nozzle",
"sub_path": "machine/MyToolChanger 0.2 nozzle.json"
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,27 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.2 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "3w1uyJdmm14QhDnH",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"default_print_profile": "0.12mm Fine @MyBeltPrinter",
"nozzle_diameter": [
"0.2"
],
"max_layer_height": [
"0.16"
],
"min_layer_height": [
"0.04"
],
"printer_variant": "0.2",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}
@@ -0,0 +1,20 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "6nRHUtvJOUffocbu",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.4"
],
"printer_variant": "0.4",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}
@@ -0,0 +1,26 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.6 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "K0m9HbUNwKT4UCJV",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.6"
],
"max_layer_height": [
"0.4"
],
"min_layer_height": [
"0.12"
],
"printer_variant": "0.6",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}
@@ -0,0 +1,26 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.8 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "rHAweDz4eNwttPNA",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.8"
],
"max_layer_height": [
"0.6"
],
"min_layer_height": [
"0.2"
],
"printer_variant": "0.8",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Generic Belt Printer",
"model_id": "my_belt_01",
"nozzle_diameter": "0.4;0.2;0.6;0.8",
"machine_tech": "FFF",
"family": "MyPrinter",
"bed_model": "Custom_350_bed.stl",
"bed_texture": "orcaslicer_bed_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}
@@ -0,0 +1,95 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyBeltPrinter",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}
@@ -0,0 +1,20 @@
{
"type": "process",
"name": "0.12mm Fine @MyBeltPrinter",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "EugqqdLJ423bgEwN",
"instantiation": "true",
"layer_height": "0.12",
"initial_layer_print_height": "0.12",
"bottom_shell_layers": "5",
"top_shell_layers": "6",
"support_top_z_distance": "0.08",
"support_bottom_z_distance": "0.08",
"skirt_loops": "0",
"skirt_distance": "0",
"compatible_printers": [
"MyBeltPrinter 0.2 nozzle",
"MyBeltPrinter 0.4 nozzle"
]
}
@@ -0,0 +1,17 @@
{
"type": "process",
"name": "0.20mm Standard @MyBeltPrinter",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "YzCDAgH3uLOM53pF",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"skirt_loops": "0",
"skirt_distance": "0",
"compatible_printers": [
"MyBeltPrinter 0.4 nozzle",
"MyBeltPrinter 0.6 nozzle",
"MyBeltPrinter 0.8 nozzle"
]
}
+54
View File
@@ -0,0 +1,54 @@
{
"name": "IdeaFormer",
"version": "02.00.00.06",
"force_update": "0",
"description": "IdeaFormer belt printer configurations",
"machine_model_list": [
{
"name": "IdeaFormer IR3 V2",
"sub_path": "machine/IdeaFormer IR3 V2.json"
}
],
"process_list": [
{
"name": "fdm_process_common",
"sub_path": "process/fdm_process_common.json"
},
{
"name": "0.20mm Standard @IdeaFormer IR3 V2",
"sub_path": "process/0.20mm Standard @IdeaFormer IR3 V2.json"
}
],
"filament_list": [
{
"name": "Generic PLA @IdeaFormer IR3 V2",
"sub_path": "filament/Generic PLA @IdeaFormer IR3 V2.json"
},
{
"name": "eSUN PLA @IdeaFormer IR3 V2",
"sub_path": "filament/eSUN PLA @IdeaFormer IR3 V2.json"
},
{
"name": "Generic PETG @IdeaFormer IR3 V2",
"sub_path": "filament/Generic PETG @IdeaFormer IR3 V2.json"
}
],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_klipper_common",
"sub_path": "machine/fdm_klipper_common.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "IdeaFormer IR3 V2 0.4 nozzle",
"sub_path": "machine/IdeaFormer IR3 V2 0.4 nozzle.json"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

@@ -0,0 +1,77 @@
{
"type": "filament",
"name": "Generic PETG @IdeaFormer IR3 V2",
"inherits": "Generic PETG @System",
"from": "system",
"setting_id": "n4zaXcUUzTqAxq5f",
"instantiation": "true",
"filament_extruder_variant": [
"Direct Drive Standard"
],
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PETG"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PETG @IdeaFormer IR3 V2"
],
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"25"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"245"
],
"cool_plate_temp": [
"80"
],
"cool_plate_temp_initial_layer": [
"80"
],
"fan_min_speed": [
"40"
],
"fan_max_speed": [
"60"
],
"overhang_fan_threshold": [
"25%"
],
"overhang_fan_speed": [
"80"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"filament_retraction_length": [
"2"
],
"filament_retraction_speed": [
"40"
],
"filament_deretraction_speed": [
"40"
],
"filament_start_gcode": [
"; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
]
}
@@ -0,0 +1,65 @@
{
"type": "filament",
"name": "Generic PLA @IdeaFormer IR3 V2",
"inherits": "Generic PLA @System",
"from": "system",
"setting_id": "1xjycsEAFh6KQIhp",
"instantiation": "true",
"filament_extruder_variant": [
"Direct Drive Standard"
],
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PLA @IdeaFormer IR3 V2"
],
"nozzle_temperature": [
"215"
],
"hot_plate_temp": [
"75"
],
"hot_plate_temp_initial_layer": [
"75"
],
"cool_plate_temp": [
"75"
],
"cool_plate_temp_initial_layer": [
"75"
],
"textured_plate_temp": [
"75"
],
"textured_plate_temp_initial_layer": [
"75"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"filament_retraction_length": [
"1.5"
],
"filament_retraction_speed": [
"35"
],
"filament_deretraction_speed": [
"30"
],
"filament_start_gcode": [
"; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
]
}
@@ -0,0 +1,36 @@
{
"type": "filament",
"name": "eSUN PLA @IdeaFormer IR3 V2",
"inherits": "Generic PLA @IdeaFormer IR3 V2",
"from": "system",
"setting_id": "XqkviBmFHEglXueX",
"filament_id": "OFkrxQC4",
"instantiation": "true",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"eSUN"
],
"filament_settings_id": [
"eSUN PLA @IdeaFormer IR3 V2"
],
"nozzle_temperature_initial_layer": [
"200"
],
"nozzle_temperature": [
"200"
],
"enable_pressure_advance": [
"1"
],
"pressure_advance": [
"0.12"
],
"filament_max_volumetric_speed": [
"20"
]
}
@@ -0,0 +1,98 @@
{
"type": "machine",
"name": "IdeaFormer IR3 V2 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "MDQZgwRgg72lmjtu",
"instantiation": "true",
"printer_model": "IdeaFormer IR3 V2",
"printer_variant": "0.4",
"nozzle_diameter": [
"0.4"
],
"printable_area": [
"0x0",
"250x0",
"250x2000",
"0x2000"
],
"printable_height": "250",
"belt_printer_infinite_y": "1",
"thumbnails": [
"48x48/PNG",
"300x300/PNG"
],
"default_filament_profile": [
"Generic PLA @IdeaFormer IR3 V2"
],
"default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
"use_relative_e_distances": "1",
"machine_max_acceleration_extruding": [
"5000",
"5000"
],
"machine_max_acceleration_retracting": [
"1000",
"1000"
],
"machine_max_acceleration_travel": [
"9000",
"9000"
],
"machine_max_acceleration_x": [
"5000",
"5000"
],
"machine_max_acceleration_y": [
"5000",
"5000"
],
"machine_max_acceleration_z": [
"100",
"100"
],
"machine_max_jerk_x": [
"10",
"10"
],
"machine_max_jerk_y": [
"10",
"10"
],
"machine_max_jerk_z": [
"0.4",
"0.4"
],
"machine_max_speed_e": [
"60",
"60"
],
"machine_max_speed_x": [
"500",
"500"
],
"machine_max_speed_y": [
"500",
"500"
],
"machine_max_speed_z": [
"20",
"20"
],
"retraction_length": [
"2"
],
"retraction_speed": [
"40"
],
"deretraction_speed": [
"40"
],
"retract_lift_below": [
"300"
],
"machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n",
"machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n",
"machine_pause_gcode": "PAUSE",
"layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "IdeaFormer IR3 V2",
"model_id": "IdeaFormer_IR3_V2",
"nozzle_diameter": "0.4",
"machine_tech": "FFF",
"family": "IdeaFormer",
"bed_model": "",
"bed_texture": "",
"hotend_model": "",
"default_materials": "Generic PLA @IdeaFormer IR3 V2;Generic PETG @IdeaFormer IR3 V2"
}
@@ -0,0 +1,98 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"printer_extruder_id": [
"1"
],
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}
@@ -0,0 +1,140 @@
{
"type": "machine",
"name": "fdm_klipper_common",
"inherits": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"machine_max_acceleration_e": [
"5000",
"5000"
],
"machine_max_acceleration_extruding": [
"20000",
"20000"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"20000",
"20000"
],
"machine_max_acceleration_x": [
"20000",
"20000"
],
"machine_max_acceleration_y": [
"20000",
"20000"
],
"machine_max_acceleration_z": [
"500",
"200"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"machine_max_jerk_e": [
"2.5",
"2.5"
],
"machine_max_jerk_x": [
"9",
"9"
],
"machine_max_jerk_y": [
"9",
"9"
],
"machine_max_jerk_z": [
"0.2",
"0.4"
],
"machine_min_extruding_rate": [
"0",
"0"
],
"machine_min_travel_rate": [
"0",
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"printer_settings_id": "",
"printer_technology": "FFF",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"1"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"0.8"
],
"retract_length_toolchange": [
"2"
],
"z_hop": [
"0.4"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"30"
],
"deretraction_speed": [
"30"
],
"z_hop_types": "Normal Lift",
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyKlipper",
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
"machine_end_gcode": "PRINT_END",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "PAUSE",
"scan_first_layer": "0",
"nozzle_type": "undefine",
"auxiliary_fan": "0"
}
@@ -0,0 +1,118 @@
{
"type": "machine",
"name": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"printer_technology": "FFF",
"deretraction_speed": [
"40"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"gcode_flavor": "marlin",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"10000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_x": [
"10000"
],
"machine_max_acceleration_y": [
"10000"
],
"machine_max_acceleration_z": [
"500"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"10"
],
"machine_max_jerk_e": [
"5"
],
"machine_max_jerk_x": [
"8"
],
"machine_max_jerk_y": [
"8"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_min_extruding_rate": [
"0"
],
"machine_min_travel_rate": [
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"nozzle_diameter": [
"0.4"
],
"printer_settings_id": "",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"2"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"1"
],
"retract_length_toolchange": [
"1"
],
"z_hop": [
"0"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"60"
],
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_print_profile": "",
"machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
"machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "M601"
}
@@ -0,0 +1,23 @@
{
"type": "process",
"name": "0.20mm Standard @IdeaFormer IR3 V2",
"inherits": "fdm_process_common",
"from": "system",
"setting_id": "91atcIwv5728phqX",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"initial_layer_line_width": "0.42",
"wall_loops": "2",
"reduce_infill_retraction": "1",
"detect_overhang_wall": "1",
"skirt_loops": "0",
"skirt_distance": "0",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "200",
"support_base_pattern": "rectilinear",
"support_interface_pattern": "rectilinear",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
]
}
@@ -0,0 +1,106 @@
{
"type": "process",
"name": "fdm_process_common",
"from": "system",
"instantiation": "false",
"reduce_crossing_wall": "0",
"max_travel_detour_distance": "0",
"bottom_surface_pattern": "monotonic",
"bottom_shell_thickness": "0",
"bridge_speed": "50",
"brim_width": "5",
"brim_object_gap": "0.1",
"compatible_printers": [],
"compatible_printers_condition": "",
"print_sequence": "by layer",
"default_acceleration": "1000",
"initial_layer_acceleration": "500",
"top_surface_acceleration": "1000",
"travel_acceleration": "1000",
"inner_wall_acceleration": "1000",
"outer_wall_acceleration": "700",
"bridge_no_support": "0",
"draft_shield": "disabled",
"elefant_foot_compensation": "0",
"enable_arc_fitting": "0",
"wall_infill_order": "inner wall/outer wall/infill",
"infill_direction": "45",
"sparse_infill_density": "15%",
"sparse_infill_pattern": "crosshatch",
"initial_layer_print_height": "0.2",
"infill_combination": "0",
"infill_wall_overlap": "25%",
"interface_shells": "0",
"ironing_flow": "10%",
"ironing_spacing": "0.15",
"ironing_speed": "30",
"ironing_type": "no ironing",
"reduce_infill_retraction": "1",
"filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
"detect_overhang_wall": "1",
"slowdown_for_curled_perimeters": "1",
"overhang_1_4_speed": "0",
"overhang_2_4_speed": "50",
"overhang_3_4_speed": "30",
"overhang_4_4_speed": "10",
"line_width": "110%",
"inner_wall_line_width": "110%",
"outer_wall_line_width": "100%",
"top_surface_line_width": "93.75%",
"sparse_infill_line_width": "110%",
"initial_layer_line_width": "120%",
"internal_solid_infill_line_width": "120%",
"support_line_width": "96%",
"wall_loops": "3",
"print_settings_id": "",
"raft_layers": "0",
"seam_position": "aligned",
"skirt_distance": "2",
"skirt_height": "3",
"min_skirt_length": "4",
"skirt_loops": "0",
"minimum_sparse_infill_area": "15",
"spiral_mode": "0",
"standby_temperature_delta": "-5",
"enable_support": "0",
"resolution": "0.012",
"support_type": "normal(auto)",
"support_on_build_plate_only": "0",
"support_top_z_distance": "0.2",
"support_bottom_z_distance": "0.2",
"support_filament": "0",
"support_interface_loop_pattern": "0",
"support_interface_filament": "0",
"support_interface_top_layers": "2",
"support_interface_bottom_layers": "2",
"support_interface_spacing": "0.5",
"support_interface_speed": "80",
"support_base_pattern": "default",
"support_base_pattern_spacing": "2.5",
"support_speed": "150",
"support_threshold_angle": "30",
"support_object_xy_distance": "0.35",
"tree_support_branch_angle": "30",
"tree_support_wall_count": "0",
"detect_thin_wall": "0",
"top_surface_pattern": "monotonicline",
"top_shell_thickness": "0.8",
"enable_prime_tower": "1",
"wipe_tower_no_sparse_layers": "0",
"prime_tower_width": "60",
"xy_hole_compensation": "0",
"xy_contour_compensation": "0",
"layer_height": "0.2",
"bottom_shell_layers": "3",
"top_shell_layers": "4",
"bridge_flow": "1",
"initial_layer_speed": "45",
"initial_layer_infill_speed": "45",
"outer_wall_speed": "45",
"inner_wall_speed": "80",
"sparse_infill_speed": "150",
"internal_solid_infill_speed": "150",
"top_surface_speed": "50",
"gap_infill_speed": "30",
"travel_speed": "200"
}
+54
View File
@@ -0,0 +1,54 @@
{
"name": "Printcepts",
"version": "01.00.00.04",
"force_update": "0",
"description": "Printcepts belt printer configurations",
"machine_model_list": [
{
"name": "BabyBelt Pro",
"sub_path": "machine/BabyBelt Pro.json"
}
],
"process_list": [
{
"name": "fdm_process_common",
"sub_path": "process/fdm_process_common.json"
},
{
"name": "0.20mm Standard @BabyBelt Pro",
"sub_path": "process/0.20mm Standard @BabyBelt Pro.json"
}
],
"filament_list": [
{
"name": "Generic PLA @BabyBelt Pro",
"sub_path": "filament/Generic PLA @BabyBelt Pro.json"
},
{
"name": "eSUN PLA @BabyBelt Pro",
"sub_path": "filament/eSUN PLA @BabyBelt Pro.json"
},
{
"name": "Generic PETG @BabyBelt Pro",
"sub_path": "filament/Generic PETG @BabyBelt Pro.json"
}
],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_klipper_common",
"sub_path": "machine/fdm_klipper_common.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "BabyBelt Pro 0.4 nozzle",
"sub_path": "machine/BabyBelt Pro 0.4 nozzle.json"
}
]
}
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="95.0mm" height="500.0mm" viewBox="0 0 95.0 500.0" preserveAspectRatio="xMidYMid meet">
<!-- Printcepts BabyBelt Pro bed texture: 95 x 500 mm belt plate. -->
<!-- Transparent plate; green (#195F30) BabyBelt Pro logo centered along X, near the bottom edge. -->
<rect x="0" y="0" width="95.0" height="500.0" fill="none"/>
<g transform="translate(14.2500,436.3488) scale(0.067538)">
<g transform="translate(-11.000000,692.938562) scale(0.100000,-0.100000)"
fill="#195F30" stroke="none">
<path d="M1963 5604 l-1423 -1324 0 -2050 0 -2050 443 0 c244 0 741 3 1105 7
l662 6 0 746 c-1 575 -4 768 -14 841 -47 324 -179 486 -473 581 -40 12 -73 26
-73 30 0 4 32 17 72 29 212 64 333 166 378 320 35 121 38 191 32 868 l-5 662
-629 0 c-395 0 -628 4 -628 10 0 5 635 601 1410 1325 776 724 1410 1318 1410
1321 0 2 -190 4 -422 3 l-423 0 -1422 -1325z m-334 -2029 c143 -16 174 -96
173 -446 -2 -411 -24 -458 -224 -462 l-93 -2 -3 450 c-1 248 0 456 3 463 3 9
18 12 47 8 24 -3 67 -8 97 -11z m-4 -1556 c160 -29 173 -62 182 -469 9 -455
-14 -593 -108 -641 -39 -19 -193 -44 -210 -33 -10 6 -13 1147 -3 1157 6 6 45
2 139 -14z"/>
<path d="M3464 5979 c-142 -132 -263 -245 -268 -250 -6 -5 69 -9 190 -9 l199
1 268 249 267 250 -198 0 -198 0 -260 -241z"/>
<path d="M3650 5649 c-135 -126 -254 -238 -265 -249 -19 -20 -18 -20 177 -20
l197 0 228 211 c125 116 246 229 268 250 l40 39 -200 -1 -200 0 -245 -230z"/>
<path d="M2537 5089 c-101 -24 -204 -105 -251 -197 -96 -190 -19 -420 172
-514 l67 -33 2670 0 2670 0 57 27 c74 34 146 107 184 183 43 88 43 230 0 322
-35 76 -113 153 -193 191 l-58 27 -2640 2 c-1513 0 -2656 -3 -2678 -8z m5063
-77 c-57 -37 -118 -111 -140 -168 -31 -82 -25 -206 12 -279 26 -49 93 -121
133 -143 15 -8 -640 -11 -2410 -11 l-2430 0 30 21 c200 146 201 425 1 569
l-39 29 2434 -1 c2263 0 2432 -1 2409 -17z m-4890 -43 c270 -122 185 -526
-109 -522 -257 2 -370 324 -170 485 74 60 194 76 279 37z m5201 -12 c94 -55
140 -135 140 -242 0 -285 -393 -374 -517 -117 -26 54 -30 162 -9 219 28 74 97
139 173 164 53 17 166 4 213 -24z"/>
<path d="M2917 3973 c-4 -174 -7 -550 -7 -835 l0 -518 326 0 326 0 -7 150 -7
150 110 0 110 0 11 -32 c5 -18 26 -86 46 -150 l36 -118 325 0 c179 0 323 4
320 9 -3 4 -155 374 -337 822 -182 448 -333 820 -336 827 -4 9 -105 12 -457
12 l-453 0 -6 -317z m773 -745 c0 -5 -47 -8 -104 -8 l-103 0 -7 92 c-3 50 -6
202 -5 337 l1 246 109 -330 c60 -181 109 -333 109 -337z"/>
<path d="M4680 3455 l0 -835 448 0 c693 1 885 16 985 80 99 63 126 132 134
340 12 327 -44 405 -342 476 -28 7 -27 8 25 19 199 42 255 95 267 248 11 146
-32 285 -110 353 -145 126 -329 153 -1049 154 l-358 0 0 -835z m846 520 c36
-23 44 -54 44 -162 0 -152 -29 -183 -170 -183 l-40 0 0 186 0 187 71 -6 c39
-3 82 -13 95 -22z m4 -625 c33 -18 40 -52 40 -208 0 -200 -9 -214 -143 -228
l-67 -7 0 233 0 233 74 -6 c41 -3 84 -10 96 -17z"/>
<path d="M6150 4286 c0 -3 131 -242 290 -531 l290 -526 0 -304 0 -305 385 0
385 0 0 299 0 299 305 533 305 534 -377 3 c-207 1 -381 -2 -385 -6 -16 -16
-110 -258 -172 -444 l-62 -187 -18 77 c-18 75 -139 450 -171 525 l-15 37 -380
0 c-209 0 -380 -2 -380 -4z"/>
<path d="M2910 1355 l0 -1185 830 0 830 0 0 240 0 240 -350 0 -350 0 0 255 0
255 300 0 300 0 0 230 0 230 -300 0 -300 0 0 220 0 220 320 0 320 0 0 240 0
240 -800 0 -800 0 0 -1185z"/>
<path d="M4680 1355 l0 -1185 775 0 775 0 0 240 0 240 -295 0 -295 0 0 945 0
945 -480 0 -480 0 0 -1185z"/>
<path d="M5800 2300 l0 -240 280 0 280 0 0 -945 0 -945 480 0 480 0 0 945 0
945 285 0 285 0 0 240 0 240 -1045 0 -1045 0 0 -240z"/>
<path d="M8032 1358 l-2 -1188 1008 1 c621 1 971 5 912 10 -309 27 -631 139
-885 306 -593 391 -984 1122 -1025 1918 -4 77 -8 -394 -8 -1047z"/>
<path d="M7441 1934 c-43 -36 -59 -70 -70 -148 -18 -124 16 -252 76 -291 32
-21 226 -33 328 -20 114 14 161 97 153 269 -5 96 -24 151 -68 191 -20 18 -39
20 -205 23 l-182 3 -32 -27z m389 -199 c7 -8 10 -22 6 -30 -4 -13 -34 -15
-186 -15 -189 0 -202 3 -186 45 8 22 348 22 366 0z"/>
<path d="M7450 1267 c-14 -6 -35 -32 -47 -57 -21 -41 -23 -58 -23 -222 l0
-178 270 0 270 0 0 105 0 105 -121 0 -120 0 3 28 3 27 118 3 117 3 0 99 0 100
-113 0 c-121 0 -138 -7 -162 -65 -8 -19 -9 -19 -12 2 -10 55 -116 84 -183 50z
m134 -203 c15 -38 8 -44 -54 -44 -62 0 -69 6 -54 44 9 23 99 23 108 0z"/>
<path d="M466 1193 l-29 -43 -163 0 -164 0 0 -235 0 -235 165 0 165 0 27 -42
28 -42 3 163 c1 89 1 233 0 319 l-3 157 -29 -42z"/>
<path d="M7443 620 c-48 -20 -58 -60 -61 -262 l-4 -188 271 0 271 0 0 110 0
110 -110 0 -110 0 0 70 c0 76 -21 145 -51 160 -22 12 -176 12 -206 0z m161
-186 c15 -39 8 -44 -64 -44 -72 0 -79 5 -64 44 9 23 119 23 128 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,77 @@
{
"type": "filament",
"name": "Generic PETG @BabyBelt Pro",
"inherits": "Generic PETG @System",
"from": "system",
"setting_id": "gCzHpDNgVwQR6tgk",
"instantiation": "true",
"filament_extruder_variant": [
"Direct Drive Standard"
],
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PETG"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PETG @BabyBelt Pro"
],
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"25"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"245"
],
"cool_plate_temp": [
"80"
],
"cool_plate_temp_initial_layer": [
"80"
],
"fan_min_speed": [
"40"
],
"fan_max_speed": [
"60"
],
"overhang_fan_threshold": [
"25%"
],
"overhang_fan_speed": [
"80"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"filament_retraction_length": [
"2"
],
"filament_retraction_speed": [
"40"
],
"filament_deretraction_speed": [
"40"
],
"filament_start_gcode": [
"; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
]
}
@@ -0,0 +1,65 @@
{
"type": "filament",
"name": "Generic PLA @BabyBelt Pro",
"inherits": "Generic PLA @System",
"from": "system",
"setting_id": "24PpcnhVx9v5f4fD",
"instantiation": "true",
"filament_extruder_variant": [
"Direct Drive Standard"
],
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PLA @BabyBelt Pro"
],
"nozzle_temperature": [
"215"
],
"hot_plate_temp": [
"75"
],
"hot_plate_temp_initial_layer": [
"75"
],
"cool_plate_temp": [
"75"
],
"cool_plate_temp_initial_layer": [
"75"
],
"textured_plate_temp": [
"75"
],
"textured_plate_temp_initial_layer": [
"75"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"filament_retraction_length": [
"1.5"
],
"filament_retraction_speed": [
"35"
],
"filament_deretraction_speed": [
"30"
],
"filament_start_gcode": [
"; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
]
}
@@ -0,0 +1,36 @@
{
"type": "filament",
"name": "eSUN PLA @BabyBelt Pro",
"inherits": "Generic PLA @BabyBelt Pro",
"from": "system",
"setting_id": "EH3X7oE0DU5tSpjW",
"filament_id": "OFkrxQC4",
"instantiation": "true",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"eSUN"
],
"filament_settings_id": [
"eSUN PLA @BabyBelt Pro"
],
"nozzle_temperature_initial_layer": [
"200"
],
"nozzle_temperature": [
"200"
],
"enable_pressure_advance": [
"1"
],
"pressure_advance": [
"0.12"
],
"filament_max_volumetric_speed": [
"20"
]
}
@@ -0,0 +1,88 @@
{
"type": "machine",
"name": "BabyBelt Pro 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "34OWINlJpJgA9DwQ",
"instantiation": "true",
"printer_model": "BabyBelt Pro",
"printer_variant": "0.4",
"nozzle_diameter": [
"0.4"
],
"default_filament_profile": [
"Generic PLA @BabyBelt Pro"
],
"default_print_profile": "0.20mm Standard @BabyBelt Pro",
"printable_area": [
"0x0",
"95x0",
"95x500",
"0x500"
],
"printable_height": "100",
"best_object_pos": "0.5,0.05",
"nozzle_type": [
"hardened_steel"
],
"printer_extruder_id": [
"1"
],
"printer_extruder_variant": [
"Direct Drive Standard"
],
"thumbnails": [
"48x48/PNG",
"300x300/PNG"
],
"machine_max_acceleration_e": [
"500",
"5000"
],
"machine_max_acceleration_extruding": [
"500",
"20000"
],
"machine_max_acceleration_retracting": [
"500",
"5000"
],
"machine_max_acceleration_x": [
"500",
"20000"
],
"machine_max_acceleration_y": [
"500",
"20000"
],
"machine_max_junction_deviation": [
"0.01",
"0.01"
],
"machine_max_speed_x": [
"50",
"200"
],
"machine_max_speed_y": [
"50",
"200"
],
"machine_max_speed_z": [
"5",
"12"
],
"retraction_length": [
"1.5"
],
"retraction_speed": [
"20"
],
"deretraction_speed": [
"25"
],
"retract_lift_enforce": [
"Top and Bottom"
],
"support_chamber_temp_control": "0",
"machine_start_gcode": ";Start GCode\nPRINT_START ANGLE=[belt_slice_rotation_angle] EXTRUDER=[nozzle_temperature_initial_layer] BED=[hot_plate_temp_initial_layer] MATERIAL=[filament_type]\n"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "BabyBelt Pro",
"model_id": "Printcepts_BabyBelt_Pro",
"nozzle_diameter": "0.4",
"machine_tech": "FFF",
"family": "Printcepts",
"bed_model": "",
"bed_texture": "BabyBelt Pro_bed_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @BabyBelt Pro;Generic PETG @BabyBelt Pro"
}
@@ -0,0 +1,98 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @BabyBelt Pro",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"printer_extruder_id": [
"1"
],
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}
@@ -0,0 +1,140 @@
{
"type": "machine",
"name": "fdm_klipper_common",
"inherits": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"machine_max_acceleration_e": [
"5000",
"5000"
],
"machine_max_acceleration_extruding": [
"20000",
"20000"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"20000",
"20000"
],
"machine_max_acceleration_x": [
"20000",
"20000"
],
"machine_max_acceleration_y": [
"20000",
"20000"
],
"machine_max_acceleration_z": [
"500",
"200"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"machine_max_jerk_e": [
"2.5",
"2.5"
],
"machine_max_jerk_x": [
"9",
"9"
],
"machine_max_jerk_y": [
"9",
"9"
],
"machine_max_jerk_z": [
"0.2",
"0.4"
],
"machine_min_extruding_rate": [
"0",
"0"
],
"machine_min_travel_rate": [
"0",
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"printer_settings_id": "",
"printer_technology": "FFF",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"1"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"0.8"
],
"retract_length_toolchange": [
"2"
],
"z_hop": [
"0.4"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"30"
],
"deretraction_speed": [
"30"
],
"z_hop_types": "Normal Lift",
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyKlipper",
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
"machine_end_gcode": "PRINT_END",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "PAUSE",
"scan_first_layer": "0",
"nozzle_type": "undefine",
"auxiliary_fan": "0"
}
@@ -0,0 +1,118 @@
{
"type": "machine",
"name": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"printer_technology": "FFF",
"deretraction_speed": [
"40"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"gcode_flavor": "marlin",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"10000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_x": [
"10000"
],
"machine_max_acceleration_y": [
"10000"
],
"machine_max_acceleration_z": [
"500"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"10"
],
"machine_max_jerk_e": [
"5"
],
"machine_max_jerk_x": [
"8"
],
"machine_max_jerk_y": [
"8"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_min_extruding_rate": [
"0"
],
"machine_min_travel_rate": [
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"nozzle_diameter": [
"0.4"
],
"printer_settings_id": "",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"2"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"1"
],
"retract_length_toolchange": [
"1"
],
"z_hop": [
"0"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"60"
],
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_print_profile": "",
"machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
"machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "M601"
}
@@ -0,0 +1,23 @@
{
"type": "process",
"name": "0.20mm Standard @BabyBelt Pro",
"inherits": "fdm_process_common",
"from": "system",
"setting_id": "JGfGtqX6CWjCt437",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"initial_layer_line_width": "0.42",
"wall_loops": "2",
"reduce_infill_retraction": "1",
"detect_overhang_wall": "1",
"skirt_loops": "0",
"skirt_distance": "0",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "200",
"support_base_pattern": "rectilinear",
"support_interface_pattern": "rectilinear",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
]
}
@@ -0,0 +1,106 @@
{
"type": "process",
"name": "fdm_process_common",
"from": "system",
"instantiation": "false",
"reduce_crossing_wall": "0",
"max_travel_detour_distance": "0",
"bottom_surface_pattern": "monotonic",
"bottom_shell_thickness": "0",
"bridge_speed": "50",
"brim_width": "5",
"brim_object_gap": "0.1",
"compatible_printers": [],
"compatible_printers_condition": "",
"print_sequence": "by layer",
"default_acceleration": "1000",
"initial_layer_acceleration": "500",
"top_surface_acceleration": "1000",
"travel_acceleration": "1000",
"inner_wall_acceleration": "1000",
"outer_wall_acceleration": "700",
"bridge_no_support": "0",
"draft_shield": "disabled",
"elefant_foot_compensation": "0",
"enable_arc_fitting": "0",
"wall_infill_order": "inner wall/outer wall/infill",
"infill_direction": "45",
"sparse_infill_density": "15%",
"sparse_infill_pattern": "crosshatch",
"initial_layer_print_height": "0.2",
"infill_combination": "0",
"infill_wall_overlap": "25%",
"interface_shells": "0",
"ironing_flow": "10%",
"ironing_spacing": "0.15",
"ironing_speed": "30",
"ironing_type": "no ironing",
"reduce_infill_retraction": "1",
"filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
"detect_overhang_wall": "1",
"slowdown_for_curled_perimeters": "1",
"overhang_1_4_speed": "0",
"overhang_2_4_speed": "50",
"overhang_3_4_speed": "30",
"overhang_4_4_speed": "10",
"line_width": "110%",
"inner_wall_line_width": "110%",
"outer_wall_line_width": "100%",
"top_surface_line_width": "93.75%",
"sparse_infill_line_width": "110%",
"initial_layer_line_width": "120%",
"internal_solid_infill_line_width": "120%",
"support_line_width": "96%",
"wall_loops": "3",
"print_settings_id": "",
"raft_layers": "0",
"seam_position": "aligned",
"skirt_distance": "2",
"skirt_height": "3",
"min_skirt_length": "4",
"skirt_loops": "0",
"minimum_sparse_infill_area": "15",
"spiral_mode": "0",
"standby_temperature_delta": "-5",
"enable_support": "0",
"resolution": "0.012",
"support_type": "normal(auto)",
"support_on_build_plate_only": "0",
"support_top_z_distance": "0.2",
"support_bottom_z_distance": "0.2",
"support_filament": "0",
"support_interface_loop_pattern": "0",
"support_interface_filament": "0",
"support_interface_top_layers": "2",
"support_interface_bottom_layers": "2",
"support_interface_spacing": "0.5",
"support_interface_speed": "80",
"support_base_pattern": "default",
"support_base_pattern_spacing": "2.5",
"support_speed": "150",
"support_threshold_angle": "30",
"support_object_xy_distance": "0.35",
"tree_support_branch_angle": "30",
"tree_support_wall_count": "0",
"detect_thin_wall": "0",
"top_surface_pattern": "monotonicline",
"top_shell_thickness": "0.8",
"enable_prime_tower": "1",
"wipe_tower_no_sparse_layers": "0",
"prime_tower_width": "60",
"xy_hole_compensation": "0",
"xy_contour_compensation": "0",
"layer_height": "0.2",
"bottom_shell_layers": "3",
"top_shell_layers": "4",
"bridge_flow": "1",
"initial_layer_speed": "45",
"initial_layer_infill_speed": "45",
"outer_wall_speed": "45",
"inner_wall_speed": "80",
"sparse_infill_speed": "150",
"internal_solid_infill_speed": "150",
"top_surface_speed": "50",
"gap_infill_speed": "30",
"travel_speed": "200"
}
+1
View File
@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
+2 -1
View File
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -85,7 +86,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
+1
View File
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
+1
View File
@@ -41,6 +41,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
+1
View File
@@ -29,6 +29,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
+2 -1
View File
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -87,7 +88,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
+1
View File
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
+1
View File
@@ -44,6 +44,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
+5
View File
@@ -167,6 +167,11 @@ OBSOLETE_KEYS = {
"filament_load_time", "filament_unload_time", "smooth_coefficient",
"overhang_totally_speed", "silent_mode", "overhang_speed_classic",
"anisotropic_surfaces",
# Belt printer options retired before the feature shipped (#16236).
"belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z",
"preslice_remap_global", "belt_support_z_offset_mode", "first_layer_plane",
"first_layer_plane_offset", "belt_preslice_global", "gcode_back_transform",
"belt_support_floor_mode", "first_layer_plane_thickness",
}
# Keys renamed at some point, whose old and new spellings must never co-exist:
+7 -1
View File
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
# wxInspector 1.0.0 installs its headers but accidentally declares the
# INSTALL_INTERFACE include directory PRIVATE, so its imported target does
# not expose them to consumers. Restore the package prefix include path until
# the upstream export is fixed.
get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
# wxInspector's exported interface names the release wxWidgets import
# libraries, which a Debug build cannot link. wx is linked above instead.
@@ -186,7 +192,7 @@ endif ()
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
if (SLIC3R_GUI)
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
target_link_libraries(OrcaSlicer libslic3r_gui)
target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
if (MSVC)
# Generate debug symbols even in release mode.
target_link_options(OrcaSlicer PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
+46 -13
View File
@@ -3435,9 +3435,14 @@ int CLI::run(int argc, char **argv)
max_self_index = std::max(max_self_index, v);
min_self_index = std::min(min_self_index, v);
}
if (max_self_index > filament_count || min_self_index < 1) {
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
% min_self_index % max_self_index % filament_count;
// And a project saved with FEWER filaments than are now loaded (a
// one-filament project sliced with two --load-filaments) leaves the tables half filled:
// the variant matching below then reads past filament_extruder_variant and
// set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
|| (int) filament_self_index_opt->values.size() < filament_count) {
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
% min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
need_regenerate_self_index = true;
}
}
@@ -3528,6 +3533,10 @@ int CLI::run(int argc, char **argv)
std::vector<std::string>& filament_variants = curr_variant_opt->values;
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
}
// See the filament_self_index note above: one variant per filament for
// the filaments the project did not know about.
if ((int) curr_variant_opt->values.size() < filament_count)
curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
const ConfigOptionStrings *new_variant_opt = dynamic_cast<const ConfigOptionStrings*>(config.option("filament_extruder_variant", true));
std::vector<int> new_variant_indice;
@@ -3536,7 +3545,7 @@ int CLI::run(int argc, char **argv)
for (int i = 0; i < new_variant_count; i++)
{
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
{
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
new_variant_indice[i] = j;
@@ -3588,7 +3597,18 @@ int CLI::run(int argc, char **argv)
ConfigOptionVectorBase* opt_vec_dst = static_cast<ConfigOptionVectorBase*>(opt);
const ConfigOptionVectorBase* opt_vec_src = static_cast<const ConfigOptionVectorBase*>(source_opt);
//set with index
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
try {
// A project with fewer filaments than are loaded: grow the
// destination to the filament's slot first (set_with_restore_2 only restores).
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
} catch (const std::exception &ex) { // Was an uncaught abort
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
}
continue;
@@ -3637,7 +3657,16 @@ int CLI::run(int argc, char **argv)
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
std::vector<int> temp_variant_indice;
temp_variant_indice.resize(new_variant_count, -1);
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
try {
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
} catch (const std::exception &ex) { // Was an uncaught abort
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
if (opt_key == "filament_extruder_variant")
new_variant_counts[filament_index - 1] = opt_vec_src->size();
@@ -4153,6 +4182,10 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
}
// Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
const ConfigOptionBool* belt_printer_opt = m_print_config.option<ConfigOptionBool>("belt_printer");
const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
auto timelapse_type_opt = m_print_config.option("timelapse_type");
bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
@@ -4390,11 +4423,11 @@ int CLI::run(int argc, char **argv)
}
};
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
if (!print_config.has("wipe_tower_x")) {
if (is_belt_printer || !print_config.has("wipe_tower_x")) {
plate_obj_size_info.has_wipe_tower = false;
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
return;
@@ -5265,7 +5298,7 @@ int CLI::run(int argc, char **argv)
}
}
if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
@@ -5417,7 +5450,7 @@ int CLI::run(int argc, char **argv)
bool is_seq_print = false;
get_print_sequence(cur_plate, m_print_config, is_seq_print);
if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
@@ -5585,7 +5618,7 @@ int CLI::run(int argc, char **argv)
};
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
if (plate_needs_wipe_tower(max_filament_count))
if (!is_belt_printer && plate_needs_wipe_tower(max_filament_count))
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
@@ -5692,7 +5725,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
}
if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
float x;
float y;
if (duplicate_count > 0) {
@@ -6317,7 +6350,7 @@ int CLI::run(int argc, char **argv)
// The stored (or default) tower position may not fit the tower these plates
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
// reload clamp and fit every plate's tower into the printable area first.
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
if (!is_belt_printer && m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
continue;
+29 -11
View File
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
{
const Vec2d center = printable_area_center(cfg);
const bool belt = cfg.opt_bool("belt_printer");
std::vector<Vec2d> cube_mins;
if (by_object) {
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
} else if (belt) {
// A belt object's slicing Z starts at the belt below its leading end, well below its first
// printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
// behind the other along the belt, the second on filament 2, give the one filament change
// instead (the purge prism is an object the GUI adds, so there is no tower to place).
cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
} else {
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
if (!cfg.opt_string("wrapping_detection_gcode").empty())
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
if (belt && !by_object) {
// The second cube along the belt is on filament 2 (see cube_mins above).
if (&cube_min == &cube_mins.back())
obj->config.set_key_value("extruder", new ConfigOptionInt(2));
} else {
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
}
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
}
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
const std::string filament_name = bundle.filaments.get_selected_preset_name();
const std::string what = "Printer \"" + printer + "\"";
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
// A belt printer has no wipe tower (it purges into a prism object on the belt), so its
// filament change is the plain tool change set_extruder() emits rather than the tower's block.
const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
++failures;
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
// The filament change never rode the tower, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
else if (out.find(marker) == std::string::npos) {
// The filament change never fired, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
<< (belt ? "T1" : "CP TOOLCHANGE START") << ")";
++failures;
}
cover(bundle.prints.get_selected_preset());
+84 -9
View File
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
if (params.is_seq_print) {
if (params.is_belt) {
// Pack from the end of the belt that prints first, and keep the pile on the
// bed when it is larger than the room around that end.
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
PConf::Alignment::BOTTOM_RIGHT;
pcfg.clamp_to_bin = true;
}
else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
}
@@ -444,6 +452,50 @@ protected:
return bindist;
}
// Belt printers pack from the end of the belt that prints first, and a corner
// packer's checks (pile inside the bin, pack origin) apply to them as well.
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
// Position along the belt in print order: increasing from the end that prints first.
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
// The corner of the bin the belt pile grows from.
Box::PointType belt_origin() const
{
const Box bb = sl::boundingBox(m_bin);
auto o = bb.minCorner();
if (params.belt_reversed) {
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
else setY(o, getY(bb.maxCorner()));
}
return o;
}
// An item's far edge in print order is what it costs (so a row fills across the
// belt before the pile advances), with a slight pull toward the near lateral
// edge and the same penalty as the bottom-left heuristic for sitting outside
// the corner.
double dist_along_belt(const Box &ibb)
{
const Box bin = sl::boundingBox(m_bin);
const int l = 1 - params.belt_axis;
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
double d = belt_end(ibb) - belt_start(bin);
d += lat < 0 ? 10 * -lat : 0.1 * lat;
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
d += 10 * -behind;
return norm(d);
}
double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
{
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
}
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
{
double bindist = 0;
@@ -533,8 +585,8 @@ protected:
// The smalles distance from the arranged pile center:
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
double bindist = corner_bindist(ibb, origin_pack);
score = 0.2 * dist + 0.8 * bindist;
}
else {
@@ -591,8 +643,8 @@ protected:
break;
}
case LAST_BIG_ITEM: {
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
score = corner_bindist(ibb, origin_pack);
}
else {
if (m_pilebb.defined)
@@ -607,8 +659,8 @@ protected:
// already processed bigger items.
// No need to play around with the anchor points, the center will be
// just fine for small items
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing())
score = corner_bindist(ibb, origin_pack);
else {
// Align mainly around existing items
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
@@ -709,6 +761,28 @@ protected:
score += 1 * (new_extruder_cnt-last_extruder_cnt);
}
// On a belt the parts print in belt order, so every colour change between
// parts is a filament change. Items arrive sorted by extruder and the pile
// grows from the leading end; keep each colour's run contiguous by charging
// an item for every packed item of another colour it does not fully follow,
// counting the tilted layers that reach belt_tilt_slope * height past that
// item's far edge.
if (params.is_belt && !params.is_seq_print) {
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
const double item_start = belt_start(ibb);
for (Item &p : m_items) {
if (p.is_virt_object)
continue;
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
if (same_colour)
continue;
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
score += 10.;
}
}
return std::make_tuple(score, fullbb);
}
@@ -785,7 +859,8 @@ public:
auto binbb = sl::boundingBox(m_bin);
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
auto starting_point = this->params.is_belt ? belt_origin() :
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
// if we have wipe tower, items should be arranged around wipe tower
for (Item itm : items) {
if (itm.is_wipe_tower) {
@@ -936,7 +1011,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
auto mp = m_merged_pile;
mp.emplace_back(itm.transformedShape());
auto chull = sl::convexHull(mp);
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
if (corner_packing())
{
if (!sl::isInside(chull, m_bin))
score += LARGE_COST_TO_REJECT;
+7
View File
@@ -146,6 +146,13 @@ struct ArrangeParams {
float nozzle_height = 0;
float printable_height = 256.0;
Vec2d align_center{ 0.5,0.5 };
// Belt printer: items print in the order they lie along the belt axis, from
// its low end unless belt_reversed, and a part's top prints
// belt_tilt_slope * height further along it than its base.
bool is_belt = false;
int belt_axis = 1; // 0 = X, 1 = Y
bool belt_reversed = false;
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
ArrangePolygons excluded_regions; // regions cant't be used
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
+562
View File
@@ -0,0 +1,562 @@
#include <limits>
#include "BeltBrim.hpp"
#include "ClipperUtils.hpp"
#include "Flow.hpp"
#include "Layer.hpp"
#include "Polygon.hpp"
#include "Print.hpp"
#include "ShortestPath.hpp"
#include "Support/BeltFloorContext.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "Point.hpp"
#include "Polyline.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include <vector>
namespace Slic3r {
// ---------------------------------------------------------------- scaling
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
{
// llround, not a cast: casting truncates toward zero, so a round trip would
// walk every vertex toward the origin by up to one unit per pass.
return from_axis == 0 ?
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
}
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
{
for (Point &p : poly.points)
p = scale_u_point(p, from_axis, factor);
}
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
{
ExPolygons out = src;
for (ExPolygon &ex : out) {
scale_u_polygon(ex.contour, frame.from_axis, factor);
for (Polygon &hole : ex.holes)
scale_u_polygon(hole, frame.from_axis, factor);
}
return out;
}
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
{
Polylines out = src;
for (Polyline &pl : out)
for (Point &p : pl.points)
p = scale_u_point(p, frame.from_axis, factor);
return out;
}
// ---------------------------------------------------------------- sweep
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
{
if (src.empty())
return {};
if (t == Point(0, 0))
return src;
// One parallelogram per boundary edge. Together with P and P + t these
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
// putting q in that edge's parallelogram. Hole edges must be included, or
// holes narrower than t along t would wrongly survive the sweep.
Polygons quads;
for (const ExPolygon &ex : src)
for (size_t c = 0; c < ex.num_contours(); ++ c)
for (const Line &e : ex.contour_or_hole(c).lines()) {
if (e.a == e.b)
continue;
Polygon q;
q.points = { e.a, e.b, e.b + t, e.a + t };
// The non-zero fill rule counts a clockwise ring as -1, which
// would punch a hole instead of adding material. Edges parallel
// to t give a zero-area quad; Clipper discards those harmlessly.
if (q.is_clockwise())
q.reverse();
quads.emplace_back(std::move(q));
}
ExPolygons shifted = src;
for (ExPolygon &ex : shifted)
ex.translate(t);
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
// argument above relies on.
return union_ex(union_ex(src, shifted), quads);
}
// ---------------------------------------------------------------- brim region
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
bool has_outer,
bool has_inner,
coord_t brim_width,
coord_t object_gap,
coord_t leading,
coord_t lateral,
const BeltBrimFrame &frame)
{
if (footprint_flat.empty() || (! has_outer && ! has_inner))
return {};
ExPolygons out;
if (has_outer) {
// Offset the outer ring from the contours only, so a hole cannot punch
// through it. Same reasoning as the plate brim in Brim.cpp.
Polygons contours;
contours.reserve(footprint_flat.size());
for (const ExPolygon &ex : footprint_flat)
contours.emplace_back(ex.contour);
// Inner and outer boundary offset from the same polygon, to avoid
// round-off mismatch between them.
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
// Close the interior before offsetting outwards. A belt contact patch is often a
// narrow, broken-up strip, and the offset rings of two islands less than
// 2 x brim_width apart merge and fill the space between them - space that lies
// UNDER the part, which is not what "outer brim" means. Closing also swallows
// holes in the patch for the same reason. Concavity-filling only, so an apron or
// any other outward protrusion is untouched.
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
ExPolygons base = envelope;
if (leading > 0) {
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
// the ring instead of starting inside the gap.
const Point t = frame.from_axis == 0 ?
Point(frame.downhill_sign() * leading, 0) :
Point(0, frame.downhill_sign() * leading);
base = union_ex(base, sweep_ex(envelope, t));
}
if (lateral > 0) {
// Across the belt, both ways. Swept from `base` so the apron is widened
// too, and in the flattened frame the cross-belt axis is unscaled, so this
// distance is already a true on-belt distance.
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
base = union_ex(base, to_polygons(widened));
}
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
expolygons_append(out, diff_ex(outer, envelope));
}
if (has_inner) {
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
// No apron here: an apron growing into a hole interior is never useful.
Polygons holes;
for (const ExPolygon &ex : footprint_flat)
polygons_append(holes, ex.holes);
polygons_reverse(holes);
if (! holes.empty()) {
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
}
}
return union_ex(out);
}
// ---------------------------------------------------------------- line lattice
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
coord_t u_hi,
coord_t pitch_u,
coord_t u_anchor)
{
std::vector<coord_t> out;
if (pitch_u <= 0 || u_hi <= u_lo)
return out;
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
// the half-open interval needs no epsilon: a point landing exactly on u_hi
// belongs to the next band.
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
while (u_anchor + coord_t(k) * pitch_u < u_lo)
++ k;
for (;; ++ k) {
const coord_t u = u_anchor + coord_t(k) * pitch_u;
if (u >= u_hi)
break;
out.emplace_back(u);
}
return out;
}
// ---------------------------------------------------------------- pipeline
// A band of the belt surface as an explicit box, clamped to `bounds` along the
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
// feed through the flattening scale.
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
{
coord_t lo = scale_(u_lo);
coord_t hi = scale_(u_hi);
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
lo = std::max(lo, bmin);
hi = std::min(hi, bmax);
Polygon poly;
if (hi <= lo)
return poly;
if (from_axis == 0)
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
else
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
return poly;
}
ExPolygons belt_brim_clip_leading_edge(const ExPolygons &region, const BeltBrimFrame &frame, coordf_t u_cut)
{
if (region.empty())
return region;
BoundingBox keep_bb = get_extents(region);
keep_bb.offset(scale_(1.));
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
const Polygon keep = band_box(keep_bb, frame.from_axis,
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
}
// Everything the per-band line generator needs, gathered once per object.
struct BeltBrimContext
{
BeltFloorContext ctx;
BeltBrimFrame frame;
ExPolygons region; // brim region, object-local slicing XY
BoundingBox region_bbox;
Flow brim_flow;
coord_t pitch_u = 0;
coord_t u_anchor = 0;
double in_plane_pitch = 0.; // mm
};
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
static void belt_brim_band_paths(const BeltBrimContext &bc,
coordf_t print_z,
coordf_t height,
const Polygons &obstacles,
ExtrusionEntityCollection &out,
ExPolygons &areas_out)
{
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
// How wide this band is measured ON the belt, versus one nominal bead.
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
// Fraction of the layer height at which a line sits above the belt. Toward the
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
// the belt itself.
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
std::vector<coord_t> us;
double uniform_clearance = 0.; // 0 => derive per line from its own position
double line_pitch = bc.in_plane_pitch;
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
// wider. Two lattice lines in such a band would land almost on top of each other.
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
// Steep belt, which is the normal case: the band is narrower than one bead, so
// exactly one line fits. Place it at a FIXED fraction of the band rather than
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
// point in its band, the clearance sweeps [0, height] from band to band, and the
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
// Anchoring to the band makes the clearance identical everywhere, so every bead
// is the same width.
//
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
// rather than the nominal bead spacing, so the flow below is matched to THAT
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
// gap-free; using nominal flow at band spacing would over-feed it.
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
line_pitch = band_in_plane;
} else {
// Shallow belt: the band is wider than a bead, so it takes several lines and they
// have to sit on the nominal lattice. Their clearances then differ, and so do
// their widths - unavoidable here, but shallow belts are the rare case.
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
}
if (us.empty())
return;
const Polygons region_polys = to_polygons(bc.region);
// One lattice line at a time: the clearance - and therefore the extrusion
// volume - is a property of the line's u, so the pieces of different lines
// must not be pooled before the flow is resolved.
// Overshoot the region so the clip, not the line's ends, decides the extent.
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
coord_t u_prev = std::numeric_limits<coord_t>::min();
for (coord_t u : us) {
// Nozzle-to-belt clearance for this line. Constant along the line, because the
// belt height depends only on the shear-axis coordinate. Band-anchored lines
// share one clearance by construction; lattice lines (shallow belts, or a first
// layer thick enough that the band is wider than a bead) each get their own.
//
// A lattice line can fall where the belt is only a hair below the band's print_z.
// The bead there would be laid scraping the belt while its flow is sized for a
// taller cell, so it is moved uphill to the same fraction of the band the
// single-line case uses. (The clearance is along slice Z; the real gap under the
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
double clearance = uniform_clearance;
if (clearance <= 0.) {
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
clearance = print_z - bc.ctx.floor_print_z(probe);
if (clearance < BAND_CLEARANCE_FRACTION * height) {
clearance = BAND_CLEARANCE_FRACTION * height;
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
}
clearance = std::min(clearance, height);
}
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
// than half a pitch would be laid into the same cell.
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
continue;
u_prev = u;
Polyline line;
if (bc.frame.from_axis == 0)
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
else
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
if (! obstacles.empty())
pieces = diff_pl(pieces, obstacles);
if (pieces.empty())
continue;
// with_cross_section, not with_height: it reaches the prescribed volume while
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
// pitch x clearance cell of the sheet.
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
// Footprint of these beads, for the first-layer convex hull and bbox.
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
areas_out.emplace_back(ExPolygon(p));
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
}
}
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
// overhang outside the belt footprint and land in the brim ring, which the flattened
// brim_object_gap - a belt-plane separation - does not cover.
//
// SEQUENCING: this reads every object's layers and this object's own support layers.
// Another object's support step shifts that object's layer Z into the object frame for
// the duration of the run (PrintObject::_generate_support_material()), so the brims must
// not overlap with the parallel support step: Print::process() generates them one object
// after the other once that step is over (PrintObject::generate_belt_brim()), and an
// object that arrives on or leaves the plate invalidates the other brim owners' support
// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
// object's supports are dodged; another object's support at the same Z is not.
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
// objects are skipped without materialising their polygons. On a typical plate the
// objects do not overlap and every foreign object drops out here, which matters because
// this runs once per band - hundreds of times per object.
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
const BoundingBox &region_bbox, coordf_t print_z, coordf_t tol)
{
const Point shift_self = self.instances().empty() ? Point(0, 0)
: self.instances().front().shift_without_plate_offset();
Polygons out;
for (const PrintObject *o : print.objects()) {
const bool is_self = (o == &self);
for (const PrintInstance &inst : o->instances()) {
const Point delta = inst.shift_without_plate_offset() - shift_self;
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
BoundingBox lb = get_extents(l->lslices);
lb.translate(delta.x(), delta.y());
if (lb.overlap(region_bbox)) {
Polygons ps = to_polygons(l->lslices);
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
if (! is_self)
continue;
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
}
if (out.size() < 2)
return out; // union_() of 0 or 1 polygons is pure overhead
return union_(out);
}
void make_belt_brim(PrintObject &object)
{
object.clear_belt_brim();
if (! object.has_belt_brim())
return;
const Print &print = *object.print();
BeltBrimContext bc;
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
return;
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
const size_t nlayers = object.layers().size();
if (nlayers == 0)
return;
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
// own contact band. This is the object's bottom face, which on a belt is
// spread over every layer instead of sitting in layer 0.
ExPolygons footprint_acc;
// The first layer that touches the belt: where the leading-edge brim is cut.
const Layer *first_contact = nullptr;
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
if (layer.lslices.empty())
continue;
// print_z - height, not the previous layer's print_z: variable layer
// heights make the latter wrong.
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
BoundingBox bb = get_extents(layer.lslices);
bb.offset(scale_(1.));
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
if (band.empty())
continue;
ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
if (contact.empty())
continue;
if (first_contact == nullptr)
first_contact = &layer;
expolygons_append(footprint_acc, std::move(contact));
}
const ExPolygons footprint = union_ex(footprint_acc);
if (footprint.empty())
return;
// 2. Brim region, offset in the flattened (true on-belt) metric.
const PrintObjectConfig &cfg = object.config();
bc.brim_flow = print.brim_flow();
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
// Quantize to an even number of lines, as the plate brim does.
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
const coord_t leading = scale_(cfg.leading_brim_length.value);
const coord_t lateral = scale_(cfg.extra_brim_width.value);
const coord_t gap = scale_(cfg.brim_object_gap.value);
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
// is an outer brim too; it is narrowed down to the first contact below.
const BrimType bt = cfg.brim_type.value;
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|| bt == btLeadingEdgeOnly;
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
bc.region = belt_unflatten(
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
width, gap, leading, lateral, bc.frame),
bc.frame);
if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
// The cut is the uphill edge of the first contact's band: everything past it
// belongs to later contacts. The first contact is the first layer that touches
// the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
// first layer with geometry, which is an overhang's tip when the part overhangs
// its leading end: both lie ahead of the part.
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
if (bc.region.empty())
return;
bc.region_bbox = get_extents(bc.region);
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
// footprint's leading-most edge so lines stay collinear across
// disconnected islands and across the apron prologue.
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
{
const BoundingBox fbb = get_extents(footprint);
const bool low_side = bc.frame.shear > 0.;
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
: (low_side ? fbb.min.y() : fbb.max.y());
}
// 4. Bands coincident with an object layer.
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
std::vector<ExPolygons> areas_by_layer(nlayers);
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
}
// 5. Apron prologue: the part of the region downhill of the object's first
// layer, which has no object layer to ride on.
std::vector<BeltBrimBand> prologue;
{
const Layer &first = *object.layers().front();
const coordf_t h = first.height;
const bool low_side = bc.frame.shear > 0.;
const coord_t u_lead_s = bc.frame.from_axis == 0
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
const coordf_t u_lead = unscale<double>(u_lead_s);
// print_z at which the belt surface crosses the region's leading edge.
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
if (h > EPSILON)
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
BeltBrimBand band;
band.print_z = z;
band.height = h;
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
if (! band.fills.empty())
prologue.emplace_back(std::move(band));
}
// Lowest Z first, so collect_layers_to_print sees them in print order.
std::reverse(prologue.begin(), prologue.end());
}
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
}
} // namespace Slic3r
+178
View File
@@ -0,0 +1,178 @@
#ifndef slic3r_BeltBrim_hpp_
#define slic3r_BeltBrim_hpp_
#include "ExPolygon.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "Point.hpp"
#include "Polyline.hpp"
#include "libslic3r.h"
#include <cmath>
#include <vector>
// Belt-printer brim geometry.
//
// A belt printer slices in a ROTATED frame, so the belt surface is not the
// Z=0 bed plane but a tilted plane in slicing space:
//
// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
//
// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
// the axis is selected by SlicingParameters::belt_floor_from_axis. See
// Support/BeltFloorContext.hpp for the canonical accessors.
//
// Consequences that drive everything in this file:
//
// * A horizontal slicing layer touches the belt only along a narrow strip at
// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
// The object's belt footprint - its bottom face - is therefore spread over
// every layer, not contained in layer 0.
// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
// along the shear axis travels `du / cos(tilt)` across the belt. So brim
// offsets have to be taken in a "flattened" space where the shear axis is
// stretched by `1 / cos(tilt)`, then mapped back.
// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
// first layer, because the object's layer 0 is precisely its leading
// contact with the belt.
//
// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
// tested without a Print. Keep user-visible strings out of this file: it is
// not listed in localization/i18n/list.txt.
namespace Slic3r {
// Tilt window within which the BELT plane, not the bed plane, is the adhesion
// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
// brim compresses into a sliver and is not worth generating.
inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
// Description of the tilted belt plane, reduced to what the brim geometry needs.
struct BeltBrimFrame
{
// tan(tilt). Sign selects which way is downhill.
double shear = 0.;
// 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
int from_axis = 1;
// 1 / cos(tilt). Stretch factor that turns a projected distance along
// `from_axis` into the true distance travelled across the belt.
double u_stretch() const { return std::sqrt(1. + shear * shear); }
// cos(tilt). The inverse mapping.
double cos_tilt() const { return 1. / this->u_stretch(); }
// Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
// the leading edge of the part. For shear > 0 that is -u.
int downhill_sign() const { return shear > 0. ? -1 : +1; }
};
// Scale only the `from_axis` component by `factor`, rounding to nearest.
//
// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
// truncate toward zero, which is asymmetric about the origin and loses up to a
// full coordinate unit per vertex on every round trip.
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
// Into / out of the space where Euclidean offsets equal true on-belt distances.
inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
{ return belt_scale_u(src, frame, frame.u_stretch()); }
inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
{ return belt_scale_u(src, frame, frame.cos_tilt()); }
// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
// `src` along t. Used to grow the brim downhill for "extra brim width".
//
// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
// contours including holes, with every parallelogram forced counter-clockwise
// so the non-zero fill rule closes holes narrower than t along the sweep
// direction. A hole survives exactly when it is wider than |t| measured along
// t - not when it is wider in its narrowest Euclidean direction.
ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
// at or downhill of the object's first contact with the belt, so the part is
// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
// is the uphill edge of the first layer's contact band along `frame.from_axis`,
// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
// `frame.downhill_sign()` points to.
ExPolygons belt_brim_clip_leading_edge(const ExPolygons &region, const BeltBrimFrame &frame, coordf_t u_cut);
// Brim region for one already-flattened belt footprint. All lengths are scaled
// and measured in the flattened (true on-belt) metric.
//
// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
// this function never needs PrintConfig.
//
// Two directional extras are applied to the footprint before the outer offset, so
// each one buys reach in one direction only:
//
// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
// so every leading-facing edge gains an apron ahead of it.
// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
// the brim sideways without pushing it further ahead or behind.
//
// Neither is applied to the inner (hole) ring.
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
bool has_outer,
bool has_inner,
coord_t brim_width,
coord_t object_gap,
coord_t leading,
coord_t lateral,
const BeltBrimFrame &frame);
// Brim line positions for one layer band.
//
// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
// between neighbouring brim lines is constant regardless of how the lattice
// falls across layer bands. Snapping to band centres instead would quantise
// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
//
// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
// exactly one band: none duplicated at a boundary, none dropped. A band
// narrower than the pitch simply yields nothing; a band much wider (shallow
// tilt) yields several lines.
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
coord_t u_hi,
coord_t pitch_u,
coord_t u_anchor);
// ---------------------------------------------------------------- pipeline
// One brim-only layer printed BEFORE the object's first layer, carrying the
// apron that has to be stuck to the belt ahead of the part.
//
// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
// semantics that leak into initial-layer temperature selection, the spiral vase
// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
// which key off Layer::id() == 0 or off a layer's regions. A plain record
// carries only what the emitter needs.
//
// `height` is the LAYER height, used for the Z move and ordering metadata only.
// Each extrusion path inside `fills` carries its own height, equal to that
// line's nozzle-to-belt clearance, which varies across the band.
struct BeltBrimBand
{
coordf_t print_z = 0.;
coordf_t height = 0.;
// erBrim paths in the object's local slicing frame, untranslated.
ExtrusionEntityCollection fills;
// Footprint of those paths, for the first-layer convex hull / bbox.
ExPolygons areas;
};
class PrintObject;
// Generate the belt brim for one object: fills its per-object-layer bands and
// its apron prologue. No-op unless PrintObject::has_belt_brim().
//
// Runs inside posSupportMaterial rather than the brim step, because the prologue
// print_z values must exist before ToolOrdering is built at psWipeTower.
void make_belt_brim(PrintObject &object);
} // namespace Slic3r
#endif // slic3r_BeltBrim_hpp_
+50
View File
@@ -0,0 +1,50 @@
#include "BeltGCode.hpp"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "BeltTransform.hpp"
#include "Print.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <cstdlib>
namespace Slic3r {
void BeltGCode::init_belt_writer(Print &print)
{
// Axis remap and build volume max are set by base GCode after init_belt_writer
// returns; set_kinematics() replays them, so install order does not matter.
install_belt_kinematics(m_writer, print.config());
m_writer.set_force_normal_lift(true);
}
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
{
const auto &full_cfg = print.full_print_config();
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
// Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
}
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
// the origin through the belt pipeline so that back_transform(T * origin) =
// origin (correct machine position). The back_transform applied during
// G-code emission is the inverse of the forward transform.
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
Vec2d cur_origin = this->origin();
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
Vec3d adjusted = T.linear() * origin3d;
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
}
} // namespace Slic3r
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "GCode.hpp"
#include "Point.hpp"
#include "Print.hpp"
namespace Slic3r {
// Belt-printer-specific GCode export.
//
// Inherits from GCode and overrides virtual hooks to:
// - Install a BeltKinematics on the GCodeWriter
// - Write belt configuration to the G-code header
// - Adjust the origin for global pre-slice transforms when switching instances
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
class BeltGCode : public GCode
{
protected:
void init_belt_writer(Print &print) override;
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
};
} // namespace Slic3r
+433
View File
@@ -0,0 +1,433 @@
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
// classic wipe/prime tower).
//
// Kept in its own translation unit so the belt-purge logic stays out of the way
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
// no regression risk for normal printers: none of these methods do anything
// unless the print is a belt printer with the belt purge tower enabled.
//
// Print::has_belt_purge_tower() - is the belt purge tower active?
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
// printed objects' grid
// Print::_plan_belt_purge() - route filament-change purging into the
// prism (flush-into-objects), no wipe tower
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
//
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
#include "Print.hpp"
#include "PrintConfig.hpp"
#include "Exception.hpp"
#include "GCode/ToolOrdering.hpp"
#include "Layer.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r.h"
#include "BeltBrim.hpp"
#include "PrintBase.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <boost/log/trivial.hpp>
#include <cassert>
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>
namespace Slic3r {
// Belt purge prism: purging after filament changes is routed into a sliced
// prism object via the flush-into-objects machinery instead of a wipe tower.
bool Print::has_belt_purge_tower() const
{
// Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
// option (not the classic enable_prime_tower).
if (!(m_config.belt_printer.value
&& m_config.enable_belt_purge_tower.value
&& !m_config.spiral_mode.value
&& m_config.filament_diameter.values.size() > 1))
return false;
return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
return object->config().belt_purge_tower_object.value;
});
}
// Belt mode: align ALL objects on the plate (the printed objects AND the purge
// prism) onto one common layer grid, so the prism can absorb every toolchange.
//
// After belt slicing each object's layer print_z carries a per-object global z
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
// objects at different belt-Y positions get layer grids with DIFFERENT residues
// (mod layer height). Purge marking looks absorbers up with
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
// the prism to one object therefore worked for a single (assembled) multi-color
// object but failed with multiple separate objects — the prism could follow only
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
// grids" warning).
//
// Fix: pick one reference grid (the tallest object) and shift every object onto
// it. Each shift is at most half a layer height — a sub-100µm move along the
// belt, the very same mechanism the per-object global_z_offset already uses, and
// it keeps each object internally consistent (belt_shift_layer_grid moves the
// object's layers, its support layers, and its belt floor together). Equal layer
// height across objects is enforced by Print::validate(), so once residues match
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
// layer coincides with a prism layer.
void Print::_align_belt_purge_layers()
{
PrintObject *prism = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
prism = po;
break;
}
if (prism == nullptr || prism->layers().empty())
return;
const double h = prism->config().layer_height.value;
if (h <= EPSILON)
return;
// Grid residue of an object's layer grid: identical for all of an object's
// layers above the first since they step by h.
auto grid_offset = [h](const PrintObject *po) -> double {
if (po->layers().empty())
return 0.;
const double z = po->layers().front()->print_z;
return z - std::floor(z / h) * h; // in [0, h)
};
// Reference grid: the tallest non-prism object (proxy for the object with
// the most toolchange layers — minimizes how far the rest must move).
const PrintObject *ref = nullptr;
double ref_top = -std::numeric_limits<double>::max();
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value || po->layers().empty())
continue;
const double top = po->layers().back()->print_z;
if (top > ref_top) {
ref_top = top;
ref = po;
}
}
if (ref == nullptr)
return;
const double ref_offset = grid_offset(ref);
// Snap every object (printed objects AND the prism) onto the reference grid.
for (PrintObject *po : m_objects) {
if (po->layers().empty())
continue;
double delta = ref_offset - grid_offset(po);
if (delta > 0.5 * h)
delta -= h;
else if (delta <= -0.5 * h)
delta += h;
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
}
}
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
// into the belt purge prism (and any other flush_into_* object) using the
// flush-into-objects machinery, without generating classic wipe tower G-code.
// The toolchange itself is emitted by GCode::set_extruder() via the
// change_filament_gcode macro; the overrides marked here make the new
// filament's first extrusions land in the purge prism.
void Print::_plan_belt_purge()
{
m_wipe_tower_data.clear();
// psWipeTower may be invalidated without posSlice (for example after a
// filament-map or tool-ordering change). Restore a prism shortened by the
// previous plan so a newly higher toolchange can use its original layers.
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value)
po->belt_undo_purge_plan();
// Must run before ToolOrdering is built: LayerTools merge per-object layer
// print_z values, and the prism only absorbs purge where its (snapped)
// layers coincide with the toolchange layers.
this->_align_belt_purge_layers();
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
// No initial priming extrusions: there is no tower to prime on.
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
// neither object nor support and so never gets the flag even when the print changes filament.
{
bool any_change = false;
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const unsigned int e : lt.extruders)
if (e != cur) { any_change = true; cur = e; }
if (! any_change)
return;
}
this->throw_if_canceled();
// Flush volumes per filament pair, mirroring the generic wipe tower path:
// full flush matrix for single extruder multi material with purging enabled,
// plain prime volume otherwise.
std::vector<float> flush_matrix(cast<float>(
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < number_of_extruders; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
flush_matrix.begin() + (i + 1) * number_of_extruders));
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
// Cancel the purge prism early: pre-scan the tool ordering for the highest
// print_z that actually has a toolchange, then drop the prism's layers above
// it so the tower stops at the last color swap (saves filament/time). This
// MUST happen before the marking loop below: ensure_perimeters_infills_order
// force-overrides the prism's extrusions on every layer (it is a dedicated
// flush object), so truncating afterwards would leave dangling overrides
// pointing into deleted layers.
{
// The tool ordering covers the WHOLE print, and the prism is a printed
// object in it. Left unbounded, the scan below sees the prism's own
// toolchanges on layers above every model object -- the prism runs past
// them by design (ramp/height compensation at the tilted ends) -- so
// last_tc_z lands at the prism's own top and the truncation cancels
// nothing. The tower ends up justifying its own existence.
//
// Nothing above the tallest printed object can require a color change,
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
// 39.4 mm of tower that no swap ever needed.
// Support layers count too: on a belt they can extend above the object's
// own top, and a toolchange there is a real one.
double obj_top_z = -1.;
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value)
continue;
if (!po->layers().empty())
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
if (!po->support_layers().empty())
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
}
double last_tc_z = -1.;
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools()) {
// layer_tools() is ordered by print_z ascending.
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
break;
for (const unsigned int e : lt.extruders)
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
}
// Deliberately NOT cancelling the prism outright when no object toolchange
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
// with zero layers is not something the rest of the pipeline expects. The
// GUI already declines to create a prism unless more than one filament is
// in use, so this case is a stale prism, not a hot path -- leave it whole
// rather than risk a zero-layer object.
if (last_tc_z >= 0.)
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
po->belt_truncate_layers_above(last_tc_z);
break;
}
}
// The prism only absorbs purge at toolchange layers whose print_z coincides
// with one of its own layers.
PrintObject *prism_po = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
float total_leftover = 0.f;
float worst_layer_leftover = 0.f;
double worst_layer_z = 0.;
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
float layer_leftover = 0.f;
for (const unsigned int extruder_id : layer_tools.extruders) {
if (extruder_id == current_extruder_id)
continue;
float volume_to_wipe = use_flush_matrix ?
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
(float) m_config.prime_volume;
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
volume_to_wipe);
layer_leftover += leftover;
current_extruder_id = extruder_id;
}
// Plastic saving: drop the prism's fills that no toolchange on this layer
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
// purge; the rest would print as solid infill in the prism's own filament
// for nothing -- which is the whole prism on a layer with no toolchange
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
// left alone so the bar keeps a continuous wall along the belt.
//
// Non-destructive: the entities are stashed with their positions and put
// back by belt_restore_dropped_fills() at the top of the next plan. An
// earlier version deleted them outright, which broke replanning when a
// later tool ordering needed what this one had not claimed -- that is why
// it was removed rather than kept.
if (prism_po != nullptr) {
const auto &we = layer_tools.wiping_extrusions();
prism_po->belt_drop_unclaimed_fills(
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
if (layer_leftover > 0.f) {
total_leftover += layer_leftover;
if (layer_leftover > worst_layer_leftover) {
worst_layer_leftover = layer_leftover;
worst_layer_z = layer_tools.print_z;
}
}
this->throw_if_canceled();
}
if (total_leftover > 1.f) {
this->active_step_add_warning(
PrintStateBase::WarningLevel::CRITICAL,
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
"Increase the belt purge tower width, or reduce flushing volumes."),
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
}
}
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
// application in slice() — layer print_z and belt_floor_z_shift move together
// so belt floor clipping stays consistent with the shifted grid. Used by
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
// of the prism along the belt.
void PrintObject::belt_shift_layer_grid(double delta)
{
if (std::abs(delta) < EPSILON)
return;
for (Layer *layer : m_layers)
layer->print_z += delta;
for (SupportLayer *layer : m_support_layers)
layer->print_z += delta;
// The brim's apron bands below the first layer carry their own print_z.
for (BeltBrimBand &band : m_belt_brim_prologue)
band.print_z += delta;
m_slicing_params.belt_floor_z_shift += delta;
// The grid stays shifted across a support-only or brim-only change (posSlice does
// not rerun), so everything slice() derived from it has to follow: the cached floor
// that update_slicing_parameters() restores, and the global offset the organic
// support layers and the adaptive infill octree are placed with. Left alone, the
// next alignment finds a delta of 0 and the floor and the supports sit up to half
// a layer off the grid, unlike a fresh slice.
if (m_belt_floor_z_shift_cache_valid)
m_belt_floor_z_shift_cached += delta;
m_belt_global_z_offset += delta;
}
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
// once there are no more toolchanges above z, so the tower stops at the last
// color swap instead of wasting filament up the rest of the belt). Each layer's
// cross-section is already sliced, so removing upper layers does not affect the
// last toolchange's coverage. Deletes the Layer objects and clears the new top
// layer's upper-layer link. Returns the number of layers removed.
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
{
// A repeated plan always starts from the restored full layer set.
assert(m_belt_truncated_layers.empty());
size_t keep = m_layers.size();
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
--keep;
if (keep >= m_layers.size())
return 0;
const size_t removed = m_layers.size() - keep;
m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
m_layers.resize(keep);
if (!m_layers.empty())
m_layers.back()->upper_layer = nullptr;
return removed;
}
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
//
// Called per layer from _plan_belt_purge(), after the real-purge marking and
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
// remaining fill on the prism (it is a dedicated flush object), so afterwards
// everything looks claimed and nothing could be distinguished.
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
{
if (layer == nullptr)
return 0;
size_t dropped = 0;
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
LayerRegion *lr = layer->get_region(ri);
auto &ents = lr->fills.entities;
ExtrusionEntitiesPtr keep;
keep.reserve(ents.size());
for (size_t i = 0; i < ents.size(); ++i) {
if (claimed(ents[i])) {
keep.emplace_back(ents[i]);
} else {
// Stash with its original index so the restore is exact.
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
++dropped;
}
}
ents = std::move(keep);
}
return dropped;
}
void PrintObject::belt_restore_dropped_fills()
{
if (m_belt_dropped_fills.empty())
return;
// Ascending index per (layer, region): inserting in that order lands every
// entity back at its original position, because each insertion shifts only
// the entries after it, which are themselves still to be inserted.
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
if (a.layer != b.layer) return a.layer < b.layer;
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
return a.index < b.index;
});
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
}
m_belt_dropped_fills.clear();
}
void PrintObject::belt_restore_truncated_layers()
{
if (m_belt_truncated_layers.empty())
return;
m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
m_belt_truncated_layers.clear();
for (size_t i = 0; i < m_layers.size(); ++i) {
m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
}
}
} // namespace Slic3r
+82
View File
@@ -0,0 +1,82 @@
#include "BeltSliceStrategy.hpp"
#include "Model.hpp"
#include "BeltTransform.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include <limits>
#include <algorithm>
namespace Slic3r {
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z)
{
// 1. Belt rotation — the sole mesh-side belt transform (matching
// BeltTransformPipeline::build_forward_transform). Only active in
// belt-printer mode.
bool has_rotation = false;
if (config.belt_printer.value) {
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
if (has_rotation) {
Transform3d belt_xform = Transform3d::Identity();
belt_xform.linear() = rot;
trafo = belt_xform * trafo;
}
}
if (!has_rotation)
return;
// 2. Z-shift — detect if the mesh clips below the build plate after the
// transforms and lift it. Each mesh vertex must be brought into object space
// via mv->get_matrix() before applying the full trafo (which is in object
// space). Missing this on assemblies (where per-volume get_matrix() positions
// each volume within the object) would compute min_z against mesh-local vertex
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
//
// The lift is measured to the lowest point of the SUPPORT region, not of the
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
// coordinate) runs below every vertex, and under the leading end of an
// overhang it lies below the lowest vertex by up to the overhang's length
// times the shear. Supports have to reach that floor, and every support
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
// point under the footprint. The layers between it and the first vertex
// come out empty, which belt slicing already tolerates (the bottom corner
// of a tilted part is a point). Vertices on the belt have z == floor, so
// for a part resting on the belt this is simply the floor at its leading
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
BeltTransformPipeline::BeltFloorParams floor;
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue;
Transform3d vol_trafo = trafo * mv->get_matrix();
const auto &its = mv->mesh().its;
for (const stl_vertex &v : its.vertices) {
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
if (has_floor)
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
}
}
if (has_floor && min_z != std::numeric_limits<double>::max())
min_z -= BeltTransformPipeline::frame_margin(floor);
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = z_shift_val;
trafo = z_shift * trafo;
}
// out_belt_min_z is only meaningful in belt mode.
if (out_belt_min_z && config.belt_printer.value) {
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
}
}
} // namespace Slic3r
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BeltTransform.hpp"
#include "PrintConfig.hpp"
#include "Model.hpp"
namespace Slic3r {
// Belt printer pre-slice transform strategy.
//
// Composes, in order, the mesh transforms applied before slicing on a belt printer:
// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
// g-code-side stage, see MachineFrameTransform)
// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
// belt below its footprint
//
// Isolates this belt-specific logic from the generic slicing pipeline in
// PrintObjectSlice.cpp.
class BeltSliceStrategy
{
public:
// Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
// rotation is configured.
//
// out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
static void apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z = nullptr);
};
} // namespace Slic3r
+153
View File
@@ -0,0 +1,153 @@
#include "BeltTransform.hpp"
#include "Model.hpp"
#include "BoundingBox.hpp"
#include "Config.hpp"
#include "Geometry.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <limits>
#include <algorithm>
#include <cmath>
#include <cstdlib>
namespace Slic3r {
// ---- Matrix builders ------------------------------------------------------
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
{
BeltRotationAxis axis = config.belt_slice_rotation.value;
double angle_deg = config.belt_slice_rotation_angle.value;
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
if (has_rot_out) *has_rot_out = active;
if (!active)
return Matrix3d::Identity();
double angle_rad = Geometry::deg2rad(angle_deg);
Vec3d unit_axis;
switch (axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: return Matrix3d::Identity();
}
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
}
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
{
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
// stage, not part of the mesh transform.)
Transform3d combined = Transform3d::Identity();
combined.linear() = build_rotation_matrix(config);
return combined;
}
// ---- Belt floor parameters ------------------------------------------------
namespace {
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
{
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
out.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
out.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
out.shear_factor = 0.0;
out.from_axis = 1;
break;
}
}
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
{
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
}
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
{
BeltTransformPipeline::BeltHeightResult result;
result.object_height = original_height;
// The mesh rotation (the sole mesh-side belt transform).
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
if (!has_rotation)
return result;
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
// then derive the belt floor (the image of machine-Z = 0 under R).
double angle_rad = Geometry::deg2rad(rot_angle);
Vec3d unit_axis;
switch (rot_axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
// The slicing frame starts at the lowest point of the support region: the
// lowest belt-floor point under the footprint, not the lowest vertex. The
// belt under the leading end of an overhang lies below every vertex of the
// part, and supports have to be able to reach it (see
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
// counterpart of this bbox estimate).
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
Vec3d rc = R * c;
double z = rc.z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
}
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
result.object_height = max_rz - min_rz;
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
}
} // anonymous namespace
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, bbox, original_height);
}
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
{
out = BeltFloorParams{};
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
return false;
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
return std::abs(out.shear_factor) > EPSILON;
}
} // namespace Slic3r
+132
View File
@@ -0,0 +1,132 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
#include "Geometry.hpp"
#include "Config.hpp"
#include <cmath>
namespace Slic3r {
class ModelObject;
// Shared belt-printer transform math.
//
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
// trafo_out = z_shift * rotation * trafo_in
//
// Rotation is the sole mesh-side belt transform; shear & scale are applied
// to the g-code instead (see MachineFrameTransform). This class provides the
// building blocks so every call site uses the same implementation. z_shift is
// object-dependent (computed from mesh vertex bounds) and is NOT included in
// build_forward_transform(). The machine-frame shear/scale is derived directly
// from the tilt angle in MachineFrameTransform and no longer lives here.
//
// Design note: this mesh-rotation approach replaced an earlier pre-shear
// method (now removed). While that initial pre-shear method was instrumental
// in getting belt printer slicing off the ground in the first place, its place is
// in the past. A big thank you goes to the Unlayered3D team, who recommended
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
// operation isometric — no distortion of the sliced geometry — while the
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
// derived from the same tilt angle.
//
// This fixed a number of issues, including several issues noticed by hotcubcar
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
// that were all mostly resolved as a result of the switch.
//
// This also means that the pre-slice rotation transform methodology can be used
// more cleanly on non-belt printers.
// - HarrierPigeon (Joseph Robertson)
class BeltTransformPipeline
{
public:
// ---- Identity checks --------------------------------------------------
// Whether the G-code axis remap applies at all. The remap fields are only
// offered in the belt printer group, so a value left in a profile must not
// change a non-belt print: with belt mode off every belt-only key is a no-op.
// This is the one place to widen if a non-belt use ever needs them.
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
static bool has_rotation(const PrintConfig &config)
{
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
}
// Physical belt tilt derived from the slicing rotation — the single source of
// truth for bed rendering, support gravity tilt and the bed-exclusion
// projection. Returns the tilt magnitude in degrees split onto the X and Y
// build-plate tilt axes according to the rotation axis:
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
// The magnitude uses abs(angle) so a negative rotation still reports a positive
// physical tilt.
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
{
PhysicalTilt t;
double mag = std::abs(angle_deg);
switch (axis) {
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
default: break; // Z / None: no physical tilt
}
return t;
}
// ---- Matrix builders --------------------------------------------------
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
// Returns Identity if rotation axis is None or angle is ~0.
// Also sets has_rot_out if non-null.
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
// Forward transform (the rotation) — the mesh-side belt transform that
// BeltSliceStrategy applies and BeltBackTransform inverts.
// Does NOT include the per-object Z-shift.
static Transform3d build_forward_transform(const PrintConfig &config);
// ---- Belt floor parameters --------------------------------------------
struct BeltFloorParams {
double shear_factor = 0.0;
int from_axis = 1;
double z_shift = 0.0;
};
// Shear factor and from-axis of the belt floor in the rotated slicer frame
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
// rotation the config selects. z_shift is left at 0. Returns false (and
// zero shear) when the config has no tilt.
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
// How far below the lowest belt-floor point under the footprint the slicing
// frame starts, in slicing Z. A support column meeting the belt is wider at
// its base than at its tip, so under a leading overhang the base reaches ahead
// of the part along the belt, and the layers that trim it to the belt plane
// lie below that lowest point: 10 mm along the belt.
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
BeltFloorParams floor_params;
};
// Compute effective object height and belt floor parameters from config
// and the object's bounding box. original_height is the input height
// (bb.size().z() or model_object.max_z()).
static BeltHeightResult compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &bbox,
double original_height);
};
} // namespace Slic3r
+14 -1
View File
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
const bool use_brim_ears = object->config().brim_type == btPainted;
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
// edge, so it degrades to an ordinary outer brim rather than silently to none.
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::vector<unsigned int>& printExtruders,
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
{
// Belt printers never use the flat plate brim.
//
// With a tilted belt the brim has to be laid onto the belt plane over many layers,
// which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
// could in principle fall through and produce an ordinary brim, but it would never
// reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
// _make_skirt() builds, and that returns early for every belt printer. Running the
// generator anyway would just burn time on geometry nobody prints.
if (print.config().belt_printer.value)
return;
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
Flow flow = print.brim_flow();
ExPolygons islands_area_ex = outer_inner_brim_area(print,
+1 -1
View File
@@ -84,7 +84,7 @@ public:
indexed_triangle_set bounding_mesh(bool scale=true) const;
// Center of the print bed, unscaled.
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
// Convex hull of polygon(), scaled.
const Polygon& convex_hull() const { return m_convex_hull; }
// Smallest enclosing circle of polygon(), scaled.
+19
View File
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
BoundingBox.hpp
BridgeDetector.cpp
BridgeDetector.hpp
BeltBrim.cpp
BeltBrim.hpp
BeltGCode.cpp
BeltGCode.hpp
BeltPurge.cpp
BeltSliceStrategy.cpp
BeltSliceStrategy.hpp
BeltTransform.cpp
BeltTransform.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
@@ -232,6 +241,14 @@ set(lisbslic3r_sources
GCode/AdaptivePAProcessor.hpp
GCode/AvoidCrossingPerimeters.cpp
GCode/AvoidCrossingPerimeters.hpp
GCode/BeltBackTransform.cpp
GCode/BeltBackTransform.hpp
GCode/MachineFrameTransform.cpp
GCode/MachineFrameTransform.hpp
GCode/BeltKinematics.cpp
GCode/BeltKinematics.hpp
GCode/MachineKinematics.cpp
GCode/MachineKinematics.hpp
GCode/ConflictChecker.cpp
GCode/ConflictChecker.hpp
GCode/CoolingBuffer.cpp
@@ -453,6 +470,8 @@ set(lisbslic3r_sources
SlicingAdaptive.hpp
Slicing.cpp
Slicing.hpp
Support/BeltFloorContext.cpp
Support/BeltFloorContext.hpp
Support/SupportCommon.cpp
Support/SupportCommon.hpp
Support/SupportLayer.hpp
+5
View File
@@ -403,6 +403,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
expoly.translate(p);
}
inline void translate(Polygons &polys, const Point &p) {
for (Polygon &poly : polys)
poly.translate(p);
}
inline void polygons_append(Polygons &dst, const ExPolygon &src)
{
dst.reserve(dst.size() + src.holes.size() + 1);
+31 -12
View File
@@ -1041,10 +1041,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
void _stop_object_xml_parser(const std::string& msg = std::string())
{
assert(! obj_parse_error);
assert(obj_parse_error_message.empty());
assert(object_xml_parser != nullptr);
obj_parse_error = true;
obj_parse_error_message = msg;
if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already
obj_parse_error_message = msg;
XML_StopParser(object_xml_parser, false);
}
@@ -3901,11 +3901,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
if (m_curr_object)
m_curr_object->geometry.vertices.emplace_back(
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
if (m_curr_object) {
const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
// A non-finite coordinate ("nan", "inf") used to be accepted and crashed
// qhull in ModelVolume's convex hull while the file was still loading. Refuse the file.
if (! v.allFinite()) {
_stop_xml_parser("Invalid vertex coordinate: not a finite number");
return true; // the parser is stopped; returning false would overwrite the message
}
m_curr_object->geometry.vertices.emplace_back(v);
}
return true;
}
@@ -5195,6 +5202,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
for (const Vec3f &v : sub_object->geometry.vertices)
if (! v.allFinite()) { // Qhull cannot take a NaN vertex
add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id));
return false;
}
its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end());
// BBS
@@ -5708,11 +5720,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
if (current_object)
current_object->geometry.vertices.emplace_back(
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
if (current_object) {
const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
// See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate
// crashed qhull while the file loaded. The dispatcher stops this parser on `false`.
if (! v.allFinite()) {
obj_parse_error_message = "Invalid vertex coordinate: not a finite number";
return false;
}
current_object->geometry.vertices.emplace_back(v);
}
return true;
}
+633 -39
View File
File diff suppressed because it is too large Load Diff
+130 -8
View File
@@ -7,6 +7,7 @@
#include "Print.hpp"
#include "libslic3r.h"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -40,13 +41,16 @@
#include <memory>
#include <map>
#include <unordered_map>
#include <optional>
#include <set>
#include <string>
#include <cfloat>
#include <vector>
#include <utility>
#include <cmath>
#include "BoundingBox.hpp"
#include "Polyline.hpp"
#include "BeltBrim.hpp"
namespace Slic3r { class ExtrusionEntityCollection; }
@@ -249,8 +253,9 @@ public:
m_toolchange_count(0),
m_nominal_z(0.)
{}
~GCode() = default;
virtual ~GCode() = default;
public:
// throws std::runtime_exception on error,
// throws CanceledException through print->throw_if_canceled().
void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
@@ -342,6 +347,13 @@ public:
const Layer* object_layer;
const SupportLayer* support_layer;
const PrintObject* original_object; //BBS: used for shared object logic
// Belt printers only: an apron band that prints BELOW the object's first
// layer, so it has no object or support layer of its own. Deliberately
// not a Layer, so it cannot leak Layer::id() semantics into initial-layer
// temperature, spiral vase, cooling or interpolation logic. When this is
// the only thing set, layer() is null and process_layer() takes its
// dedicated brim-only branch.
const BeltBrimBand* belt_brim_band { nullptr };
const Layer* layer() const
{
if (object_layer != nullptr)
@@ -371,11 +383,17 @@ public:
count++;
}
// A brim-only apron band contributes no object/support layer, and
// averaging zero terms would yield NaN. Never folded into the
// average, so the non-belt result is bit-identical.
if (count == 0 && belt_brim_band != nullptr)
return belt_brim_band->print_z;
return sum_z / count;
}
};
private:
protected:
class GCodeOutputStream {
public:
GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {}
@@ -403,9 +421,21 @@ private:
FILE *f = nullptr;
GCodeProcessor &m_processor;
};
// Virtual hooks for belt printer subclass (BeltGCode).
// No-ops in base GCode; overridden in BeltGCode.
virtual void init_belt_writer(Print &print) {}
virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {}
virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {}
// Arc fitting is suppressed whenever the writer's machine mapping cannot
// represent a G2/G3 arc. Belt printers get this through BeltKinematics
// rather than through an override of their own.
virtual bool should_disable_arc_fitting() const
{ return ! m_writer.kinematics().supports_arc_moves(); }
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object);
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false);
static std::vector<std::pair<coordf_t, std::vector<LayerToPrint>>> collect_layers_to_print(const Print &print);
std::string generate_skirt(const Print &print,
@@ -425,7 +455,29 @@ private:
std::string generate_object_brim(const Print &print,
const PrintObject &object,
size_t instance_id,
bool first_layer);
bool first_layer,
const Layer *object_layer);
// Belt printers: emit one brim-only apron layer. These print below the
// object's first layer, so there is no object or support layer for the normal
// process_layer() machinery to work from. Kept to the minimum a layer needs -
// tool, Z move, extrusions - so that nothing here can perturb the
// Layer::id()-based logic the ordinary path relies on.
LayerResult process_belt_brim_layer(
const Print &print,
const std::vector<LayerToPrint> &layers,
const LayerTools &layer_tools,
const bool last_layer,
const size_t single_object_instance_idx);
// Emit the apron bands carried by these layers whose brim filament is extruder_id
// (0-based). Called from both the brim-only branch and the ordinary path, since a
// band's print_z can coincide with another object's layer on a multi-object belt.
std::string emit_belt_brim_bands(
const Print &print,
const std::vector<LayerToPrint> &layers,
const size_t single_object_instance_idx,
const unsigned int extruder_id);
LayerResult process_layer(
const Print &print,
@@ -629,9 +681,21 @@ private:
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
// unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance
// island layout (count and whether the trailing catch-all island has anything to print), and
// the resulting visits.
// The layout is part of the key. Nodes only cover the chainable islands, so two
// layers with the same centroids but a different number of islands (thin walls, negative
// volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1
// of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in
// extrude_perimeters on multi-part objects).
struct IslandOrderCacheEntry
{
std::vector<IslandOrderNode> nodes;
std::vector<std::pair<size_t, bool>> layout;
std::vector<InstanceVisit> visits;
};
std::map<unsigned int, IslandOrderCacheEntry> m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;
@@ -768,7 +832,6 @@ private:
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
std::unique_ptr<SpiralVase> m_spiral_vase;
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
std::unique_ptr<AdaptivePAProcessor> m_pa_processor;
@@ -823,6 +886,25 @@ private:
mutable ConfigIndexCache m_filament_index_cache;
mutable ConfigIndexCache m_nozzle_index_cache;
// Belt brim apron layers only. They have no Layer, so the print_z that
// _extrude() needs for the first-layer-plane probe is published here instead.
// Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set.
std::optional<coordf_t> m_belt_brim_z;
// Belt brim only. Brim and coincident apron bands are emitted before m_layer
// is switched to their object, so belt_height_above_floor() would otherwise
// read the previously visited object's belt description -- making a brim's
// classification depend on plate visiting order. Those paths publish the
// owner here for the duration of the emission. Never left set.
const PrintObject *m_belt_floor_object{nullptr};
struct BeltFloorObjectGuard {
const PrintObject *&slot;
BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; }
~BeltFloorObjectGuard() { slot = nullptr; }
};
// The last extrusion segment was inside the belt's first-layer fan band (see _extrude()).
bool m_belt_in_band{false};
std::set<unsigned int> m_initial_layer_extruders;
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
// BBS
@@ -840,6 +922,46 @@ private:
// On the first printing layer. This flag triggers first layer speeds.
//BBS
bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; }
// Per-point first-layer test. On a belt printer the result depends on the
// supplied slicing-frame point (its height above the belt); otherwise we
// delegate to the legacy per-layer test. This is the entry point used by
// per-path call sites in _extrude.
bool on_first_layer(const Vec3d &point_slicing_mm) const {
double h;
if (this->belt_height_above_floor(point_slicing_mm, h))
return h <= m_config.initial_layer_print_height.value + EPSILON;
return on_first_layer();
}
// "Effective layer index" used to drive layer-count thresholds like
// slow_down_layers. On a belt printer this is the height above the belt in
// first_layer_band_mm() units; otherwise it is the legacy slicing layer index.
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
double h;
if (this->belt_height_above_floor(point_slicing_mm, h)) {
const double lh = this->first_layer_band_mm();
return h <= 0. ? 0 : int(std::floor(h / lh));
}
return on_first_layer() ? 0 : layer_id();
}
// Band thickness for the *effective layer index*: one first layer height, so
// "the first N layers" means the same height above the belt as on a flat bed.
double first_layer_band_mm() const {
const double band = m_config.initial_layer_print_height.value;
return band > 0. ? band : 0.2;
}
// Height of a slicing-frame point above the belt surface, or false when this
// is not a belt print.
//
// The belt surface is known exactly in the slicing frame from the slicing
// parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same
// description the support generator uses, independent of every remap and
// back-transform.
bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const;
// 1 / 0 / -1: the object layer is entirely past the first-layer band above the
// belt / reaches into it / the belt surface is not known for it.
int belt_layer_past_first_layer_band(const Layer *object_layer) const;
int layer_id() const {
if (m_layer == nullptr)
return -1;
+33
View File
@@ -0,0 +1,33 @@
#include "BeltBackTransform.hpp"
#include "../BeltTransform.hpp"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
bool BeltBackTransform::init_from_config(const PrintConfig &config)
{
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value)
return false;
// Build the forward pipeline (the rotation) and store its inverse.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
if (forward.isApprox(Transform3d::Identity()))
return false;
m_inverse = forward.inverse();
m_active = true;
return true;
}
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_inverse * pos;
}
} // namespace Slic3r
+38
View File
@@ -0,0 +1,38 @@
#ifndef slic3r_BeltBackTransform_hpp_
#define slic3r_BeltBackTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt
// printer geometry, converting G-code coordinates from the sliced (rotated)
// frame back to the machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// BeltKinematics::to_machine() before axis remapping.
//
// Active on belt printers with a non-identity pre-slice rotation.
class BeltBackTransform
{
public:
BeltBackTransform() = default;
// Initialize from belt printer config. Rebuilds the same pre-slice rotation
// as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed.
bool init_from_config(const PrintConfig &config);
// Apply the inverse transform to a point. Returns pos unchanged if
// no back-transform is active.
Vec3d apply(const Vec3d &pos) const;
private:
bool m_active = false;
Transform3d m_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_BeltBackTransform_hpp_
+29
View File
@@ -0,0 +1,29 @@
#include "BeltKinematics.hpp"
#include "../BeltTransform.hpp"
#include "../PrintConfig.hpp"
#include "../GCodeWriter.hpp"
#include "../Point.hpp"
#include <memory>
namespace Slic3r {
BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates)
: m_world_coordinates(world_coordinates)
{
m_back_transform.init_from_config(config);
m_machine_frame.init_from_config(config);
}
Vec3d BeltKinematics::to_machine(const Vec3d &p) const
{
const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p);
const Vec3d after_remap = this->apply_axis_remap(after_back);
return m_machine_frame.apply(after_remap);
}
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates)
{
writer.set_kinematics(std::make_unique<BeltKinematics>(config, world_coordinates));
}
} // namespace Slic3r
+59
View File
@@ -0,0 +1,59 @@
#ifndef slic3r_BeltKinematics_hpp_
#define slic3r_BeltKinematics_hpp_
#include "MachineKinematics.hpp"
#include "BeltBackTransform.hpp"
#include "MachineFrameTransform.hpp"
#include "../Point.hpp"
namespace Slic3r {
class PrintConfig;
class GCodeWriter;
// Belt-printer machine frame.
//
// Forward order, as applied per emitted point:
// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) )
//
// i.e. the slicer->world back-transform runs FIRST and the machine-frame
// shear/scale LAST, so the latter acts as a global linear transform on the
// already-placed coordinates.
//
// world_coordinates mode (the PA line / PA pattern calibration generators)
// treats the incoming point as already relative to the belt surface -- X across,
// Y along the belt, Z above it -- and therefore skips the back-transform while
// keeping the remap and the machine frame. It is a different coordinate map, not
// a writer mode, which is why it is fixed at construction.
class BeltKinematics : public CartesianKinematics
{
public:
explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false);
Vec3d to_machine(const Vec3d &p) const override;
// A belt writer has always emitted full XYZ on every move, whether or not any
// individual stage reports itself active. Making this conditional would change
// emitted G-code for an identity-transform belt configuration.
bool must_emit_all_axes() const override { return true; }
bool suppress_lift_at_unknown_position() const override { return true; }
// The machine frame shears and scales, so a circle is an ellipse in machine
// coordinates and G2/G3 cannot describe it.
bool supports_arc_moves() const override { return false; }
private:
BeltBackTransform m_back_transform;
MachineFrameTransform m_machine_frame;
bool m_world_coordinates { false };
};
// Install a belt machine frame on any GCodeWriter. Any axis remap and build
// volume already configured on the writer are carried over, so this may be
// called before or after those setters. Re-calling it with a different
// world_coordinates value swaps the map (used around the PA line generator).
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config,
bool world_coordinates = false);
} // namespace Slic3r
#endif // slic3r_BeltKinematics_hpp_
+31 -2
View File
@@ -18,6 +18,7 @@
#include <iostream>
#include <float.h>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
@@ -46,10 +47,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_g
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
m_extruder_ids.emplace_back(ex.id());
}
}
void CoolingBuffer::reset(const Vec3d &position)
{
m_belt_band_active = false;
// BBS: add I and J axis to store center of arc
m_current_pos.assign(7, 0.f);
m_current_pos[0] = float(position.x());
@@ -89,6 +92,9 @@ struct CoolingLine
// ORCA: Add support for ironing fan speed control
TYPE_IRONING_FAN_START = 1 << 19,
TYPE_IRONING_FAN_END = 1 << 20,
// Belt printers: extrusions within the first-layer band above the belt.
TYPE_BELT_BAND_START = 1 << 21,
TYPE_BELT_BAND_END = 1 << 22,
};
CoolingLine(unsigned int type, size_t line_start, size_t line_end) :
@@ -549,6 +555,10 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
line.type = CoolingLine::TYPE_IRONING_FAN_START;
} else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control
line.type = CoolingLine::TYPE_IRONING_FAN_END;
} else if (boost::starts_with(sline, ";_BELT_BAND_START")) {
line.type = CoolingLine::TYPE_BELT_BAND_START;
} else if (boost::starts_with(sline, ";_BELT_BAND_END")) {
line.type = CoolingLine::TYPE_BELT_BAND_END;
} else if (boost::starts_with(sline, "G4 ")) {
// Parse the wait time.
line.type = CoolingLine::TYPE_G4;
@@ -891,7 +901,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
{CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false},
{CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control
{CoolingLine::TYPE_FORCE_RESUME_FAN, false}};
bool need_set_fan = false;
// Belt printers: a band still open from the previous layer has to take the fan back from
// the layer-level speed issued just above.
bool need_set_fan = m_belt_band_active;
for (const CoolingLine *line : lines) {
const char *line_start = gcode.c_str() + line->line_start;
@@ -905,6 +917,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (new_extruder != m_current_extruder) {
m_current_extruder = new_extruder;
change_extruder_set_fan(true);
if (m_belt_band_active)
need_set_fan = true;
}
}
new_gcode.append(line_start, line_end - line_start);
@@ -957,6 +971,13 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value)
new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed);
}
else if (line->type & CoolingLine::TYPE_BELT_BAND_START) {
m_belt_band_active = true;
need_set_fan = true;
} else if (line->type & CoolingLine::TYPE_BELT_BAND_END) {
m_belt_band_active = false;
need_set_fan = true;
}
else if (line->type & CoolingLine::TYPE_EXTRUDE_END) {
// Just remove this comment.
} else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) {
@@ -1049,7 +1070,15 @@ std::string CoolingBuffer::apply_layer_cooldown(
m_current_fan_speed = speed;
}
};
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
if (m_belt_band_active) {
// Belt printers: a tilted layer runs from the belt to the top of the part, so
// "the first layers" are a band along the belt rather than the first slicing
// layers. Extrusions GCode::_extrude() marks as inside that band print with the
// fan off, whatever overhang, bridge or resume request is pending, as the first
// layers of a flat bed do. Leaving the band falls through to the branches below.
set_fan(0);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
} else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
set_fan(overhang_fan_speed);
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
set_fan(internal_bridge_fan_speed);
+4 -1
View File
@@ -21,7 +21,7 @@ struct PerExtruderAdjustments;
//
// The simple it sounds, the actual implementation is significantly more complex.
// Namely, for a multi-extruder print, each material may require a different cooling logic.
// For example, some materials may not like to print too slowly, while with some materials
// For example, some materials may not like to print too slowly, while with some materials
// we may slow down significantly.
//
class CoolingBuffer {
@@ -63,6 +63,9 @@ private:
unsigned int m_current_nozzle;
//BBS: current fan speed
int m_current_fan_speed;
// Belt printers: the extrusion being processed lies in the first-layer band above the
// belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers.
bool m_belt_band_active = false;
};
}
+133 -19
View File
@@ -2616,6 +2616,9 @@ void GCodeProcessorResult::reset() {
long_retraction_when_cut = false;
timelapse_warning_code = 0;
printable_height = 0.0f;
machine_frame_transform_active = false;
belt_tilt_angle = 0.f;
belt_z_origin = 0.f;
settings_ids.reset();
filaments_count = 0;
backtrace_enabled = false;
@@ -2852,6 +2855,32 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
return ps;
};
// Belt-printer post-gcode shear/scale/post_remap is applied as the final
// step of BeltKinematics::to_machine, so MoveVertex.position is
// in the printer's machine frame. Undo it here so the XY area check
// operates in the build-volume frame that printable_area is defined in
// (the height checks below are skipped on belt printers). For non-belt printers
// (is_active() == false) apply_inverse is identity and behaviour is
// unchanged from before.
const bool machine_frame_active = m_machine_frame_transform.is_active();
auto compare_pos = [&](const GCodeProcessorResult::MoveVertex &move) -> Vec3d {
Vec3d pos = move.position.cast<double>();
if (!machine_frame_active)
return pos;
Vec3d extruder_off = Vec3d::Zero();
if (size_t(move.extruder_id) < m_extruder_offsets.size())
extruder_off = m_extruder_offsets[move.extruder_id].cast<double>();
// Strip plate + extruder offsets to recover the raw machine-frame
// coordinate that was emitted into the G-code (see store_move_vertex).
Vec3d machine(pos.x() - m_x_offset - extruder_off.x(),
pos.y() - m_y_offset - extruder_off.y(),
pos.z() - extruder_off.z() + m_z_offset);
Vec3d build = m_machine_frame_transform.apply_inverse(machine);
// Re-apply plate offset so the result matches plate_printable_poly,
// which is translated by plate_offset below.
return Vec3d(build.x() + m_x_offset, build.y() + m_y_offset, build.z());
};
struct GCodePosInfo
{
Points pos;
@@ -2863,26 +2892,20 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
const Vec3d cp = compare_pos(move);
// For belt printers we read Z from the inverse-transformed position
// (post-origin-snap, pre-machine-frame). Otherwise keep the
// original print_z source (the slicer's layer-Z comment) so
// non-belt behaviour is bit-for-bit unchanged.
const float z_for_height = machine_frame_active ? float(cp.z()) : move.print_z;
if (move.extrusion_role == ExtrusionRole::erCustom) {
/*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) {
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
}
} else {*/
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.position.cast<double>()));
//}
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp));
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom =
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z);
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height);
} else {
/*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) {
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
}
} else {*/
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.position.cast<double>()));
//}
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp));
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
move.print_z);
z_for_height);
}
}
}
@@ -2917,7 +2940,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
valid = false;
}
}
if ( iter->second.max_print_z > plate_printable_height ) { //over height
// Belt printers: the Z recorded here grows with belt travel (machine Z with the
// frame transform, the slicing-frame Z without it), while printable_height is the
// clearance above the belt; the two are not comparable, so the over-height check
// is skipped, as the preview's ToolHeightOutside warning already is.
// Print::validate() checks the object's height against the clearance.
if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height
m_result.gcode_check_result.error_code |= (1 << 3);
std::pair<int, int> filament_to_object_id;
filament_to_object_id.first = iter->first;
@@ -2958,7 +2986,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
}
// check printable height
if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
m_result.gcode_check_result.error_code |= (1 << 1);
std::pair<int, int> filament_to_object_id;
filament_to_object_id.first = iter->first;
@@ -3124,6 +3152,13 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
m_result.printable_height = config.printable_height;
// Belt printer: cache the post-gcode machine-frame transform so the
// multi-extruder validator can undo it and compare against build-volume
// bounds rather than machine-frame positions.
m_machine_frame_transform.init_from_config(config);
m_result.machine_frame_transform_active = m_machine_frame_transform.is_active();
m_belt_printer = config.belt_printer.value;
auto filament_maps = config.option<ConfigOptionInts>("filament_map");
if (filament_maps != nullptr) {
m_filament_maps = filament_maps->values;
@@ -3154,6 +3189,32 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
{
m_parser.apply_config(config);
// Belt printer: remember the file's belt keys for export_config_for_render(). The
// config block lists belt_printer for every printer, so a non-belt file loaded while
// a belt printer is selected switches the preview's belt view off, and a belt file
// loaded on another printer brings its own tilt, remaps and bed along.
m_belt_render_config.clear();
{
const auto *belt = config.option<ConfigOptionBool>("belt_printer");
if (belt != nullptr) {
static const char *belt_keys[] = {
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
};
for (const char *key : belt_keys)
if (const ConfigOption *opt = config.option(key); opt != nullptr)
m_belt_render_config.set_key_value(key, opt->clone());
// The Rev remaps mirror inside the build volume, so the designed view needs
// the bed the file was sliced for. Only a belt file may override it.
static const char *bed_keys[] = { "printable_area", "printable_height" };
if (belt->value)
for (const char *key : bed_keys)
if (const ConfigOption *opt = config.option(key); opt != nullptr)
m_belt_render_config.set_key_value(key, opt->clone());
}
}
//BBS
const ConfigOptionFloatsNullable* nozzle_volume = config.option<ConfigOptionFloatsNullable>("nozzle_volume");
if (nozzle_volume != nullptr) {
@@ -3637,6 +3698,7 @@ void GCodeProcessor::reset()
m_zero_layer_height = 0.0f;
m_first_layer_height = 0.0f;
m_processing_start_custom_gcode = false;
m_in_config_block = false;
m_g1_line_id = 0;
m_layer_id = 0;
m_cp_color.reset();
@@ -3677,6 +3739,7 @@ DynamicConfig GCodeProcessor::export_config_for_render() const
config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values));
config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values));
config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values));
config.apply(m_belt_render_config);
return config;
}
@@ -4242,6 +4305,34 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return;
}
// ;Z: -- the layer Z tag non-BBL printers write. Only read on a belt printer,
// where the preview labels its layers with it (GCodeViewer::load_as_gcode);
// elsewhere print_z stays unset, as it always was, so nothing downstream of
// it changes for other printers.
if (m_belt_printer && boost::starts_with(comment, "Z:")) {
m_print_z = get_z_height(comment);
return;
}
if (boost::starts_with(comment, " CONFIG_BLOCK_START")) {
m_in_config_block = true;
return;
}
if (boost::starts_with(comment, " CONFIG_BLOCK_END")) {
m_in_config_block = false;
return;
}
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
// angle header comment (used to enable the preview's belt view). Only the belt
// header carries it outside the config block; the config block lists the key
// for every printer, belt or not.
if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
try {
m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29))));
} catch (...) {}
return;
}
// wipe start tag
if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) {
m_wiping = true;
@@ -6138,6 +6229,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line)
if (line.has_z()) {
m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor;
any_found = true;
// Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant
// machine-Z origin offset here; the designed-view back-transform subtracts it so
// toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle
// (set from the belt header, parsed before the body) so non-belt G-code processing
// is byte-identical — no unconditional work on the shared path.
if (m_result.belt_tilt_angle != 0.f)
m_result.belt_z_origin = m_origin[Z];
}
if (line.has_e()) {
@@ -7116,6 +7214,22 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
m_result.print_statistics.total_travel_distance += m_travel_dist;
}
// During the start G-code "prepare" stage the toolhead Z is not yet a real
// print height on a normal printer, so it is pinned to the first-layer height
// to keep the preview tidy. Belt printers are the exception: there the Z is
// written explicitly by the belt kinematics and the designed-view back-transform
// couples machine Z into the rendered model Y (the belt tilt mixes the height
// and belt-feed axes). Overriding Z therefore back-transforms the last
// prepare-stage move (the unretract before the first extrusion) to model
// Y ~= 0, and the libvgcode path builder then draws a phantom extrusion
// segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt
// printers so prepare-stage moves map correctly. Gated on belt_tilt_angle (set
// from the G-code header before the body is processed) so non-belt processing
// is byte-identical.
const float store_z = (m_processing_start_custom_gcode && m_result.belt_tilt_angle == 0.f)
? m_first_layer_height
: m_end_position[Z] - m_z_offset;
m_result.moves.push_back({
m_last_line_id,
type,
@@ -7123,7 +7237,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
static_cast<unsigned char>(filament_id),
m_cp_color.current,
//BBS: add plate's offset to the rendering vertices
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[filament_id],
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, store_z) + m_extruder_offsets[filament_id],
static_cast<float>(m_end_position[E] - m_start_position[E]),
m_feedrate,
0.0f, // actual feedrate
+29
View File
@@ -12,6 +12,7 @@
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/GCode/MachineFrameTransform.hpp"
#include <cstddef>
#include <cassert>
@@ -290,6 +291,19 @@ class Print;
bool support_traditional_timelapse{true};
float printable_height;
float z_offset;
// Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation
// header comment; used to enable the preview's belt view.
float belt_tilt_angle{ 0.f };
// Belt printer: machine-Z origin offset (mm) left in m_origin[Z] by the start
// G-code (purge-blob belt advance + G92 Z0 resets). Move positions are stored
// as gcode_Z + this offset, so the designed-view back-transform must subtract it
// to recover the model's belt coordinate.
float belt_z_origin{ 0.f };
// Belt printer: post-gcode shear/scale/post_remap is configured and
// non-identity. When set, the layer Z values in `moves` are in the
// machine frame and should not be compared against `printable_height`
// (which lives in the build-volume frame).
bool machine_frame_transform_active{ false };
SettingsIds settings_ids;
size_t filaments_count;
bool backtrace_enabled;
@@ -385,6 +399,9 @@ class Print;
// Keep the SKIPPABLE per-type time on a copied result.
skippable_part_time = std::forward<Other>(other).skippable_part_time;
initial_layer_time = std::forward<Other>(other).initial_layer_time;
belt_tilt_angle = std::forward<Other>(other).belt_tilt_angle;
belt_z_origin = std::forward<Other>(other).belt_z_origin;
machine_frame_transform_active = std::forward<Other>(other).machine_frame_transform_active;
#if ENABLE_GCODE_VIEWER_STATISTICS
time = std::forward<Other>(other).time;
#endif
@@ -1085,6 +1102,10 @@ class Print;
private:
CommandProcessor m_command_processor;
GCodeReader m_parser;
// Belt printer: the belt keys of the loaded file's config block (plus the bed they
// are relative to), handed to the preview through export_config_for_render() so the
// belt view and its back-transform follow the file, not the selected printer.
DynamicConfig m_belt_render_config;
EUnits m_units;
EPositioningType m_global_positioning_type;
EPositioningType m_e_local_positioning_type;
@@ -1160,6 +1181,13 @@ class Print;
double m_x_offset{ 0 };
double m_y_offset{ 0 };
// Belt-printer post-gcode shear/scale/post_remap. Used by
// check_multi_extruder_gcode_valid to undo the machine-frame
// transform on move positions so bounds checks operate in the
// pre-machine-frame (build-volume) frame.
MachineFrameTransform m_machine_frame_transform;
bool m_belt_printer{ false };
unsigned int m_line_id;
unsigned int m_last_line_id;
float m_feedrate; // mm/s
@@ -1189,6 +1217,7 @@ class Print;
float m_first_layer_height; // mm
float m_zero_layer_height; // mm
bool m_processing_start_custom_gcode;
bool m_in_config_block;
unsigned int m_g1_line_id;
unsigned int m_layer_id;
CpColor m_cp_color;
@@ -0,0 +1,89 @@
#include "MachineFrameTransform.hpp"
#include "../Geometry.hpp"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
#include <cmath>
namespace Slic3r {
bool MachineFrameTransform::init_from_config(const PrintConfig &config)
{
m_active = false;
m_transform = Transform3d::Identity();
m_transform_inverse = Transform3d::Identity();
if (!config.belt_printer.value)
return false;
// The machine-frame transform is derived from the single belt tilt (axis +
// angle) that also drives the pre-slice mesh rotation. Expert decouple lets
// the machine-frame angle differ from the slicing rotation; otherwise both
// use belt_slice_rotation_angle.
const BeltRotationAxis axis = config.belt_slice_rotation.value;
if (axis == BeltRotationAxis::None || axis == BeltRotationAxis::Z)
return false; // Z is an in-plane spin: no machine-frame tilt.
const double angle_deg = config.belt_frame_tilt_decouple.value
? config.belt_frame_tilt_angle.value
: config.belt_slice_rotation_angle.value;
if (std::abs(angle_deg) <= EPSILON)
return false;
const double angle_rad = Geometry::deg2rad(angle_deg);
const double sin_a = std::sin(angle_rad);
if (std::abs(sin_a) <= EPSILON)
return false;
const double cot_a = std::cos(angle_rad) / sin_a;
const double inv_sin = 1.0 / std::abs(sin_a);
// This stage runs after the conventional belt axis swap. For an X-axis
// slicing rotation, remapped Y is model height and remapped Z is travel
// along the belt. Convert those Cartesian coordinates to machine axes with
// the established belt-printer convention:
// machine gantry = model height / sin(a)
// machine belt = model belt + model height * cot(a)
// The Y-rotation case is the same mapping on X/Z, with the rotation sign.
// At 45 degrees tan/cot and sin/cos are equal, which previously hid the
// incorrect complementary-angle formulas used by this unified transform.
Matrix3d shear = Matrix3d::Identity();
Matrix3d scale = Matrix3d::Identity();
if (axis == BeltRotationAxis::X) {
shear(2, 1) = cot_a; // Z from Y
scale(1, 1) = inv_sin; // Y
} else { // BeltRotationAxis::Y
shear(2, 0) = -cot_a; // Z from X
scale(0, 0) = inv_sin; // X
}
// Apply shear first, then scale (the historical default ShearThenScale order:
// result = scale * shear * p). For the canonical 45°/X belt this maps
// (x,y,z) -> (x, y/sin, y + z), matching the previous per-axis config.
Transform3d combined = Transform3d::Identity();
combined.linear() = scale * shear;
if (combined.isApprox(Transform3d::Identity()))
return false;
m_transform = combined;
m_transform_inverse = combined.inverse();
m_active = true;
return true;
}
Vec3d MachineFrameTransform::apply(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_transform * pos;
}
Vec3d MachineFrameTransform::apply_inverse(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_transform_inverse * pos;
}
} // namespace Slic3r
@@ -0,0 +1,54 @@
#ifndef slic3r_MachineFrameTransform_hpp_
#define slic3r_MachineFrameTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Post-stage machine-frame transform for belt printers.
//
// Applied in BeltKinematics::to_machine AFTER the back-transform and
// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code
// coordinates into the printer's physical machine frame.
//
// Derived entirely from the single belt tilt (belt_slice_rotation axis +
// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed
// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert
// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the
// pre-slice rotation angle via belt_frame_tilt_angle.
class MachineFrameTransform
{
public:
MachineFrameTransform() = default;
// Initialize from belt printer config. Returns true if a non-identity
// transform was computed. Inactive when belt_printer is disabled or
// both shear and scale are identity.
bool init_from_config(const PrintConfig &config);
// Apply the transform to a point. Returns pos unchanged if not active.
Vec3d apply(const Vec3d &pos) const;
// Apply the inverse transform. Returns pos unchanged if not active.
// Used by validators that need to compare emitted machine-frame
// coordinates against build-volume bounds.
Vec3d apply_inverse(const Vec3d &pos) const;
bool is_active() const { return m_active; }
// The composed shear*scale transform (identity when inactive). Exposed so the
// G-code viewer can build the machine->model back-transform for the upright
// ("designed") belt preview.
const Transform3d& transform() const { return m_transform; }
private:
bool m_active = false;
Transform3d m_transform = Transform3d::Identity();
Transform3d m_transform_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_MachineFrameTransform_hpp_
+25
View File
@@ -0,0 +1,25 @@
#include "MachineKinematics.hpp"
#include "../Point.hpp"
namespace Slic3r {
// Moved verbatim from GCodeWriter::apply_axis_remap().
Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const
{
if (!has_axis_remap())
return pos;
auto remap = [this, &pos](int r) -> double {
int axis = r % 3;
if (r < 3) return pos[axis];
if (r < 6) return -pos[axis];
return m_build_vol_max[axis] - pos[axis];
};
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
}
Vec3d CartesianKinematics::to_machine(const Vec3d &p) const
{
return this->apply_axis_remap(p);
}
} // namespace Slic3r
+92
View File
@@ -0,0 +1,92 @@
#ifndef slic3r_MachineKinematics_hpp_
#define slic3r_MachineKinematics_hpp_
#include "../Point.hpp"
namespace Slic3r {
// The frame contract for emitted movement.
//
// GCodeWriter produces points in the *logical placed* frame: plate offsets have
// already been subtracted, but no machine-specific mapping has been applied.
// A MachineKinematics turns that into the coordinates actually written to
// G-code, and answers the two structural questions the writer needs in order to
// decide which axis words it may omit.
//
// This is a seam for writer-generated movement only. Start/end/custom G-code,
// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass
// through it: they write machine coordinates directly.
class MachineKinematics
{
public:
virtual ~MachineKinematics() = default;
// Logical placed point -> emitted machine point.
virtual Vec3d to_machine(const Vec3d &p) const = 0;
// True when a move must emit X, Y and Z because omitting a word would be
// wrong under this mapping. Deliberately not called "couples_axes": a pure
// axis permutation forces full emission without physically coupling axes.
virtual bool must_emit_all_axes() const = 0;
// True when a lift must be suppressed while the current position is unknown,
// because _travel_to_z() re-emits the logical X/Y through this mapping and an
// uninitialised position would map to a bogus machine point -- for a reverse
// mapping, the far corner of the bed.
virtual bool suppress_lift_at_unknown_position() const = 0;
// True when a G2/G3 arc in the logical XY plane is still the same arc in the
// machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower
// question than must_emit_all_axes(): whether logical X and Y reach the
// machine unchanged. A mapping that only negates or reverses Z keeps its
// arcs; one that permutes X or Y moves the arc out of the plane that I/J
// describes, and a shear turns the circle into an ellipse G2/G3 cannot
// express at all.
virtual bool supports_arc_moves() const = 0;
// Configuration. GCodeWriter forwards its setters here so that the state
// lives with the strategy and a strategy installed before the setters run
// still receives it.
virtual void set_axis_remap(int rx, int ry, int rz) = 0;
virtual void set_build_volume_max(const Vec3d &max) = 0;
};
// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim.
//
// The remap encodes, per output axis, which source axis feeds it and how:
// r < 3 : source axis r, unchanged
// r < 6 : source axis r-3, negated
// else : source axis r-6, reversed within the build volume
class CartesianKinematics : public MachineKinematics
{
public:
Vec3d to_machine(const Vec3d &p) const override;
bool must_emit_all_axes() const override { return this->has_axis_remap(); }
bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); }
// X and Y must reach the machine untouched. Because the remap is a
// permutation, pinning those two also pins Z to Z, so a mapping that only
// negates or reverses Z still supports arcs -- every word a G2/G3 emits is
// unchanged by it.
bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; }
void set_axis_remap(int rx, int ry, int rz) override
{ m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; }
void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; }
bool has_axis_remap() const
{ return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; }
protected:
Vec3d apply_axis_remap(const Vec3d &pos) const;
int m_remap_x { 0 };
int m_remap_y { 1 };
int m_remap_z { 2 };
Vec3d m_build_vol_max { Vec3d::Zero() };
};
} // namespace Slic3r
#endif // slic3r_MachineKinematics_hpp_
+2 -2
View File
@@ -706,7 +706,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
SeamPosition seam_position = spAligned) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather occlusion meshes: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
indexed_triangle_set triangle_set;
indexed_triangle_set negative_volumes_set;
//add all parts
@@ -796,7 +796,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
for (const ModelVolume *mv : po->model_object()->volumes) {
// Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way
+68 -2
View File
@@ -18,6 +18,7 @@
#include "Utils.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include "../BeltBrim.hpp"
#include <boost/log/trivial.hpp>
#include <vector>
@@ -419,6 +420,10 @@ bool ToolOrdering::insert_wipe_tower_extruder()
{
if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
return false;
// Belt mode has no classic wipe tower; the dedicated wipe tower filament
// must not inject extra toolchanges into the purge prism planning.
if (m_print_config_ptr->belt_printer)
return false;
if (m_print_config_ptr->wipe_tower_filament == 0)
return false;
@@ -516,6 +521,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude
zs.emplace_back(layer->print_z);
for (auto layer : object.support_layers())
zs.emplace_back(layer->print_z);
// Belt brim apron bands sit below the object's first layer and have no
// layer of their own, but tools_for_layer() asserts an exact Z match, so
// their print_z must be part of the ordering.
for (const BeltBrimBand &band : object.belt_brim_prologue())
zs.emplace_back(band.print_z);
this->initialize_layers(zs);
}
@@ -560,6 +570,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
zs.emplace_back(layer->print_z);
for (auto layer : object->support_layers())
zs.emplace_back(layer->print_z);
// See the single-object ctor: belt brim apron bands need their own
// ordering entries or tools_for_layer() will assert.
for (const BeltBrimBand &band : object->belt_brim_prologue())
zs.emplace_back(band.print_z);
max_layer_height = std::max(max_layer_height, object->config().layer_height.value);
}
@@ -994,6 +1008,42 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
}
// Belt brim apron bands own their layers outright: they print below the
// object's first layer, so no object or support layer claims an extruder there
// and process_layer() would bail out at "Nothing to extrude". Claim the
// object's outer wall filament, in the same raw 1-based domain the loops above
// push. Deliberately not layer_tools.has_object, which drives skirt marking
// and wiping overrides.
if (! object.belt_brim_prologue().empty()) {
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
// with the rest of the list later.
const unsigned int brim_filament = object.belt_brim_filament();
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
if (band.fills.empty())
continue;
LayerTools &layer_tools = this->tools_for_layer(band.print_z);
layer_tools.extruders.push_back(brim_filament);
}
}
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
// below it, but that layer can produce no InstanceVisit in process_layer - a
// zero-extrusion lead-in slice with no coinciding support - and the band would then
// be silently dropped. Register the brim filament on every layer that carries a
// coincident band, in the same 1-based domain as the prologue push above, so a brim
// pass always exists there.
if (object.has_belt_brim()) {
const unsigned int brim_filament = object.belt_brim_filament();
const auto &by_layer = object.belt_brim_by_layer();
const size_t n = std::min(by_layer.size(), object.layers().size());
for (size_t i = 0; i < n; ++ i) {
if (by_layer[i].empty())
continue;
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
layer_tools.extruders.push_back(brim_filament);
}
}
for (auto& layer : m_layer_tools) {
// Sort and remove duplicates
sort_remove_duplicates(layer.extruders);
@@ -1036,12 +1086,28 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
}
//FIXME this is a hack to get the ball rolling.
// The `print_z < object_bottom_z` clause reads "below the object" as "raft
// gap". On a belt printer that is wrong: the brim apron legitimately prints
// below the object's first layer, and treating those layers as raft would put a
// wipe tower at negative Z. A belt printer never prints the classic
// prime tower (Print::has_wipe_tower()), so simply drop the clause there.
//
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
// object bottom on a belt printer is legitimately a sub-object stream - brim
// apron, belt support printed below Z0, or the object's own lead-in - and none of
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
// printer outright. Narrowing this to has_belt_brim would reclassify
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
const bool belt_no_raft_gap = config.belt_printer.value;
for (LayerTools &lt : m_layer_tools)
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|| lt.print_z < object_bottom_z + EPSILON;
|| (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON);
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) {
// Skipped on belt printers for the same reason as the clause above: layers
// below the object are brim apron, not raft.
for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) {
const LayerTools &lt = m_layer_tools[i];
const LayerTools &lt_next = m_layer_tools[i + 1];
if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) {
+10 -6
View File
@@ -84,7 +84,17 @@ public:
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
// Returns true if entity is not printed with its usual extruder for a given
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
// belt purge prism uses it to tell which of its fills actually carry purge
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
}
private:
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
@@ -94,12 +104,6 @@ private:
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
// Returns true in case that entity is not printed with its usual extruder for a given copy:
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it == entity_map.end() ? false : it->second[copy_id] != -1;
}
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
// BBS
std::map<const PrintObject*, int> support_map;
+198 -18
View File
@@ -1,6 +1,7 @@
#include "GCodeWriter.hpp"
#include "Config.hpp"
#include "Extruder.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
@@ -31,6 +32,7 @@
#include <sstream>
#include <stdexcept>
#include <math.h>
#include <memory>
#ifdef __APPLE__
#include <boost/spirit/include/karma.hpp>
@@ -43,6 +45,57 @@ namespace Slic3r {
bool GCodeWriter::full_gcode_comment = true;
// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a
// mapping that must emit every axis. While the position is unknown that X/Y is
// the uninitialised origin, which maps to a real but wrong machine point, so the
// lift has to be skipped rather than commanded.
bool GCodeWriter::must_skip_lift_now() const
{
return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear();
}
bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const
{
if (m_first_layer_point_test)
return m_first_layer_point_test(point_logical);
return m_is_first_layer;
}
void GCodeWriter::set_axis_remap(int rx, int ry, int rz)
{
m_remap_x = rx;
m_remap_y = ry;
m_remap_z = rz;
m_kinematics->set_axis_remap(rx, ry, rz);
}
void GCodeWriter::set_build_volume_max(const Vec3d &max)
{
m_build_vol_max = max;
m_kinematics->set_build_volume_max(max);
}
void GCodeWriter::set_kinematics(std::unique_ptr<MachineKinematics> kinematics)
{
assert(kinematics);
m_kinematics = std::move(kinematics);
// Replay whatever was configured on the previous strategy so callers may
// install the kinematics before or after set_axis_remap/set_build_volume_max.
m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z);
m_kinematics->set_build_volume_max(m_build_vol_max);
}
// Kept as the writer-facing name for "this move must emit every axis word".
bool GCodeWriter::has_axis_remap() const
{
return m_kinematics->must_emit_all_axes();
}
Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const
{
return m_kinematics->to_machine(pos);
}
bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor)
{
return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware);
@@ -796,8 +849,14 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
GCodeG1Formatter w;
w.emit_xy(point_on_plate);
auto speed = m_is_first_layer
if (has_axis_remap()) {
// Axis remap may couple XY with Z; emit full XYZ in machine coordinates.
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
w.emit_xyz(machine);
} else {
w.emit_xy(point_on_plate);
}
auto speed = this->point_on_first_layer(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()))
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
//BBS
@@ -810,6 +869,8 @@ it will not perform subsequent lifts, even if Z was raised manually
(i.e. with travel_to_z()) and thus _lifted was reduced. */
std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
{
if (m_force_normal_lift)
lift_type = LiftType::NormalLift;
// check whether the above/below conditions are met
double target_lift = 0;
{
@@ -824,6 +885,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
// BBS
if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) {
if (spiral_vase) {
if (this->must_skip_lift_now())
// Record no lift, so a later unlift() does not descend from a
// height that was never commanded.
return "";
m_lifted = target_lift;
return this->_travel_to_z(m_pos(2) + target_lift, "lift Z");
}
@@ -836,8 +901,9 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
}
// BBS: immediately execute an undelayed lift move with a spiral lift pattern
// designed specifically for subsequent gcode injection (e.g. timelapse)
// designed specifically for subsequent gcode injection (e.g. timelapse)
std::string GCodeWriter::eager_lift(const LiftType type) {
const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type;
std::string lift_move;
double target_lift = 0;
{
@@ -851,7 +917,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
}
// BBS: spiral lift only safe with known position
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
// static spiral alignment when no move in x,y plane.
// spiral centra is a radius distance to the right (y=0)
@@ -868,7 +934,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
}
//BBS: if position is unknown use normal lift
else if (target_lift > 0) {
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
if (this->must_skip_lift_now())
// Skipped, not deferred: leave m_lifted at zero below so unlift()
// does not descend from a height that was never commanded.
target_lift = 0.;
else
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
}
m_lifted = target_lift;
m_to_lift = 0;
@@ -889,7 +960,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
// BBS
Vec3d dest_point = point;
auto travel_speed =
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
this->point_on_first_layer(Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
// See uses_pointwise_travel_speed(): the historical path deliberately emits the
// raw configured speed in the final branch below, ignoring travel_speed.
const double final_travel_speed = this->uses_pointwise_travel_speed()
? travel_speed
: this->config.travel_speed.get_at(m_cached_extruder_idx);
//BBS: a z_hop need to be handle when travel
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
@@ -938,13 +1014,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope());
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
// A slope lift is a straight (linear) diagonal move, so remapping its
// endpoint is exact. Route the destination through apply_axis_remap()
// when a remap is active (no-op at identity).
w0.emit_xyz(has_axis_remap() ? apply_axis_remap(slope_top_point) : slope_top_point);
w0.emit_f(travel_speed * 60.0);
//BBS
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
else if (m_to_lift_type == LiftType::NormalLift) {
else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) {
// Only lift in place when the current position is known, for a mapping
// that makes _travel_to_z re-emit logical X/Y: at print start (and after
// custom gcode) m_pos.xy is still the uninitialised origin, which would
// map to a bogus machine point. The xy_z_move below then travels straight
// to the destination with full XYZ and establishes the correct position.
// Mappings that do not need this (the historical Cartesian behaviour)
// report false and keep lifting unconditionally.
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
@@ -952,7 +1038,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
std::string xy_z_move;
{
GCodeG1Formatter w0;
if (this->is_current_position_clear()) {
if (has_axis_remap()) {
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
w0.emit_xyz(apply_axis_remap(target));
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
xy_z_move = w0.string();
}
else if (this->is_current_position_clear()) {
w0.emit_xyz(target);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
@@ -990,17 +1083,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
std::string out_string;
GCodeG1Formatter w;
if (!this->is_current_position_clear())
if (has_axis_remap()) {
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
w.emit_xyz(apply_axis_remap(point_on_plate));
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
} else if (!this->is_current_position_clear())
{
//force to move xy first then z after filament change
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string() + _travel_to_z(point_on_plate.z(), comment);
} else {
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
}
@@ -1035,12 +1134,19 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
speed = this->point_on_first_layer(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z))
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
GCodeG1Formatter w;
w.emit_z(z);
if (has_axis_remap()) {
// Remap may couple Z with other axes; emit full XYZ.
Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
w.emit_xyz(machine);
} else {
w.emit_z(z);
}
w.emit_f(speed * 60.0);
//BBS
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
@@ -1049,6 +1155,14 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment)
{
// A circular XY arc / spiral lift cannot be correctly axis-remapped by
// transforming only its endpoint: the arc plane (G17/XY) and the I-J center
// would change under the remap. When an axis remap is active, fall back to a
// plain linear lift instead of emitting a possibly-wrong spiral/arc. This
// single guard covers every spiral call site (lazy/eager lift and travel_to_xyz).
if (has_axis_remap())
return _travel_to_z(z, comment);
std::string output;
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
@@ -1155,7 +1269,12 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE,
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
GCodeG1Formatter w;
w.emit_xy(point_on_plate);
if (has_axis_remap()) {
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
w.emit_xyz(machine);
} else {
w.emit_xy(point_on_plate);
}
if (!force_no_extrusion)
w.emit_e(filament()->E());
//BBS
@@ -1163,6 +1282,42 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE,
w.append_to(out);
}
// Approximate an arc with linear extrusions, for machine mappings that cannot
// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre
// relative to the CURRENT position, which is why this must run before m_pos is
// updated.
void GCodeWriter::extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d &center_offset,
double dE, const bool is_ccw,
const std::string &comment, bool force_no_extrusion)
{
const Vec2d start = Vec2d(m_pos.x(), m_pos.y());
const Vec2d centre = start + center_offset;
const double r = (start - centre).norm();
if (r < EPSILON) {
// Degenerate: no arc to speak of, so a single move is exact.
this->extrude_to_xy(out, point, dE, comment, force_no_extrusion);
return;
}
double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x());
double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x());
double sweep = a1 - a0;
if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; }
else { while (sweep >= 0.) sweep -= 2. * PI; }
// Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol.
const double tol = 0.005; // mm
const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r);
const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON))));
for (int i = 1; i <= n; ++ i) {
const double a = a0 + sweep * (double(i) / double(n));
const Vec2d p = (i == n) ? point
: Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a));
this->extrude_to_xy(out, p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion);
}
}
//BBS: generate G2 or G3 extrude which moves by arc
//point is end point which means X and Y axis
//center_offset is I and J axis
@@ -1175,6 +1330,23 @@ std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& cent
void GCodeWriter::extrude_arc_to_xy(std::string &out, const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion)
{
// Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be
// expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops
// arcs being generated at all for such a mapping, but this is public API, so
// define the behaviour rather than asserting.
//
// This check MUST precede every state mutation below: falling through to
// extrude_to_xy() after filament()->extrude(dE) would advance E twice.
//
// A single chord is not a safe substitute either -- a semicircle would become
// its diameter and a full circle a stationary blob -- so approximate the arc
// with linear segments bounded by a chord tolerance, splitting dE between
// them in proportion to arc length.
if (! m_kinematics->supports_arc_moves()) {
this->extrude_arc_as_polyline(out, point, center_offset, dE, is_ccw, comment, force_no_extrusion);
return;
}
m_pos(0) = point(0);
m_pos(1) = point(1);
if (!force_no_extrusion)
@@ -1215,10 +1387,18 @@ void GCodeWriter::extrude_to_xyz(std::string &out, const Vec3d &point, double dE
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
GCodeG1Formatter w;
if (z_changed)
if (has_axis_remap()) {
// z_changed was computed from the ORIGINAL slicing Z, but an axis remap can
// make machine-Z depend on slicing X/Y. An X/Y-only move (slicing-Z
// unchanged) would then drop the required machine-Z word, so always emit
// full XYZ whenever a remap is active.
point_on_plate = apply_axis_remap(point_on_plate);
w.emit_xyz(point_on_plate);
else
} else if (z_changed) {
w.emit_xyz(point_on_plate);
} else {
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
}
if (!force_no_extrusion)
w.emit_e(filament()->E());
//BBS
+101 -28
View File
@@ -10,31 +10,38 @@
#include <string>
#include <charconv>
#include <vector>
#include <functional>
#include <utility>
#include "Extruder.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "Config.hpp"
#include "GCode/MachineKinematics.hpp"
#include <memory>
namespace Slic3r {
class GCodeWriter {
public:
GCodeConfig config;
bool multiple_extruders;
GCodeWriter() :
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_cached_extruder_idx(0),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true),
multiple_extruders(false),
m_lifted(0),
m_to_lift(0),
m_to_lift_type(LiftType::NormalLift),
m_current_speed(3600), m_is_first_layer(true)
m_is_first_layer(true), m_current_speed(3600),
m_kinematics(std::make_unique<CartesianKinematics>()),
m_cached_extruder_idx(0),
m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true)
{}
Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
@@ -91,6 +98,10 @@ public:
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
//BBS: generate G2 or G3 extrude which moves by arc
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
// Linear approximation of an arc, used when the machine mapping cannot
// express a G2/G3. Must be called before m_pos is updated: center_offset is
// relative to the current position.
void extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
// Each appends its line to `out`.
void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
@@ -147,16 +158,94 @@ public:
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
void invalidate_jerk() { m_last_jerk = 0; }
// Axis remap: permute/negate/reverse axes in G-code output.
// Works standalone (without belt mode) for printers with non-standard axis conventions.
void set_axis_remap(int rx, int ry, int rz);
void set_build_volume_max(const Vec3d &max);
bool has_axis_remap() const;
// Install the machine frame mapping. Any axis remap / build volume already
// configured is carried over, so install order does not matter.
void set_kinematics(std::unique_ptr<MachineKinematics> kinematics);
const MachineKinematics& kinematics() const { return *m_kinematics; }
// Per-point first-layer test. When set, travel speed selection asks it per
// destination point (in the writer's logical placed frame) instead of using
// the layer-coarse m_is_first_layer flag. GCode installs it on belt printers
// with the same test its extrusions use (GCode::on_first_layer(point)), so a
// travel is judged against the belt surface exactly as the path it leads to.
using FirstLayerPointTest = std::function<bool(const Vec3d &point_logical)>;
void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); }
// Force every lift to a plain vertical lift. Spiral and slope lifts compute
// their slope in the logical frame and do not account for a machine mapping
// that couples axes.
void set_force_normal_lift(bool force) { m_force_normal_lift = force; }
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
private:
protected:
// Position/lift/offset state.
Vec3d m_pos = Vec3d::Zero();
double m_x_offset{ 0 };
double m_y_offset{ 0 };
double m_lifted;
double m_to_lift;
LiftType m_to_lift_type;
bool m_is_first_layer = true;
bool m_is_current_pos_clear = false;
double m_current_speed;
std::string _travel_to_z(double z, const std::string &comment);
// Whether a destination gets first-layer treatment. With a point test
// installed it decides; otherwise the layer-coarse m_is_first_layer flag does.
bool point_on_first_layer(const Vec3d &point_logical) const;
// True when a lift must be skipped because this mapping would emit the
// stored logical X/Y and that position is not yet known.
bool must_skip_lift_now() const;
// True when travel speed is selected per destination point rather than per
// layer. Set for writers that install a first-layer point test. The
// historical path emits the raw configured travel speed in the final branch
// of travel_to_xyz(), ignoring the first-layer selection computed at the top
// of that function; a point-test-driven writer uses the first-layer-aware
// value throughout. Both are preserved exactly -- unifying them would change
// emitted feedrates and belongs in its own commit.
bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); }
FirstLayerPointTest m_first_layer_point_test;
bool m_force_normal_lift = false;
// The machine frame mapping. Owns the axis-remap state that used to live
// here as m_remap_* / m_build_vol_max; the setters above forward to it.
// Never null: a CartesianKinematics at the identity remap reproduces the
// historical behaviour exactly.
std::unique_ptr<MachineKinematics> m_kinematics;
// Last configured remap / build volume, replayed onto a newly installed
// kinematics so set_kinematics() and the setters are order-independent.
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
int m_remap_y = 1;
int m_remap_z = 2;
Vec3d m_build_vol_max = Vec3d::Zero();
// Apply the machine frame mapping to a point. Returns pos unchanged when the
// mapping is the identity.
Vec3d apply_axis_remap(const Vec3d &pos) const;
// Motion uses the global/base process variant until a filament becomes active.
// Indexes the per-extruder speed options (travel_speed, travel_speed_z,
// initial_layer_travel_speed).
size_t m_cached_extruder_idx;
private:
// Extruders are sorted by their ID, so that binary search is possible.
std::vector<Extruder> m_filament_extruders;
bool m_single_extruder_multi_material;
std::vector<Extruder*> m_curr_filament_extruder;
int m_curr_extruder_id;
// Motion uses the global/base process variant until a filament becomes active.
size_t m_cached_extruder_idx;
unsigned int m_last_acceleration;
unsigned int m_last_travel_acceleration;
std::vector<unsigned int> m_max_travel_acceleration;
@@ -178,19 +267,6 @@ public:
//BBS
int m_last_bed_temperature;
bool m_last_bed_temperature_reached;
double m_lifted;
// BBS
double m_to_lift;
LiftType m_to_lift_type;
Vec3d m_pos = Vec3d::Zero();
//BBS: this flag is used to indicate whether the m_pos is real.
//A example that of the first move, the m_pos is zero, but the real position of extruder doesn't
//Pos must be clear after the first xyz travel move
bool m_is_current_pos_clear = false;
//BBS: x, y offset for gcode generated
double m_x_offset{ 0 };
double m_y_offset{ 0 };
// Orca: slicing resolution in mm
double m_resolution = 0.01;
@@ -202,21 +278,18 @@ public:
// non-rectangular beds such as delta/circular printers.
Polygon m_bed_printable_area;
std::vector<Polygon> m_extruder_printable_areas;
std::string m_gcode_label_objects_start;
std::string m_gcode_label_objects_end;
//SoftFever
bool m_is_bbl_printers = false;
double m_current_speed;
bool m_is_first_layer = true;
enum class Acceleration {
Travel,
Print
};
std::string _travel_to_z(double z, const std::string &comment);
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
const Polygon *active_printable_area() const;
+16 -9
View File
@@ -1260,7 +1260,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
// project downards pointing painted triangles over bottom surfaces.
std::vector<std::vector<Polygons>> top_raw(num_facets_states), bottom_raw(num_facets_states);
std::vector<float> zs = zs_from_layers(layers);
Transform3d object_trafo = print_object.trafo_centered();
Transform3d object_trafo = print_object.trafo_sliced();
#ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM
static int iRun = 0;
@@ -1289,10 +1289,16 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
slicing_params.trafo = volume_trafo;
Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params);
top.erase(top.begin());
bottom.erase(bottom.begin());
bottom[0] = union_(bottom[0], bottom_slice);
// Only the requested projections exist: with
// top_shell_layers = 0 `top` is empty and erasing its begin() was
// undefined (found by fuzzing: a sunk, painted object crashed here).
if (! top.empty())
top.erase(top.begin());
if (! bottom.empty()) {
bottom.erase(bottom.begin());
if (! bottom.empty())
bottom[0] = union_(bottom[0], bottom_slice);
}
} else
slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback);
auto merge = [](std::vector<Polygons> &&src, std::vector<Polygons> &dst) {
@@ -2088,17 +2094,19 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
}
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin";
// The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object.
const Transform3d object_trafo = print_object.trafo_sliced();
for (const ModelVolume *mv : print_object.model_object()->volumes) {
const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv);
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) {
throw_on_cancel_callback();
const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx));
if (!mv->is_model_part() || custom_facets.indices.empty())
continue;
const Transform3f tr = print_object.trafo().cast<float>() * mv->get_matrix().cast<float>();
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
const Transform3f tr = (object_trafo * mv->get_matrix()).cast<float>();
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) {
float min_z = std::numeric_limits<float>::max();
float max_z = std::numeric_limits<float>::lowest();
@@ -2151,7 +2159,6 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())),
Point(scale_(line_end_f.x()), scale_(line_end_f.y())));
line_to_test.translate(-print_object.center_offset());
// BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could
// be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618).
+3 -1
View File
@@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic()
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
if (is_outer_wall_first ||
//BBS: always print outer wall first when there indeed has brim.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
(this->layer_id == 0 &&
this->object_config->brim_type == BrimType::btOuterOnly &&
(this->object_config->brim_type == BrimType::btOuterOnly ||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
entities.reverse();
// Orca: sandwich mode. Apply after 1st layer.
+11 -3
View File
@@ -1238,7 +1238,7 @@ static std::vector<std::string> s_Preset_print_options{
"top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed",
"bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed",
"outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield",
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
"brim_width", "leading_brim_length", "extra_brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
"raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion",
"support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style",
// BBS
@@ -1308,6 +1308,8 @@ static std::vector<std::string> s_Preset_print_options{
"prime_volume",
"prime_tower_infill_gap",
"prime_tower_flat_ironing",
"belt_purge_tower_width",
"belt_purge_tower_object",
"enable_tower_interface_features",
"enable_tower_interface_cooldown_during_tower",
"wipe_tower_no_sparse_layers",
@@ -1558,8 +1560,14 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y",
"belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
"belt_support_floor_offset",
"enable_belt_purge_tower",
"gcode_flavor", "gcode_skip_config_block",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
+314 -50
View File
@@ -69,6 +69,8 @@
#include "Thread.hpp"
#include "Time.hpp"
#include "GCode.hpp"
#include "BeltGCode.hpp"
#include "BeltTransform.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
@@ -77,6 +79,7 @@
#include "MaterialType.hpp"
#include "Model.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include <float.h>
#include <algorithm>
@@ -103,6 +106,7 @@
#include "Format/STEP.hpp"
#include "PlaceholderParser.hpp"
#include "SurfaceCollection.hpp"
#include "BeltBrim.hpp"
namespace fs = boost::filesystem;
@@ -166,6 +170,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Cache the plenty of parameters, which influence the G-code generator only,
// or they are only notes not influencing the generated G-code.
static std::unordered_set<std::string> steps_gcode = {
// Belt printer G-code axis remap (only affects G-code output, not slicing).
"gcode_remap_x",
"gcode_remap_y",
"gcode_remap_z",
// Machine-frame transform (derived from belt tilt; only affects G-code output).
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
// Only inflates the GUI bed volume, like printable_area.
"belt_printer_infinite_y",
//BBS
"additional_cooling_fan_speed",
"reduce_crossing_wall",
@@ -367,8 +379,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Spiral Vase forces different kind of slicing than the normal model:
// In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer.
// Therefore toggling the Spiral Vase on / off requires complete reslicing.
|| opt_key == "spiral_mode") {
|| opt_key == "spiral_mode"
// Build plate tilt changes slicing plane orientation.
|| opt_key == "build_plate_tilt_x"
|| opt_key == "build_plate_tilt_y"
// Belt printer transform options change the mesh geometry before slicing.
|| opt_key == "belt_printer"
|| opt_key == "belt_slice_rotation"
|| opt_key == "belt_slice_rotation_angle") {
osteps.emplace_back(posSlice);
} else if (
opt_key == "belt_support_floor_offset") {
osteps.emplace_back(posSupportMaterial);
} else if (
opt_key == "print_sequence"
|| opt_key == "filament_type"
@@ -403,6 +425,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "hot_plate_temp"
|| opt_key == "textured_plate_temp"
|| opt_key == "enable_prime_tower"
|| opt_key == "enable_belt_purge_tower"
|| opt_key == "enable_wrapping_detection"
|| opt_key == "prime_tower_enable_framework"
|| opt_key == "prime_tower_width"
@@ -437,6 +460,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "prime_volume"
|| opt_key == "flush_into_infill"
|| opt_key == "flush_into_support"
|| opt_key == "belt_purge_tower_width"
|| opt_key == "initial_layer_infill_speed"
|| opt_key == "travel_speed"
|| opt_key == "travel_speed_z"
@@ -668,6 +692,9 @@ std::vector<ObjectID> Print::print_object_ids() const
bool Print::has_infinite_skirt() const
{
// Belt printer: no skirt support.
if (m_config.belt_printer.value)
return false;
// Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed.
// return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1);
@@ -676,6 +703,9 @@ bool Print::has_infinite_skirt() const
bool Print::has_skirt() const
{
// Belt printer: no skirt support.
if (m_config.belt_printer.value)
return false;
return (m_config.skirt_height > 0);
}
@@ -684,6 +714,24 @@ bool Print::has_brim() const
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_brim(); });
}
bool Print::has_tilted_belt() const
{
if (! m_config.belt_printer.value)
return false;
// A Z rotation leaves the belt floor flat (BeltTransform forces shear = 0) and no
// rotation at all means the machine is geometrically a flat bed.
const BeltRotationAxis axis = m_config.belt_slice_rotation.value;
if (axis != BeltRotationAxis::X && axis != BeltRotationAxis::Y)
return false;
const double tilt = std::abs(m_config.belt_slice_rotation_angle.value);
return tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG;
}
bool Print::has_belt_brim() const
{
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_belt_brim(); });
}
//BBS
std::vector<size_t> Print::layers_sorted_for_object(float start, float end, std::vector<LayerPtrs> &layers_of_objects, std::vector<BoundingBox> &boundingBox_for_objects, VecOfPoints &objects_instances_shift)
{
@@ -1803,6 +1851,75 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (extruders.empty())
return { L("No extrusions under current settings.") };
// Belt printer validation: incompatible features.
if (m_config.belt_printer.value) {
for (const PrintObject *object : m_objects) {
if (object->config().raft_layers > 0)
return { L("Raft is not compatible with belt printer mode.") };
}
if (m_config.draft_shield != dsDisabled)
return { L("Draft shield is not compatible with belt printer mode.") };
// Belt brim spans many layers and owns the layers below the object, which
// spiral vase cannot share. The prime tower setting is no obstacle: belt
// printers never print the classic tower, and the belt purge prism is an
// ordinary object that never takes a brim.
if (this->has_belt_brim()) {
if (m_config.spiral_mode.value)
return { L("Brim is not compatible with spiral vase mode on a belt printer. "
"Disable one of them.") };
}
for (const PrintObject *object : m_objects) {
const PrintObjectConfig &ocfg = object->config();
// Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive
// brim_width (leading/extra widen only the outer ring), so keep this
// predicate in step or the belt-brim warnings below would fire for a brim
// that has_belt_brim() rejects.
const bool wants_brim = ocfg.brim_type != btNoBrim
&& (ocfg.brim_type == btInnerOnly
? ocfg.brim_width.value > 0.
: (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|| ocfg.extra_brim_width.value > 0.));
if (! wants_brim)
continue;
if (! this->has_tilted_belt()) {
if (std::abs(m_config.belt_slice_rotation_angle.value) > BELT_BRIM_MAX_TILT_DEG)
warn(L("The belt is too steep for a brim, so no brim will be generated."),
"brim_width", object->model_object());
else
warn(L("A brim is only generated when the belt is tilted. Set a belt tilt angle, "
"or remove the brim setting."),
"brim_type", object->model_object());
}
if (ocfg.brim_type == btAutoBrim || ocfg.brim_type == btEar || ocfg.brim_type == btPainted)
warn(L("Belt printers support outer and inner brim only. Auto, Mouse ear and Painted "
"brim are printed as Outer brim only, using Brim width."),
"brim_type", object->model_object());
if (ocfg.leading_brim_length.value > 0. && ocfg.brim_object_gap.value > 0.)
warn(L("Brim-object gap separates the leading brim from the object's leading edge, "
"which is the edge it is meant to anchor. Set the gap to 0 when using leading "
"brim length."),
"brim_object_gap", object->model_object());
}
if (this->has_belt_brim() && m_objects.size() > 1)
warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does "
"not reserve that space. Leave room between objects."),
"leading_brim_length");
} else {
// "Leading edge only" describes where a part meets a moving belt, so it has no
// meaning on a fixed bed. Brim.cpp prints it as an ordinary outer brim rather
// than silently producing nothing; say so.
for (const PrintObject *object : m_objects)
if (object->config().brim_type == btLeadingEdgeOnly)
warn(L("\"Leading edge only\" brim applies to belt printers. On this printer it is "
"printed as an ordinary outer brim."),
"brim_type", object->model_object());
}
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
// the gradient is dropped silently. extruders() already covers painting, height ranges,
@@ -1868,6 +1985,40 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence == PrintSequence::ByObject
&& extruders.size() > 1) {
StringObjectException warningtemp;
warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; "
"filament changes will not be purged.");
warningtemp.opt_key = "enable_belt_purge_tower";
warningtemp.is_warning = true;
add_warning(warningtemp);
}
// The purge tower is a model object the GUI creates and sizes; libslic3r only purges
// into one that exists. A project sliced without it (the CLI on a project saved before
// the tower was generated) changes filament with nowhere to purge.
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence != PrintSequence::ByObject
&& ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) {
StringObjectException warningtemp;
warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; "
"filament changes will not be purged. Open the project in the application "
"to generate the tower.");
warningtemp.opt_key = "enable_belt_purge_tower";
warningtemp.is_warning = true;
add_warning(warningtemp);
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) {
const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
return object->config().belt_purge_tower_object.value;
});
if (prism_count > 1)
return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")};
}
if (m_config.enable_prime_tower) {
for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value) {
@@ -1921,35 +2072,57 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return profile;
};
// Checks that the print does not exceed the max print height
// Checks that the print does not exceed the max print height.
// For belt printers the slicing-frame Z spans the sheared X-length and
// is not comparable to printable_height (which is gantry clearance in the
// build-volume frame). Compare against the model's pre-shear Z instead,
// mirroring the bbox computed in PrintObject::update_slicing_parameters.
// The machine-frame transform only changes how that height is written to
// G-code, not how much room there is under the gantry.
const bool belt_printer = this->config().belt_printer.value;
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) {
const PrintObject &print_object = *m_objects[print_object_idx];
//FIXME It is quite expensive to generate object layers just to get the print height!
if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
!layers.empty()) {
Vec3d test =this->shrinkage_compensation();
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
if (shrinkage_compensation_z != 1. && layers.back() > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
print_object.model_object(),
""
};
} else if (layers.back() > this->config().printable_height + EPSILON) {
// Test whether the last slicing plane is below or above the print volume.
return StringObjectException{
0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().printable_height + EPSILON ?
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) :
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry."),
print_object.model_object(),
""
};
double effective_max_z = 0;
bool last_layer_below_max = false;
bool have_height = false;
if (belt_printer) {
const double raw_z = print_object.model_object()->max_z();
effective_max_z = raw_z;
have_height = raw_z > 0;
} else {
//FIXME It is quite expensive to generate object layers just to get the print height!
auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
if (!layers.empty()) {
effective_max_z = layers.back();
last_layer_below_max = layers.size() >= 2 &&
0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
have_height = true;
}
}
if (!have_height)
continue;
if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
print_object.model_object(),
""
};
} else if (effective_max_z > this->config().printable_height + EPSILON) {
return StringObjectException{
last_layer_below_max ?
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry.") :
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name),
print_object.model_object(),
""
};
}
}
// Some of the objects has variable layer height applied by painting or by a table.
@@ -1969,12 +2142,12 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return {_u8L("Variable layer height is not supported with Organic supports.") };
}
if (this->has_wipe_tower() && ! m_objects.empty()) {
if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) {
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
// mixed slots from the option; this guards loaded projects and the CLI.
if (m_config.wipe_tower_filament > 0) {
if (this->has_wipe_tower() && m_config.wipe_tower_filament > 0) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
@@ -1996,12 +2169,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
if (! m_config.use_relative_e_distances)
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
// The following two constraints come from the classic wipe tower G-code
// generator; purging into the belt purge prism uses normal object
// extrusions and does not need them.
if (this->has_wipe_tower()) {
if (! m_config.use_relative_e_distances)
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
}
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
#if 0
if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware &&
m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware)
@@ -2728,8 +2906,28 @@ BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std
a += area(slice);
}
}
if (has_brim())
// Guard on `defined`: make_brim() can return before assigning this (it does on
// belt printers, where has_brim() is still true but the plate brim is skipped),
// and overwriting a valid bbox with an undefined one corrupted the first-layer
// centre and the GUI's first-layer area readout.
if (has_brim() && firstLayerObjectBrimBoundingBox.defined)
bbox = firstLayerObjectBrimBoundingBox;
// Belt brim: the apron reaches ahead of the object along the belt.
if (has_belt_brim()) {
const Point shift = instances().empty() ? Point(0, 0) : instances()[0].shift_without_plate_offset();
for (const ExPolygons &areas : m_belt_brim_areas_by_layer)
for (const ExPolygon &ex : areas) {
BoundingBox bb = get_extents(ex.contour);
bb.translate(shift.x(), shift.y());
bbox.merge(bb);
}
for (const BeltBrimBand &band : m_belt_brim_prologue)
for (const ExPolygon &ex : band.areas) {
BoundingBox bb = get_extents(ex.contour);
bb.translate(shift.x(), shift.y());
bbox.merge(bb);
}
}
return bbox;
}
@@ -2776,6 +2974,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
// Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's
// layers and drops its unclaimed fills, stashing both so a replan can undo
// them. The object steps below regenerate per-layer content over m_layers
// ONLY, so if any of them is about to rerun the stashes must go back first;
// otherwise truncated layers keep stale perimeters/fills and dropped fills
// are re-inserted next to freshly generated ones. Every object-step
// invalidation also invalidates psWipeTower, so "psWipeTower not done" is
// exactly "some object step may rerun" -- and when it IS done nothing below
// regenerates, and the plan's edits have to stay.
if (!this->is_step_done(psWipeTower))
for (PrintObject *obj : m_objects)
obj->belt_undo_purge_plan();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
@@ -2837,15 +3048,20 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
int object_count = m_objects.size();
std::set<PrintObject*> need_slicing_objects;
std::set<PrintObject*> re_slicing_objects;
// Belt global modes couple each object's bed position into its layer Z values,
// so sharing layers between "identical" objects is wrong.
bool belt_no_share = m_config.belt_printer.value;
if (!use_cache) {
for (int index = 0; index < object_count; index++)
{
PrintObject *obj = m_objects[index];
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
break;
if (!belt_no_share) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
break;
}
}
}
if (!obj->get_shared_object())
@@ -2864,12 +3080,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
PrintObject *obj = m_objects[index];
bool found_shared = false;
if (need_slicing_objects.find(obj) == need_slicing_objects.end()) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
found_shared = true;
break;
if (!belt_no_share) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
found_shared = true;
break;
}
}
}
if (!found_shared) {
@@ -2992,7 +3210,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (int i = range.begin(); i < range.end(); i++) {
PrintObject* obj = m_objects[i];
if (need_slicing_objects.count(obj) != 0) {
obj->generate_support_material();
// The belt brim follows sequentially below.
obj->generate_support_material(false);
}
else {
if (obj->set_started(posSupportMaterial))
@@ -3001,6 +3220,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
}
);
// The belt brim keeps clear of every object's layers and support layers,
// so it runs once no support step is rebuilding them any more.
for (PrintObject *obj : m_objects)
obj->generate_belt_brim();
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
@@ -3077,7 +3300,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
m_wipe_tower_data.clear();
m_tool_ordering.clear();
if (this->has_wipe_tower()) {
if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) {
this->_plan_belt_purge();
}
else if (this->has_wipe_tower()) {
this->_make_wipe_tower();
}
else if (this->config().print_sequence != PrintSequence::ByObject) {
@@ -3331,6 +3557,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
// Belt brim: bound the first-layer convex hull by the lowest apron band, so
// bed levelling and the initial purge line account for brim that reaches
// ahead of every object.
if (this->has_belt_brim()) {
for (PrintObject *object : m_objects) {
if (! object->has_belt_brim() || object->belt_brim_prologue().empty())
continue;
const BeltBrimBand &lowest = object->belt_brim_prologue().front();
for (const PrintInstance &instance : object->instances())
for (const ExPolygon &ex : lowest.areas) {
Polygon poly = ex.contour;
poly.translate(instance.shift);
append(m_first_layer_convex_hull.points, std::move(poly.points));
}
}
}
// Unchanged for belt printers: _make_skirt() already returns early for them, and
// the belt brim does not populate m_brimMapByInstance, which is what the
// skirt/brim grouping reads.
if (has_skirt() || has_infinite_skirt() || has_brim()) {
// Generate skirt/brim groups after brim so per-object and draft-shield footprints
// include brims when grouping and offsetting skirt loops.
@@ -3434,12 +3680,17 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
this->set_status(80, message);
// The following line may die for multiple reasons.
GCode gcode;
// Factory: use BeltGCode for belt printers, plain GCode otherwise.
std::unique_ptr<GCode> gcode;
if (m_config.belt_printer.value)
gcode = std::make_unique<BeltGCode>();
else
gcode = std::make_unique<GCode>();
//BBS: compute plate offset for gcode-generator
const Vec3d origin = this->get_plate_origin();
gcode.set_gcode_offset(origin(0), origin(1));
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
gcode.export_layer_filaments(result);
gcode->set_gcode_offset(origin(0), origin(1));
gcode->do_export(this, path.c_str(), result, thumbnail_cb);
gcode->export_layer_filaments(result);
//BBS
if (result != nullptr) {
result->conflict_result = m_conflict_result;
@@ -3454,6 +3705,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
void Print::_make_skirt()
{
// Belt printer: skirt is not compatible.
if (m_config.belt_printer.value)
return;
const bool generate_skirt = this->has_skirt() || this->has_infinite_skirt();
// First off we need to decide how tall the skirt must be.
@@ -4502,6 +4757,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
// Wipe tower support.
bool Print::has_wipe_tower() const
{
// Belt printers never get the classic wipe tower: its G-code is generated
// directly in machine XY coordinates and bypasses the belt rotation
// transform. Purging is routed into the belt purge prism instead
// (see has_belt_purge_tower() / _plan_belt_purge()).
if (m_config.belt_printer.value)
return false;
if (m_config.enable_prime_tower.value == true) {
if (m_config.enable_wrapping_detection.value && m_config.wrapping_exclude_area.values.size() > 2)
return true;
@@ -4514,6 +4776,7 @@ bool Print::has_wipe_tower() const
return false;
}
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
@@ -4543,6 +4806,7 @@ bool Print::enable_timelapse_print() const
return m_config.timelapse_type.value == TimelapseType::tlSmooth;
}
void Print::_make_wipe_tower()
{
m_wipe_tower_data.clear();

Some files were not shown because too many files have changed in this diff Show More