mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 15:28:15 +03:00
Compare commits
40 Commits
belt-print
...
feat/print
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07c62112b8 | ||
|
|
f23e4963bf | ||
|
|
95279f7084 | ||
|
|
1014558c91 | ||
|
|
6515062a3a | ||
|
|
8153e26b6d | ||
|
|
79fd33d49a | ||
|
|
cdb662d4b2 | ||
|
|
a27fa7b5df | ||
|
|
cdeed107c5 | ||
|
|
be92f7e78c | ||
|
|
76f9de4cfb | ||
|
|
cd6d2cbf5e | ||
|
|
deaca0189f | ||
|
|
7d54ee286d | ||
|
|
dddfc7a8ad | ||
|
|
673bfb0dee | ||
|
|
b9e8c5a38c | ||
|
|
a2a4ca20e4 | ||
|
|
12075459d2 | ||
|
|
f3770f3106 | ||
|
|
90fbf1770f | ||
|
|
bde94ab37f | ||
|
|
6d25e1777e | ||
|
|
7ce26ca8e5 | ||
|
|
367fcce634 | ||
|
|
9b3a44b339 | ||
|
|
6384191102 | ||
|
|
6b5c8af1c8 | ||
|
|
e72a3a65b2 | ||
|
|
1b71835337 | ||
|
|
f9fa1c117f | ||
|
|
6f3ca7d1b9 | ||
|
|
13ae3a1c90 | ||
|
|
fb36d5e73b | ||
|
|
abb2ab8d3f | ||
|
|
95b781745d | ||
|
|
80e64f80a6 | ||
|
|
477208a969 | ||
|
|
2e08b19d6b |
@@ -80,8 +80,12 @@ endif()
|
||||
|
||||
if (DEFINED BBL_RELEASE_TO_PUBLIC)
|
||||
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=${BBL_RELEASE_TO_PUBLIC}")
|
||||
if (BBL_RELEASE_TO_PUBLIC)
|
||||
add_compile_definitions(WXINSPECTOR_DISABLE)
|
||||
endif ()
|
||||
else ()
|
||||
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=$<CONFIG:Release>")
|
||||
add_compile_definitions("$<$<CONFIG:Release>:WXINSPECTOR_DISABLE>")
|
||||
endif ()
|
||||
|
||||
find_package(Git)
|
||||
|
||||
8
deps/OpenSSL/OpenSSL.cmake
vendored
8
deps/OpenSSL/OpenSSL.cmake
vendored
@@ -52,6 +52,14 @@ ExternalProject_Add(dep_OpenSSL
|
||||
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
|
||||
"--openssldir=${DESTDIR}"
|
||||
"--prefix=${DESTDIR}"
|
||||
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
|
||||
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
|
||||
# --with-openssl only ever emits -L<dir>/lib, so it misses the bundled
|
||||
# static libs and silently links the system OpenSSL instead -- which,
|
||||
# against 1.1.1w headers, leaves _ssl.so with an undefined
|
||||
# SSL_get_peer_certificate (removed in OpenSSL 3.x). Pin libdir so the
|
||||
# prefix stays single-layout.
|
||||
"--libdir=lib"
|
||||
${_cross_comp_prefix_line}
|
||||
no-shared
|
||||
no-asm
|
||||
|
||||
72
docs/printer-agent/README.md
Normal file
72
docs/printer-agent/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Printer agents
|
||||
|
||||
Printer agents let OrcaSlicer communicate with printers through a
|
||||
standardized protocol. They translate between a printer's
|
||||
native API and the application interfaces that the app already
|
||||
uses.
|
||||
|
||||
This documentation explains the compatibility boundary, runtime ownership,
|
||||
connection and status flow, command and feature behavior, built-in and plugin
|
||||
agent implementations, and the testing evidence required for compatibility
|
||||
claims.
|
||||
|
||||
## What printer agents do
|
||||
|
||||
A printer agent has two jobs:
|
||||
|
||||
1. Accept the app's existing commands and translate the ones its
|
||||
printer supports.
|
||||
2. Convert native printer status into correctly-shaped state that
|
||||
`MachineObject` understands.
|
||||
|
||||
Currently, agents work at a compatibility boundary, i.e., making other vendors compatible with Bambu-shaped code, not a vendor-neutral one.
|
||||
Some Bambu concepts remain part of the payload and command vocabulary.
|
||||
End goal is to make the whole command and payload interfaces vendor-neutral.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
Every chapter reuses these terms. The "Is not" column is the part that
|
||||
causes confusion when it is left implicit.
|
||||
|
||||
| Term | Is | Selected by | Is not |
|
||||
| --- | --- | --- | --- |
|
||||
| Agent ID | Which printer agent implementation to use | `printer_agent` on the printer preset; empty is the legacy `bbl`-or-`orca` sentinel | Which printer |
|
||||
| Printer agent | The live `IPrinterAgent` instance for that ID, created and cached once per ID by `NetworkAgentFactory` | Factory lookup on the agent ID | A connection, and not one object per printer |
|
||||
| Device ID | One printer inside that implementation | Bind with Access Code for the Moonraker family, where the entered address becomes the ID; Bambu uses its own discovery identity | Which protocol |
|
||||
| `MachineObject` | The Device tab's view of one selected printer | `DeviceManager::selected_machine`, which stores only an ID | Proof that a printer is reachable |
|
||||
| Freshness | `is_connected()`, a test over the last-update time | Any reset of the update time, including one no status has followed | Proof that status arrived |
|
||||
| Status-confirmed readiness | A push-status message has actually been parsed | The first real status message | The same thing as a successful `connect_printer()` |
|
||||
|
||||
Earlier drafts used "transport" for the printer agent instance. That term
|
||||
is retired: the code selects an implementation, not a wire protocol.
|
||||
|
||||
## How to use this guide
|
||||
|
||||
- [Architecture](architecture.md) describes objects, ownership, lifetimes,
|
||||
error handling, the feature gate, and compatibility contracts for printer
|
||||
agents.
|
||||
- [Connection and status](connection-and-status.md) describes how presets,
|
||||
machines, access codes, status messages, and commands fit together at
|
||||
runtime. Unlike Architecture, it follows the sequence of selecting an
|
||||
agent, connecting, receiving status, and sending commands.
|
||||
- [Printing](printing.md), [filament synchronization](filament.md), and
|
||||
[camera support](camera.md) are separate chapters because they contain
|
||||
per-feature detail rather than because they are universally special:
|
||||
Printing has its send, preflight, recovery, and start contracts; Filament
|
||||
covers acquisition, mapping selection, and print-time delivery; Camera
|
||||
covers the distinct Bambu, Moonraker, and Snapmaker ownership models.
|
||||
- [Built-in agents](agents.md) describes the Moonraker family and the Qidi,
|
||||
Snapmaker variants.
|
||||
- [Python plugin agents](plugin-agents.md) describes the plugin bridge and
|
||||
lifecycle.
|
||||
- [Testing and troubleshooting](testing.md) explains automated checks, manual
|
||||
hardware work, known defects, and the evidence required for compatibility
|
||||
claims.
|
||||
- The [capability matrix](reference/capability-matrix.md) is the compact
|
||||
feature reference. The [manual checklist](reference/manual-checklist.html)
|
||||
is for a live-printer verification pass.
|
||||
|
||||
Treat source code as authoritative when it differs from this guide. In
|
||||
particular, preserve the compatibility rules called out in each chapter:
|
||||
they protect stored presets, existing profiles, and the Device tab's
|
||||
assumptions.
|
||||
205
docs/printer-agent/agents.md
Normal file
205
docs/printer-agent/agents.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Built-in printer agents
|
||||
|
||||
*Owns the per-vendor behavior of the built-in agents: what each subclass
|
||||
changes and what it inherits unchanged. Defers the interface every agent
|
||||
implements to [Architecture](architecture.md) and
|
||||
[Python plugin agents](plugin-agents.md).*
|
||||
|
||||
This chapter covers the built-in Moonraker family: the general
|
||||
`MoonrakerPrinterAgent` and the Qidi and Snapmaker variants. Creality
|
||||
(`CrealityPrintAgent`) is also a member of this family and inherits the base
|
||||
behavior, but has no section here; see the capability matrix for its
|
||||
per-feature coverage. They share the same connection and status machinery.
|
||||
Change the base class only when the behavior is valid for all of them.
|
||||
|
||||
Each subclass is thin. `MoonrakerPrinterAgent` holds the HTTP connection,
|
||||
the WebSocket status subscription, the REST command worker, thumbnail
|
||||
lookup, the chamber-light heuristic, and the upload-and-start path.
|
||||
`QidiPrinterAgent` overrides filament discovery and adds multi-color box
|
||||
mapping; `SnapmakerPrinterAgent` overrides filament discovery and camera
|
||||
setup; `CrealityPrintAgent` overrides filament refresh. Each derives from
|
||||
`MoonrakerPrinterAgent` and is `final`, which is why the guard rule below
|
||||
must be type-based.
|
||||
|
||||
## Moonraker family
|
||||
|
||||
### Connection and commands
|
||||
|
||||
Moonraker-family agents use plain HTTP for the LAN connection. The connection
|
||||
path deliberately ignores a TLS request because the supported printer stacks
|
||||
serve Moonraker or a reverse proxy over HTTP. Restoring the caller's TLS
|
||||
default can send a connection to an unavailable HTTPS endpoint.
|
||||
|
||||
Status is a Moonraker WebSocket subscription. Commands use REST. Command
|
||||
translation happens immediately, but the resulting HTTP work runs through one
|
||||
agent-owned FIFO worker. Each queued operation captures the current base URL
|
||||
and API key before it is queued, so a later printer switch does not redirect
|
||||
an earlier command. Keep this separation: network work on the UI path makes
|
||||
controls feel stalled, and allowing a queued command to reread connection
|
||||
state can send it to the wrong printer.
|
||||
|
||||
Pause, resume, and cancel use the dedicated Moonraker print endpoints. Do not
|
||||
replace them with queued `PAUSE`, `RESUME`, or `CANCEL_PRINT` G-code. The
|
||||
endpoints interrupt the print directly; a G-code command can wait behind the
|
||||
active print or macro.
|
||||
|
||||
The request router accepts the Bambu-shaped JSON used by the native device
|
||||
tab. Supply object-shaped namespaces such as `print` and `system`. A malformed
|
||||
but parseable payload with a scalar where the router expects an object can
|
||||
still fail before the unsupported-command fallback. The supported generic fan
|
||||
status is the standard `fan` object, which represents the part fan only.
|
||||
Ordinary part-fan control also works through the legacy `gcode_line` path,
|
||||
which sends `M106` while `is_enable_np` is false. Auxiliary and chamber fans
|
||||
are neither reported nor controlled.
|
||||
|
||||
Do not add `cfg`, `fun`, `aux`, and `stat` to the Moonraker status payload just
|
||||
to make it look more complete. Together those fields set `is_enable_np` and
|
||||
make the UI choose its structured fan and extruder commands instead. The
|
||||
Moonraker agent does not translate those commands, so working controls become
|
||||
unsupported no-ops. This is a UI-routing constraint, not a reason to expose
|
||||
structured fan support.
|
||||
|
||||
### Status shown by the native device tab
|
||||
|
||||
The agent translates Moonraker status into the Bambu-shaped status payload the
|
||||
existing Device tab understands. Some fields are necessarily synthetic:
|
||||
|
||||
- The virtual SD-card readiness bit and a basic software-version row make the
|
||||
native UI consider the printer ready. Each pull payload also ensures
|
||||
`m_push_count` and `m_full_msg_count` are at least one and refreshes
|
||||
`last_push_time`. Together with the normal-storage state and a placeholder
|
||||
module version, this satisfies the native `is_info_ready()` and printing
|
||||
gates. These are compatibility scaffolding, not reports of physical storage
|
||||
or OTA support.
|
||||
- Current and total layers are emitted only when `print_stats.info` contains
|
||||
numeric values. Moonraker may send `null`, and many profiles do not emit the
|
||||
`SET_PRINT_STATS_INFO` data needed to populate them. Do not turn that gap
|
||||
into a JSON conversion exception.
|
||||
- Remaining time is estimated from elapsed print time and virtual-SD progress.
|
||||
It is omitted below two percent progress because the early estimate is too
|
||||
unstable. Do not derive an ETA by subtracting Moonraker duration counters:
|
||||
both are elapsed counters, so their difference is overhead, not remaining
|
||||
time.
|
||||
- Temperature readings are available, but nozzle diameter and nozzle type are
|
||||
not supplied in the status payload. The UI can therefore show an unknown
|
||||
nozzle. Do not make print submission depend on those missing fields.
|
||||
|
||||
### Camera thumbnails and lights
|
||||
|
||||
For a running job, the agent asks Moonraker for thumbnails and chooses the
|
||||
widest usable entry, rather than assuming the first entry is useful. It accepts
|
||||
both thumbnail path spellings used by Moonraker versions, encodes each path
|
||||
segment, and caches the result by filename. A failed transient lookup is tried
|
||||
again only a bounded number of times; a clean response without a thumbnail is
|
||||
cached as a negative result. The response shape handling is source-derived,
|
||||
not hardware-verified.
|
||||
|
||||
> **Do not perform this HTTP lookup while holding `payload_mutex`.** The
|
||||
> WebSocket thread builds the status payload under that mutex and the UI
|
||||
> path also needs it, so a thumbnail timeout taken under the lock would
|
||||
> stall status delivery or the UI. The lookup still blocks the WebSocket
|
||||
> thread briefly, so move it to a worker if that becomes measurable.
|
||||
|
||||
Chamber-light control searches Moonraker objects for names that look like a
|
||||
light or a standalone LED, then writes the first matching pin, LED, or macro.
|
||||
The filter exists to avoid treating unrelated objects, such as a beeper, as a
|
||||
lamp. It remains a heuristic. The incoming `led_node` is validated, but only
|
||||
`chamber_light` is acted on; `chamber_light2` is deliberately ignored. A
|
||||
printer with more than one lamp therefore has no reliable node-to-object map.
|
||||
|
||||
### Common maintenance limits
|
||||
|
||||
The same cache is reused for a selected agent ID, not per physical printer.
|
||||
Qidi and Snapmaker inherit this behavior. A stateful feature added
|
||||
to the base class must be reset carefully when a preset switches hosts.
|
||||
|
||||
> **Keep guards for this family type-based** - check whether an agent
|
||||
> derives from `MoonrakerPrinterAgent` rather than comparing its ID to
|
||||
> `moonraker`. An ID-based guard silently excludes Qidi, Snapmaker, and
|
||||
> Creality, even though they share the base behavior.
|
||||
|
||||
The family has no generic implementation for firmware-specific AMS write
|
||||
commands. Keep unsupported commands unsupported until the printer-side macro
|
||||
or API is known. Reporting success for an untranslated command makes the
|
||||
native UI claim that an action happened when it did not.
|
||||
|
||||
## Qidi
|
||||
|
||||
Qidi inherits the Moonraker connection, status, camera, and local-print path.
|
||||
Its differences are Qidi filament discovery and the pre-print multi-color-box
|
||||
mapping.
|
||||
|
||||
### Filament discovery
|
||||
|
||||
Discovery first reads the printer's device information to infer a Qidi series
|
||||
identifier, then falls back to the configured Orca model if needed. Series
|
||||
inference intentionally recognizes only a narrow set of known names. An
|
||||
unknown model still produces usable generic filament data, but not a
|
||||
series-specific preset identifier.
|
||||
|
||||
The agent reads a Qidi filament dictionary and the `save_variables` plus
|
||||
slot-runout data. Failing to fetch the dictionary is non-fatal: slot discovery
|
||||
continues with fallback material and colour values. Failing to fetch or parse
|
||||
slot data is fatal to the refresh. A missing runout value means the agent
|
||||
cannot prove filament is loaded, so it reports that slot as empty. This is an
|
||||
ambiguity in the firmware data, not proof that the box is empty.
|
||||
|
||||
`save_variables.variables` must be an object. Qidi firmware can return `null`
|
||||
there, and generic JSON value access can throw on a present null. The parser
|
||||
rejects that shape without throwing. Preserve the null-slot tests whenever the
|
||||
response parser changes.
|
||||
|
||||
### Multi-color mapping before a print
|
||||
|
||||
Before every Qidi print-start wrapper, the agent writes `enable_box` and, for
|
||||
mapped tools, persistent `value_t<tool>` variables. These writes survive the
|
||||
job. Invalid mapping JSON is checked only after `enable_box` has been written.
|
||||
When the mapping is enabled, that failure can therefore leave `enable_box=1`.
|
||||
There is no rollback for this or for a later per-tool write failure, so a
|
||||
partial mapping can remain on the printer. An empty mapping is accepted when
|
||||
the box is enabled. Single-colour jobs disable the box but leave old per-tool
|
||||
assignments in place.
|
||||
|
||||
`enable_box` currently follows `task_use_ams`. That meaning has not been
|
||||
verified against all Qidi firmware: if firmware treats it as "a box exists"
|
||||
rather than "use the box for this job", this gate is wrong and needs hardware
|
||||
evidence before it changes.
|
||||
|
||||
Only `start_local_print` reaches Moonraker's real upload-and-start path. The
|
||||
other Qidi mapping wrappers currently return success stubs after applying the
|
||||
mapping. Do not describe those wrappers as confirmed print paths.
|
||||
|
||||
Because the agent cache is keyed by agent type, a Qidi mapping can also become
|
||||
stale when switching between Qidi printers. This is a generic Moonraker-family
|
||||
state risk, made more consequential by Qidi's persistent firmware variables.
|
||||
The configured `printer_type` can also be stale, so treat it as a fallback
|
||||
hint rather than device truth.
|
||||
|
||||
## Snapmaker
|
||||
|
||||
Snapmaker uses the Moonraker base and overrides filament discovery and camera
|
||||
setup. Neither path is hardware-verified in the current documentation set.
|
||||
|
||||
Filament information comes from parallel arrays in `print_task_config`.
|
||||
`filament_exist` defines the number of slots; shorter type, subtype, colour,
|
||||
vendor, or NFC arrays use safe fallback values. The agent first tries a visible
|
||||
vendor, type, and colour preset, then a visible type match, and finally a
|
||||
generic identifier when no preset bundle is available. An empty reported type
|
||||
is changed to `PLA`, so an unknown occupied spool can look like confirmed PLA.
|
||||
An unrecognized type can also reach the visible-preset fallback and be paired
|
||||
with an unrelated visible preset. Treat the resulting preset as a suggestion,
|
||||
not printer-ground truth.
|
||||
|
||||
Snapmaker U1 camera support starts the printer's monitor RPC, then serves the
|
||||
still JPEG through a small local HTML page that reloads it after each load or
|
||||
error. The wrapper is required because a direct still-image URL looks frozen.
|
||||
The RPC is sent from a detached thread so the UI timer does not block on socket
|
||||
I/O. That thread captures `this` directly, so agent destruction can race with
|
||||
the camera command. Do not widen this pattern. Route future asynchronous work
|
||||
through owned lifetime-managed work where possible.
|
||||
|
||||
## Source locations
|
||||
|
||||
- `src/slic3r/Utils/MoonrakerPrinterAgent.cpp`
|
||||
- `src/slic3r/Utils/QidiPrinterAgent.cpp`
|
||||
- `src/slic3r/Utils/SnapmakerPrinterAgent.cpp`
|
||||
158
docs/printer-agent/architecture.md
Normal file
158
docs/printer-agent/architecture.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Architecture
|
||||
|
||||
*Owns the structural rules: what the objects are, who owns them, what an
|
||||
agent must implement, and which behaviors are compatibility contracts.
|
||||
Defers the runtime sequence - selecting, connecting, receiving status,
|
||||
sending commands - to [Connection and status](connection-and-status.md).*
|
||||
|
||||
## The compatibility boundary
|
||||
|
||||
The Device tab was built around Bambu-style commands and status. A printer
|
||||
agent is the translation boundary between that existing contract and a
|
||||
vendor's native protocol:
|
||||
|
||||
```text
|
||||
Device tab <-> MachineObject <-> NetworkAgent <-> IPrinterAgent
|
||||
<-> vendor protocol
|
||||
```
|
||||
|
||||
Note: end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
|
||||
|
||||
The GUI builds commands and reads `MachineObject` state. An agent owns the
|
||||
vendor request, response, connection, and status translation. Keep vendor
|
||||
details on the agent side of this boundary.
|
||||
|
||||
Status translation is deliberately Bambu-shaped. Agents deliver payloads
|
||||
through the callbacks used by the existing Bambu path, and
|
||||
`MachineObject::parse_json()` interprets them. This preserves the Device
|
||||
tab's established behavior, but it is not a vendor-neutral protocol.
|
||||
|
||||
Important (again): end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
|
||||
|
||||
## Runtime objects and ownership
|
||||
|
||||
`NetworkAgent` is the facade used by the application. It holds one live
|
||||
`IPrinterAgent` pointer, which is initially null and may return to null
|
||||
when a selected ID is unavailable. Callers must handle the null case. An
|
||||
absent agent is an inert state, not permission to fall back to another
|
||||
printer agent. A fallback would connect to a different implementation than
|
||||
the one selected by the preset, and could therefore send commands or status
|
||||
work to the wrong printer.
|
||||
|
||||
`NetworkAgentFactory` registers built-in and plugin implementations by
|
||||
agent ID. It creates and caches one implementation for each ID. The ID
|
||||
selects a printer agent implementation, while a `MachineObject` selects one
|
||||
printer by device ID. The resulting cardinality is one active agent to many
|
||||
machines.
|
||||
|
||||
For example, suppose two Moonraker printers are on the LAN at
|
||||
`192.168.1.20` and `192.168.1.21`. In the Device tab machine-select popup, the
|
||||
user chooses **Bind with Access Code**; `PinCodePanel::on_mouse_left_up` opens
|
||||
`InputIpAddressDialog`, and each entered address is bound as a separate
|
||||
printer. Both presets store the same agent ID, `moonraker`, so
|
||||
`NetworkAgentFactory::create_printer_agent_by_id` returns the same cached
|
||||
`IPrinterAgent` pointer for both presets. Each printer nevertheless has its
|
||||
own `MachineObject` and device ID. For the Moonraker family,
|
||||
`MoonrakerPrinterAgent::bind_detect` calls `init_device_info` with the entered
|
||||
address as both the device ID and address, so the two device IDs are the two
|
||||
addresses.
|
||||
|
||||
That is what one active agent to many machines means. Per-printer state must
|
||||
be keyed by device ID rather than held only on the agent instance, because one
|
||||
agent object is shared by both printers. State stored only on that object
|
||||
would be shared between two different machines and could route status or
|
||||
commands to the wrong one. The same sharing explains why
|
||||
`GUI_App::switch_printer_agent` compares device IDs even when the agent pointer
|
||||
is unchanged: otherwise its unchanged-agent early return would skip
|
||||
reselection when the user switches between these presets, leaving status and
|
||||
filament work aimed at the previous printer.
|
||||
|
||||
> **Do not make an agent instance per printer just to hold device state.**
|
||||
> Keep per-printer state keyed by device ID, because one agent object is
|
||||
> shared by every printer of that type - state held on the instance would
|
||||
> route status or commands to the wrong `MachineObject`.
|
||||
|
||||
> **Do not fall back to another printer agent when the live one is null.**
|
||||
> An absent agent is an inert state. A fallback would connect to a
|
||||
> different implementation than the preset selected.
|
||||
|
||||
## Commands and unsupported work
|
||||
|
||||
An agent must either translate a Device-tab command or return an explicit
|
||||
error. `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` means no translation exists.
|
||||
`ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE` means a translation exists but this
|
||||
printer cannot use it. `MachineObject::publish_json()` turns either result
|
||||
into the user-visible unsupported-command response.
|
||||
|
||||
Every Device-tab command must leave by one of these four exits. The fifth
|
||||
path is the one to watch for in review:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CMD["Device-tab command JSON"] --> PUSH{"pushing namespace?"}
|
||||
PUSH -- yes --> OK1["Accept - the status stream already satisfies it"]
|
||||
PUSH -- no --> TRANS{"Translation exists for this agent?"}
|
||||
TRANS -- no --> E1["Return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED"]
|
||||
TRANS -- yes --> CAP{"This printer can use it?"}
|
||||
CAP -- no --> E2["Return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE"]
|
||||
CAP -- yes --> OK2["Translate and send to the printer"]
|
||||
TRANS -. FORBIDDEN .-> BAD["Return success without translating"]
|
||||
E1 --> PUB["MachineObject::publish_json turns both errors<br/>into the unsupported-command response"]
|
||||
E2 --> PUB
|
||||
BAD --> LIE["UI reports an action that never happened"]
|
||||
```
|
||||
|
||||
> **Do not return success for an unhandled command.** That makes an
|
||||
> unsupported button look as though it worked and hides missing coverage
|
||||
> from both users and maintainers.
|
||||
|
||||
The `pushing` command namespace is the exception. Its request means
|
||||
"send status"; an active status stream already satisfies it. The Device
|
||||
Manager sends these requests repeatedly as a keepalive, so rejecting them
|
||||
would surface a warning repeatedly even though no action is missing.
|
||||
|
||||
## Feature gate
|
||||
|
||||
`use_printer_agents` enables printer-agent routing. With the gate off,
|
||||
agent code must have no observable effect. Released profiles can already
|
||||
contain `printer_agent` values, so activating an agent while the gate is
|
||||
off would change existing user behavior merely by loading a profile.
|
||||
|
||||
Keep the gate at the routing call sites. Do not fold it into general Bambu
|
||||
vendor checks: slicing and hardware decisions such as AMS, lidar, bed
|
||||
types, and G-code flavor still describe printer capabilities, not the
|
||||
selected printer agent.
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
`printer_agent` remains a `coString`, even when the ID is currently
|
||||
unregistered. A preset may refer to an optional plugin that is not
|
||||
installed. The unknown string must load, remain unchanged, and round-trip
|
||||
without making the preset dirty. The UI may show it as missing, but must
|
||||
not rewrite it to a fallback ID.
|
||||
|
||||
Keep the feature gate's off-path behavior unchanged, preserve stored agent
|
||||
IDs, and treat Bambu-shaped payloads as a compatibility contract.
|
||||
|
||||
The reason these three are grouped is that each looks like a local code
|
||||
change and is not. Switching which printer agent handles a preset edits no
|
||||
profile and no project file, so it reads in review as contained to the
|
||||
agent layer. But a user's stored presets and `.3mf` projects already carry
|
||||
`printer_agent` values and were saved against the Bambu-shaped payload. So
|
||||
a change that is local in the code is not local in effect: it reaches
|
||||
every previously saved file. That is why the gate must be inert when off,
|
||||
an unknown ID must survive untouched, and the payload shape is treated as
|
||||
a contract rather than an implementation detail.
|
||||
|
||||
## Threading rule
|
||||
|
||||
Agents may perform network work on their own threads, but all mutations of
|
||||
Device Manager maps and `MachineObject` UI state must run on the UI thread.
|
||||
Queue incoming status before it reaches `parse_json()` or any operation
|
||||
that adds, removes, selects, or changes a device. This prevents races
|
||||
between background network callbacks and UI reads. For example, when a status
|
||||
callback arrives on an agent's network thread, queue it to the UI thread
|
||||
before it reaches `MachineObject::parse_json()` or changes a device map or
|
||||
selection. The Device tab reads those same structures on the UI thread, so
|
||||
parsing or adding, removing, or selecting a device from the network thread
|
||||
could race with that read.
|
||||
96
docs/printer-agent/camera.md
Normal file
96
docs/printer-agent/camera.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Camera support
|
||||
|
||||
*Owns the three camera ownership models and what each one renders through.
|
||||
Defers the Snapmaker filament path to [Built-in agents](agents.md), even
|
||||
though the same subclass owns both.*
|
||||
|
||||
Camera support has three ownership models. They share the Device panel,
|
||||
but not a common frame or stream interface.
|
||||
|
||||
Two render surfaces, never one:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BU["Bambu URL provider<br/>LAN or cloud, not from IPrinterAgent"]
|
||||
MU["Moonraker webcam discovery<br/>/server/webcams/list stream URL"]
|
||||
SU["Snapmaker camera page<br/>local HTML that polls monitor.jpg"]
|
||||
|
||||
BV["wxMediaCtrl2<br/>native Bambu media pipeline"]
|
||||
WV["Device-panel wxWebView<br/>stream URL or local polling page"]
|
||||
|
||||
BU --> BV
|
||||
MU --> WV
|
||||
SU -- overrides normal discovery --> WV
|
||||
```
|
||||
|
||||
## Bambu
|
||||
|
||||
Bambu playback uses `wxMediaCtrl2` and the native Bambu media pipeline.
|
||||
The URL comes from the Bambu LAN or cloud path, not from
|
||||
`IPrinterAgent`. A printer agent should not attempt to force a Bambu URL
|
||||
through the Moonraker or WebView path.
|
||||
|
||||
## Moonraker live view
|
||||
|
||||
On connection, the Moonraker agent obtains the first enabled webcam URL
|
||||
from `/server/webcams/list`. An absolute HTTP URL is used directly. A
|
||||
relative URL is resolved against the printer's host web root, with the
|
||||
Moonraker API port removed. This is necessary because a relative webcam
|
||||
path may exist on the printer's web server but not on the API port.
|
||||
|
||||
The connection generation guards the result. A late request must not
|
||||
replace the URL after the user has selected a different printer. Failed
|
||||
discovery clears the URL, which prevents a prior camera from remaining
|
||||
visible on a printer with no camera.
|
||||
|
||||
The agent places the discovered URL in its status payload. The Device
|
||||
panel renders it in `wxWebView`. It reloads only when the URL changes and
|
||||
resets the camera-start timestamp at that point. Reloading every update
|
||||
would loop indefinitely for endpoints that redirect, so an unchanged URL
|
||||
is shown again without calling `LoadURL()`.
|
||||
|
||||
The trade-off is intentional: a WebView that loses an unchanged stream
|
||||
does not automatically reload. Camera controls beyond live viewing remain
|
||||
out of scope for Moonraker. Recording, timelapse, settings, and virtual
|
||||
camera are Bambu-oriented features and must not be presented as supported
|
||||
merely because live view works.
|
||||
|
||||
## Snapmaker polling view
|
||||
|
||||
Snapmaker overrides normal webcam discovery. It writes a per-printer local
|
||||
HTML page that polls the printer's `monitor.jpg` with a cache-busting URL.
|
||||
Each next request starts after the prior image loads or fails, preventing
|
||||
requests from piling up on a slow printer. A raw snapshot URL is not used,
|
||||
because it would display one frozen frame instead of a live-looking view.
|
||||
|
||||
The printer must be asked to start its camera capture task. While the
|
||||
camera view is visible, the Device panel requests this at first display
|
||||
and then attempts another request every 300 seconds. Other agents reject
|
||||
the command quietly, so the common timer does not create an error for
|
||||
Bambu or ordinary Moonraker.
|
||||
|
||||
The Snapmaker command is sent from a detached thread because the request
|
||||
can block on socket I/O and the printer responds over a different channel.
|
||||
This avoids blocking the UI but leaves a raw-`this` lifetime risk: the
|
||||
agent can be destroyed while the detached operation still refers to it.
|
||||
Do not extend this path without addressing that ownership boundary.
|
||||
|
||||
The wrapper is written below the application cache with a name derived from
|
||||
the printer IP. The source contains no cleanup path for those files, so they
|
||||
can accumulate as different printer IPs are used. This is source-derived and
|
||||
was not reproduced during this rewrite.
|
||||
|
||||
Source code proves 300-second renewal attempts only. The long-running
|
||||
behavior of the shipped polling and renewal cycle has not yet been tested.
|
||||
Do not claim that the attempt renews an active capture task or that it
|
||||
prevents camera expiry until hardware verification establishes both.
|
||||
|
||||
## Maintenance checklist
|
||||
|
||||
- Keep the three ownership models separate.
|
||||
- Preserve host-root resolution for relative Moonraker URLs.
|
||||
- Keep generation guards and stale-URL clearing on every discovery path.
|
||||
- Reload WebView content only after a URL change.
|
||||
- Reset the camera-start timestamp when the camera URL changes.
|
||||
- Treat Moonraker as live-view-only and Snapmaker lifecycle behavior as
|
||||
not yet verified beyond the observed renewal attempts.
|
||||
248
docs/printer-agent/connection-and-status.md
Normal file
248
docs/printer-agent/connection-and-status.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Connection and status
|
||||
|
||||
*Owns the runtime sequence in order: selecting an agent and machine,
|
||||
starting a connection, receiving status, sending commands. Defers the
|
||||
structural rules those steps must obey - ownership, no-fallback, unknown
|
||||
IDs, threading - to [Architecture](architecture.md), and cites them at the
|
||||
point where they bite.*
|
||||
|
||||
## The four runtime concepts
|
||||
|
||||
Keep these concepts separate when tracing a connection problem:
|
||||
|
||||
- A preset stores an agent ID and printer address.
|
||||
- `NetworkAgent` holds the active printer agent for that agent ID.
|
||||
- `MachineObject` represents the selected printer at that address.
|
||||
- Freshness and status-confirmed readiness are separate states.
|
||||
|
||||
An agent ID selects a printer agent implementation. A device ID selects one
|
||||
printer within that implementation.
|
||||
|
||||
A non-Bambu printer reaches the machine list through **Bind with Access
|
||||
Code**, the tile in the Device tab's machine-select popup. The user enters
|
||||
an address and an access code, `bind_detect()` probes the address before
|
||||
any connect, and `DeviceManager::insert_local_device()` creates the
|
||||
`MachineObject`. For the Moonraker family the address itself becomes the
|
||||
device ID: `MoonrakerPrinterAgent::bind_detect()` seeds `dev_name` and
|
||||
`dev_id` from the entered address, so an unreachable or unnamed printer
|
||||
still shows up as its IP rather than blank.
|
||||
|
||||
Binding is the only route for that family. `MoonrakerPrinterAgent::start_discovery()`
|
||||
deliberately announces nothing, because a partial discovery implementation
|
||||
would populate the machine list with stale hosts. Bambu is the exception:
|
||||
it has its own discovery identity and does not use the address as an ID.
|
||||
|
||||
`DeviceManager::selected_machine` is only a selected ID. It can name no
|
||||
resolvable object. `get_selected_machine()` answers whether an object is
|
||||
actually available. `set_selected_machine()` accepting an ID therefore
|
||||
does not prove the printer is connected. The selected ID can remain when its
|
||||
object is unavailable, so connection state must come from the object itself.
|
||||
|
||||
## Selecting the agent and machine
|
||||
|
||||
`GUI_App::switch_printer_agent()` reads the edited printer preset and
|
||||
resolves its stored agent ID through `NetworkAgentFactory`.
|
||||
|
||||
1. An empty stored ID is a legacy sentinel. It resolves to `bbl` for a
|
||||
Bambu vendor preset and to `orca` otherwise.
|
||||
2. If that effective ID is registered, the factory provides the matching
|
||||
printer agent implementation.
|
||||
3. Clear the live printer agent only when a nonempty stored ID is unregistered
|
||||
or the factory cannot construct the matching registered agent.
|
||||
4. When the active printer agent changes, clear the current selection, user
|
||||
selection, stale device discoveries, sidebar state, and AMS state before
|
||||
installing the replacement.
|
||||
5. Select the preset's address-derived machine for non-Bambu agents.
|
||||
|
||||
The lifetimes are easier to see than to read. Note that the agent pointer
|
||||
can be unchanged while the machine still must be re-selected - that is the
|
||||
trap in the same-agent path below:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant P as Printer preset
|
||||
participant G as GUI_App::switch_printer_agent
|
||||
participant F as NetworkAgentFactory
|
||||
participant N as NetworkAgent
|
||||
participant M as MachineObject
|
||||
|
||||
U->>P: Edit or switch the printer preset
|
||||
P->>G: printer_agent value, possibly empty
|
||||
G->>G: resolve_printer_agent_id - empty becomes bbl or orca
|
||||
G->>F: get_printer_agent_info(effective ID)
|
||||
|
||||
alt ID not registered, and stored value was nonempty
|
||||
F-->>G: no info
|
||||
G->>N: clear the live printer agent
|
||||
Note over N: null is inert - never fall back to another agent
|
||||
else ID registered
|
||||
F->>F: create once per ID, then serve from cache
|
||||
F-->>G: the cached IPrinterAgent for this ID
|
||||
alt Agent pointer changed
|
||||
G->>N: clear selection, user selection, discoveries, sidebar, AMS
|
||||
G->>N: install the replacement agent
|
||||
else Same cached pointer returned
|
||||
Note over G,F: Two presets can share one agent ID
|
||||
end
|
||||
G->>M: compare device ID, re-select if the address differs
|
||||
end
|
||||
```
|
||||
|
||||
> **Do not use the first available machine as a fallback** (rule owned by
|
||||
> [Architecture](architecture.md), Runtime objects and ownership). It
|
||||
> connects to a printer the user did not choose, including one owned by a
|
||||
> different printer agent.
|
||||
|
||||
The same-agent path is important too. Two presets can use one agent type
|
||||
but point at different addresses, and the factory caches one agent per ID,
|
||||
so switching between them returns the same pointer and would otherwise
|
||||
skip reselection entirely. Re-select the machine whenever the preset's
|
||||
address changes, even when the factory returned the same active agent.
|
||||
Otherwise status and filament work can continue against the previous
|
||||
printer.
|
||||
|
||||
Note: this is a legacy coupling, not the primary workflow. It reads an
|
||||
address stored on the printer preset itself (`print_host` and
|
||||
`printhost_port`, named here only so the keys can be found in the code)
|
||||
and derives a device ID from it with `dev_id_from_address()`. Those keys
|
||||
predate printer agents and are edited through `PhysicalPrinterDialog`,
|
||||
which despite its name writes the printer preset rather than a
|
||||
`PhysicalPrinter` object - that object is no longer constructed. Printers
|
||||
normally arrive through Bind with Access Code instead, which does not
|
||||
touch the preset. Both routes end at `insert_local_device()`, so they must
|
||||
agree on the device ID: `dev_id_from_address()` strips the URL scheme and
|
||||
drops an empty port, while the bind path stores the address as the user
|
||||
typed it.
|
||||
|
||||
The unknown-`coString` compatibility rule belongs to `architecture.md` under
|
||||
Backward compatibility. Keep a nonempty unknown `printer_agent` ID unchanged
|
||||
and display a missing state if needed; do not rewrite it during plugin unload
|
||||
or choose an arbitrary replacement, so the preset can round-trip while its
|
||||
plugin is temporarily unavailable.
|
||||
|
||||
## Starting a connection
|
||||
|
||||
Machine selection causes `MachineObject::connect()` to invoke the active
|
||||
agent's `connect_printer()` with the selected address and effective access
|
||||
code. A success return means that the connection attempt started. It does
|
||||
not mean that the printer is ready or that a status stream is alive.
|
||||
|
||||
Moonraker-family agents must force HTTP. Moonraker and print-host
|
||||
installations commonly serve plain HTTP, while the generic machine path
|
||||
can request TLS by default. Passing that default through turns a valid
|
||||
connection into an HTTPS request the printer will refuse. The agent therefore
|
||||
must keep the connection on HTTP unless its protocol support changes
|
||||
deliberately and is verified.
|
||||
|
||||
## Access codes: four coordinated slots
|
||||
|
||||
One effective access code can live in four places:
|
||||
|
||||
| Slot | Location | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Device runtime | `MachineObject::access_code` | Code learned from the device. |
|
||||
| User runtime | `MachineObject::user_access_code` | Code entered by the user. |
|
||||
| Device config | `access_code[dev_id]` | Persisted device value. |
|
||||
| User config | `user_access_code[dev_id]` | Persisted user value. |
|
||||
|
||||
The effective code prefers the user value when present, then the device
|
||||
value. Keep user input in the user path and device replies in the device
|
||||
path. Crossing those paths obscures which value should win.
|
||||
|
||||
`set_access_code()` deliberately does not save configuration immediately.
|
||||
Device replies and polls can update it often; forcing a full config write
|
||||
for each message adds unnecessary work. The normal deferred config save
|
||||
persists dirty state later. Do not add an eager save just to make this one
|
||||
path symmetric: device replies and polls update it often, so a config
|
||||
write per message is wasted work.
|
||||
|
||||
> **Do not erase the user access code when a printer connects.** On the
|
||||
> LAN reselection path that code can be the only credential that lets the
|
||||
> machine pass the access check and receive the status or access-code
|
||||
> reply that would refresh it, so erasing it at connection time can leave
|
||||
> the machine permanently unable to receive updates. A failed connection
|
||||
> is the place to handle a proven bad credential.
|
||||
|
||||
## Receiving status
|
||||
|
||||
An agent receives native status, translates it to the existing payload
|
||||
shape, and dispatches it to the matching `MachineObject`. The object
|
||||
parses the payload and records when it last received an update.
|
||||
|
||||
Readiness is four states, and three of them look connected:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> SelectedIdOnly
|
||||
SelectedIdOnly: Selected ID only
|
||||
SelectedIdOnly: selected_machine names no resolvable object
|
||||
SelectedIdOnly --> FreshWindow: connect_printer returns success
|
||||
|
||||
FreshWindow: Fresh window
|
||||
FreshWindow: reset seeded the update time - no status yet
|
||||
FreshWindow --> Connecting: still fresh, still no push-status
|
||||
|
||||
Connecting: Connecting
|
||||
Connecting: is_connecting true - the honest state
|
||||
Connecting --> Ready: first push-status message parsed
|
||||
|
||||
Ready: Status-confirmed ready
|
||||
Ready: the only state that proves a usable printer
|
||||
Ready --> Stale: update time ages out
|
||||
|
||||
Stale: Stale
|
||||
Stale: is_connected false
|
||||
Stale --> Ready: a later status message arrives
|
||||
|
||||
note right of FreshWindow
|
||||
is_connected() is true from here on.
|
||||
It is a freshness test over the update
|
||||
time - not proof that any status arrived.
|
||||
end note
|
||||
```
|
||||
|
||||
`is_connected()` is a freshness test over the update time. It does not
|
||||
describe whether `connect_printer()` returned success or whether any status
|
||||
message arrived: reset initializes the update time, creating an initial
|
||||
freshness window. `is_connecting()` distinguishes that window from
|
||||
status-confirmed readiness: while the object is fresh and no push-status
|
||||
message has arrived, it remains connecting.
|
||||
|
||||
> **Do not treat freshness or a successful connect as proof of readiness.**
|
||||
> Code that needs a usable printer must wait for status-confirmed
|
||||
> readiness, because the fresh window exists before any status has been
|
||||
> parsed.
|
||||
|
||||
The UI-thread mutation rule belongs to `architecture.md` under Threading rule.
|
||||
Dispatch the status callback to the UI thread before changing device maps,
|
||||
selection, or `MachineObject` state, because network callbacks may run in a
|
||||
worker thread and mutating these structures there races with the Device tab.
|
||||
|
||||
## Sending commands
|
||||
|
||||
`MachineObject` builds the established command JSON and sends it through
|
||||
the active `NetworkAgent`. The agent translates it or returns an explicit
|
||||
unsupported result. It must not report success when no translation exists.
|
||||
|
||||
The `pushing` command exception belongs to `architecture.md` under Commands
|
||||
and unsupported work. It asks for status, and a working status stream already
|
||||
supplies it, so accepting it avoids false unsupported warnings from the Device
|
||||
Manager's repeated keepalive.
|
||||
|
||||
## Maintainer constraints
|
||||
|
||||
- Preserve same-agent reselection by address, because an agent type can
|
||||
serve more than one printer.
|
||||
- Preserve the null-agent, no-fallback, and unknown-`coString` rules in
|
||||
`architecture.md`; selection must remain an explicit user or preset choice,
|
||||
and stale state must not belong to a replacement printer agent.
|
||||
- An empty value is the legacy Bambu-or-Orca sentinel, not a missing printer
|
||||
agent.
|
||||
- Preserve deferred access-code saves and the no-on-connect-erase rule;
|
||||
they prevent excessive config writes and credential-driven status loss.
|
||||
- Keep Moonraker connections HTTP-only unless the agent's protocol support
|
||||
changes deliberately and is verified.
|
||||
- Do not treat freshness as proof that status arrived; wait for
|
||||
status-confirmed readiness. The UI-thread mutation rule is in
|
||||
`architecture.md` under Threading rule.
|
||||
110
docs/printer-agent/filament.md
Normal file
110
docs/printer-agent/filament.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Filament synchronization and mapping
|
||||
|
||||
*Owns the three filament stages end to end: acquiring printer state,
|
||||
selecting a mapping, and delivering it at print time. Defers the upload
|
||||
and start mechanics to [Printing](printing.md), and per-vendor discovery
|
||||
detail to [Built-in agents](agents.md).*
|
||||
|
||||
Filament support has three separate stages. A successful first stage does
|
||||
not mean that a selected mapping will be delivered to the printer.
|
||||
|
||||
Stage 3 is where the two paths diverge, and only one of them reaches the
|
||||
printer:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SRC["Filament source<br/>Moonraker lane_data, or the classic MMU object"]
|
||||
ACQ["1 - Acquire state<br/>Moonraker pull, not subscription"]
|
||||
ST["DevFilaSystem<br/>Bambu-shaped view, synthetic 4-slot groups"]
|
||||
SEL["2 - Select mapping<br/>Send dialog matches project filament to slots"]
|
||||
PAR["PrintParams<br/>ams_mapping fields"]
|
||||
QIDI["Qidi agent writes enable_box<br/>and value_t per tool, then starts the print"]
|
||||
BASE["Base Moonraker, Happy Hare, AFC<br/>upload and start ignore the mapping fields"]
|
||||
OK["Mapping reaches the printer"]
|
||||
DEAD["Mapping never reaches the printer<br/>the job runs on the printer's own loaded-lane behavior"]
|
||||
|
||||
SRC --> ACQ --> ST --> SEL --> PAR
|
||||
PAR -- Qidi --> QIDI --> OK
|
||||
PAR -- every other target --> BASE --> DEAD
|
||||
```
|
||||
|
||||
## 1. Acquire printer state
|
||||
|
||||
`FilamentSyncMode` declares how the UI obtains filament state:
|
||||
|
||||
| Mode | Meaning |
|
||||
| --- | --- |
|
||||
| `subscription` | A status stream keeps the state current. |
|
||||
| `pull` | The UI must request state before it can use it. |
|
||||
| `none` | The agent has no usable filament state. |
|
||||
|
||||
Moonraker uses `pull`. Its ordinary status stream does not supply the
|
||||
filament data used by this UI. In particular, `lane_data` is a Moonraker
|
||||
database namespace, not a printer object that the existing subscription
|
||||
can follow. Changing Moonraker to `subscription` would suppress the pull
|
||||
that actually populates the UI.
|
||||
|
||||
The agent first reads `lane_data`, which can describe AFC and newer Happy
|
||||
Hare installations. If that is unavailable, it reads the classic Happy
|
||||
Hare `mmu` object. Those response shapes are source-supported but not yet
|
||||
verified against current Happy Hare and AFC installations.
|
||||
|
||||
The current parser expects lane values as strings and silently skips
|
||||
numeric values. Whether current AFC or Happy Hare installations emit
|
||||
numeric lane values is unverified.
|
||||
|
||||
The received lanes are converted into a Bambu-shaped model so existing
|
||||
AMS UI can render them. The model groups numeric lane indexes into
|
||||
synthetic groups of four slots and passes the result through
|
||||
`ParseV1_0`. This is a UI compatibility adapter, not evidence that the
|
||||
printer has a Bambu AMS.
|
||||
|
||||
Pull state can be stale. The Send dialog can build a mapping from the
|
||||
current `DevFilaSystem` without refreshing it first, and a failed pull can
|
||||
leave older state visible. Do not represent a displayed lane list as a
|
||||
fresh printer read unless the call site just performed the pull.
|
||||
|
||||
## 2. Select a mapping
|
||||
|
||||
The Send dialog matches each project filament to a compatible reported
|
||||
slot. It rejects a mismatched material type and then prefers compatible
|
||||
slots according to the existing mapping rules. The result is carried in
|
||||
the legacy linear mapping, the explicit AMS-and-slot mapping, and mapping
|
||||
metadata for the job.
|
||||
|
||||
Treat lane numbers as printer contracts. A numeric lane index is used as a
|
||||
slot index in the synthetic four-slot view, so an incorrect numbering
|
||||
assumption can select the wrong physical lane.
|
||||
|
||||
The material identity code also retains a defect: ABS and ASA can be
|
||||
shown as PLA when profile identifiers collide. This is not fixed here,
|
||||
and multi-color mapping has not received hardware verification.
|
||||
|
||||
## 3. Deliver the mapping at print time
|
||||
|
||||
`PrintJob` copies the selected mappings into `PrintParams`, but base
|
||||
Moonraker does not read those fields when it uploads and starts a print.
|
||||
For plain Moonraker, Happy Hare, and AFC targets using that base path, a
|
||||
correct-looking mapping in the UI is therefore not delivered to the
|
||||
printer. The print runs using the printer's own loaded-lane behavior.
|
||||
|
||||
Qidi is the implemented exception. Its agent writes its own box mapping
|
||||
before starting the print. That is a Qidi-specific delivery contract, not
|
||||
a generic Moonraker solution.
|
||||
|
||||
There are deliberately no guessed Happy Hare or AFC write macros. Their
|
||||
macro and variable names are defined by printer-side configuration, so a
|
||||
guessed command could silently do nothing or control the wrong setup. Add
|
||||
a delivery path only after verifying the exact contract against upstream
|
||||
documentation or a real printer.
|
||||
|
||||
## Maintenance checklist
|
||||
|
||||
- Keep Moonraker in `pull` mode while `lane_data` remains pull-only.
|
||||
- Refresh or clearly surface stale state before relying on Send-dialog
|
||||
mappings.
|
||||
- Do not claim base Moonraker honors mappings until it consumes them at
|
||||
print time.
|
||||
- Preserve Qidi as a distinct delivery implementation.
|
||||
- Verify lane numbering, material identity, and multi-color behavior on
|
||||
hardware before expanding the mapping contract.
|
||||
161
docs/printer-agent/plugin-agents.md
Normal file
161
docs/printer-agent/plugin-agents.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Python printer-agent plugins
|
||||
|
||||
*Owns the plugin bridge: the implementation contract, registration and
|
||||
lifetime, and the audit scope. Defers what the agent must do once live to
|
||||
[Architecture](architecture.md) and
|
||||
[Connection and status](connection-and-status.md).*
|
||||
|
||||
Python printer agents use the current capability bridge. They do not use a
|
||||
separate adapter or a Moonraker-specific plugin path.
|
||||
|
||||
## What an agent must implement
|
||||
|
||||
Not overriding a member of `IPrinterAgent` has four different
|
||||
consequences depending on which tier it is in. This is the whole plugin
|
||||
contract:
|
||||
|
||||
| Tier | Members | Consequence of not overriding |
|
||||
| --- | --- | --- |
|
||||
| Pure virtual | `connect_printer`, `disconnect_printer`, `send_message_to_printer`, the `start_*` print operations, `start_discovery`, `bind`, `bind_detect`, `unbind`, the callback setters, `set_cloud_agent`, `get_agent_info`, and the rest of the pure surface | Compile error |
|
||||
| Concrete, succeeds | `start_subscribe`, `stop_subscribe`, `add_subscribe`, `del_subscribe` | Silently returns `BAMBU_NETWORK_SUCCESS` |
|
||||
| Concrete, declines | `command_ams_refresh_rfid`, `command_ams_calibrate`, `command_ams_select_tray`, `command_start_camera` | Silently returns `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` |
|
||||
| Concrete, inert | `get_filament_sync_mode`, `fetch_filament_info` | Reports `FilamentSyncMode::none` and `false` - no filament capability at all |
|
||||
|
||||
The refusal tier is deliberate: those commands carry Bambu-dialect G-code
|
||||
in their bodies, so the honest default is a refusal that
|
||||
`MachineObject::publish_json()` turns into a dialog. The success tier is
|
||||
equally deliberate - a printer whose status already streams needs no
|
||||
subscription call.
|
||||
|
||||
> **Do not assume a missing override quietly inherits useful behavior, and
|
||||
> do not assume it fails loudly either.** Only the first tier fails at
|
||||
> compile time. The second silently reports success, the third silently
|
||||
> declines, and the fourth silently reports no filament capability.
|
||||
|
||||
## The plugin contract
|
||||
|
||||
A plugin subclasses `printer_agent.PrinterAgentBase`, the Python binding for
|
||||
`PrinterAgentPluginCapability`. The capability itself is the live native
|
||||
`IPrinterAgent`; there is no intermediate protocol adapter, because
|
||||
`PrinterAgentPluginCapability` inherits both `PluginCapabilityInterface`
|
||||
and `IPrinterAgent` directly.
|
||||
|
||||
`get_type()` stays a `PluginCapabilityInterface` method and
|
||||
`set_cloud_agent()` remains the native host injection point. A plugin must
|
||||
implement the pure connection, communication, discovery, binding, print,
|
||||
callback-registration, and filament-refresh operations. The certificate,
|
||||
bind-ticket, HMS-snapshot, and user-selected-machine members are pure too;
|
||||
the table above abridges the list.
|
||||
|
||||
The only tracked Python printer-agent implementation is the BBL plugin. There
|
||||
is no Python Moonraker printer agent in the current source tree. Moonraker is
|
||||
implemented by the built-in C++ class.
|
||||
|
||||
## Registration and lifetime
|
||||
|
||||
When an enabled plugin advertises a printer-connection capability, the factory
|
||||
gets its `AgentInfo` and registers a factory under `AgentInfo.id`. This is the
|
||||
same registry used for built-in agents.
|
||||
|
||||
Two similarly named structs are involved, and they are not the same thing.
|
||||
`AgentInfo` is what the agent says about itself; `PrinterAgentInfo` is the
|
||||
registry's entry about it:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AgentInfo {
|
||||
<<returned by the agent via get_agent_info>>
|
||||
string id
|
||||
string name
|
||||
string version
|
||||
string description
|
||||
}
|
||||
class PrinterAgentInfo {
|
||||
<<the registry entry>>
|
||||
string id
|
||||
string display_name
|
||||
string plugin_identifier
|
||||
PrinterAgentFactory factory
|
||||
}
|
||||
class PrinterAgentFactory {
|
||||
<<std::function>>
|
||||
takes cloud_agent and log_dir
|
||||
returns shared_ptr~IPrinterAgent~
|
||||
}
|
||||
class NetworkAgentFactory {
|
||||
<<all static>>
|
||||
register_printer_agent(id, display_name, factory)
|
||||
create_printer_agent_by_id(id, cloud_agent, log_dir)
|
||||
clear_printer_agent_cache()
|
||||
register_python_printer_agent(plugin_key, capability_name)
|
||||
deregister_python_printer_agent(plugin_key, capability_name)
|
||||
}
|
||||
|
||||
AgentInfo ..> PrinterAgentInfo : id becomes the registry key
|
||||
PrinterAgentInfo *-- PrinterAgentFactory
|
||||
NetworkAgentFactory o-- PrinterAgentInfo : one entry per ID
|
||||
PrinterAgentFactory ..> PrinterAgentPluginCapability : weak reference
|
||||
```
|
||||
|
||||
`plugin_identifier` is empty for built-ins and
|
||||
`<plugin_key>;<uuid>;<capability_name>` for plugins - that is how the
|
||||
registry tells the two apart at deregistration time. Built-in IDs are the
|
||||
constants `ORCA_PRINTER_AGENT_ID` and `BBL_PRINTER_AGENT_ID`.
|
||||
|
||||
Agent IDs are global. A plugin cannot replace a built-in agent or another
|
||||
plugin with the same ID. Registry rejection is unconditional. The conflicting
|
||||
capability is disabled and the user is shown the conflict only when `wxTheApp`
|
||||
exists and the app is not closing. Re-registering the same plugin capability
|
||||
is allowed so a reload can replace its factory with the current capability
|
||||
instance.
|
||||
|
||||
The registered factory holds a weak reference to the capability. If the plugin
|
||||
has already gone away, creation returns null instead of reviving a destroyed
|
||||
Python object. Callers must treat that as no active printer agent.
|
||||
|
||||
On deregistration, the factory removes the registry entry and cached agent,
|
||||
disconnects a cached agent, and clears the live agent if it has the same ID.
|
||||
This order prevents `NetworkAgent` from retaining a Python implementation
|
||||
whose module is about to unload. The current path is UI-thread oriented. Raw
|
||||
pointer hazards become relevant only if deregistration moves to another thread
|
||||
without adding synchronization around the GUI-held active-agent handle.
|
||||
|
||||
## Device-tab integration
|
||||
|
||||
Plugins share the native Device tab with built-in agents. There is no
|
||||
printer-agent API for adding custom Device-tab panels and no plugin-owned
|
||||
`MachineObject` to populate directly.
|
||||
|
||||
Instead, the plugin supplies the same callbacks as any `IPrinterAgent`. Its
|
||||
status messages must use the Bambu-shaped payload that `MachineObject` already
|
||||
parses. If a required field is absent, the shared native UI shows its default
|
||||
or incomplete state. A custom protocol is acceptable inside the plugin, but
|
||||
its boundary with the app must perform this translation.
|
||||
|
||||
## Python calls, errors, and audit scope
|
||||
|
||||
The C++ trampoline acquires the Python GIL, invokes each pure virtual override,
|
||||
logs a Python exception, and rethrows it. A missing override is a separate
|
||||
C++ pure-virtual failure, not a logged Python traceback. Python construction
|
||||
also bypasses the virtual trampoline, so the bridge logs a constructor failure
|
||||
at the construction boundary.
|
||||
|
||||
Plugin-created threads need their own exception handling. An exception raised
|
||||
there does not cross the C++ trampoline; it reaches Python's thread exception
|
||||
handling and is recorded through redirected Python standard error.
|
||||
|
||||
The audit hook is defense in depth, not a sandbox. Current printer-agent
|
||||
trampoline calls use loading audit mode. In that mode, normal reads are
|
||||
allowed, only some file writes are checked against allowed roots, and many
|
||||
operations are outside the policy, including network access and process
|
||||
creation. Work that runs outside an active trampoline scope, including a
|
||||
plugin-created thread, has no attributed plugin context and is allowed by
|
||||
default. Do not treat this mechanism as permission to run untrusted code.
|
||||
|
||||
## Source locations
|
||||
|
||||
- `src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp`
|
||||
- `src/slic3r/plugin/pluginTypes/printerAgent/`
|
||||
`PrinterAgentPluginCapabilityTrampoline.hpp`
|
||||
- `src/slic3r/Utils/NetworkAgentFactory.cpp`
|
||||
- `resources/orca_plugins/BBLPrinterAgentPlugin.py`
|
||||
138
docs/printer-agent/printing.md
Normal file
138
docs/printer-agent/printing.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Printing through printer agents
|
||||
|
||||
*Owns the send path: connection choices, preflight, upload, start, and the
|
||||
two recovery flows. Defers filament mapping delivery to
|
||||
[Filament synchronization](filament.md), which is a separate contract even
|
||||
though it is applied at print time.*
|
||||
|
||||
This chapter describes the printer-agent send path. It is separate from
|
||||
the older print-host implementation, even when both target Moonraker.
|
||||
Keep the paths separate unless their contracts and failure handling can
|
||||
be deliberately reconciled.
|
||||
|
||||
## Connection choices
|
||||
|
||||
Three connection paths are in use:
|
||||
|
||||
| Target | Connection path | Use |
|
||||
| --- | --- | --- |
|
||||
| Native Bambu | Custom TLS tunnel on port 6000 | Send and optional eMMC preflight |
|
||||
| Bambu Python agent | Implicit FTPS on port 990 | Upload and Bambu preflight fallback |
|
||||
| Moonraker family | HTTP | Upload and start print |
|
||||
|
||||
The Bambu connection paths are independent. Selecting one does not prove that
|
||||
the other is available. The Moonraker agent uploads with a multipart
|
||||
request to its `gcodes` storage and then starts the uploaded filename;
|
||||
it does not reuse the legacy `Moonraker` print-host class.
|
||||
|
||||
## Bambu native tunnel
|
||||
|
||||
The native tunnel depends on the versioned networking DLL and its
|
||||
file-transfer module. `InitFTModule()` is a single-owner initialization:
|
||||
it rejects a second call. Any future shared initialization must therefore
|
||||
be idempotent, while `BBLNetworkPlugin` remains the single teardown owner.
|
||||
It must call `UnloadFTModule()` before freeing the DLL, otherwise the
|
||||
module's function pointers can point into unloaded code.
|
||||
|
||||
There is currently an initialization gap: selecting a printer agent does
|
||||
not initialize this module. It is initialized only when the
|
||||
`installed_networking` option causes the native BBL network plugin to
|
||||
initialize. Calls to the tunnel must continue to fail safely until that
|
||||
path has initialized the module. The Send UI catches this failure and
|
||||
reports an initialization error instead of letting an exception leave a
|
||||
wx event handler.
|
||||
|
||||
## Bambu FTPS upload
|
||||
|
||||
The Python Bambu agent uses implicit FTPS on port 990. Its live upload
|
||||
path closes the data connection, then waits at most two seconds for the
|
||||
control response with `voidresp()`. A `TimeoutError` is accepted as a
|
||||
completed transfer. An `error_reply` is also accepted when its reply
|
||||
begins with `200`. This is the behavior to preserve.
|
||||
|
||||
Do not describe the path as using TLS `unwrap()`: the live construction
|
||||
does not enable it. Enabling it without a bounded wait could hang while
|
||||
waiting for the peer's TLS close notification. The current timeout-based
|
||||
handling has not been verified on hardware against every printer and FTP
|
||||
server combination.
|
||||
|
||||
## Print preflight and recovery
|
||||
|
||||
For normal LAN prints, `PrintJob` performs a preflight before the real
|
||||
send. When eMMC is eligible it tries the native tunnel, then it sends a
|
||||
small `verify_job` upload through the selected agent. The latter is a real
|
||||
upload, not a special protocol command. Non-Bambu agents therefore upload
|
||||
the probe too.
|
||||
|
||||
> **Do not re-enable eMMC by default** without hardware coverage for the
|
||||
> affected devices. It is opt-in because the tunnel can hang during upload
|
||||
> on some printers.
|
||||
|
||||
The whole send, including the thread hop and the recovery fork:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as Send UI (UI thread)
|
||||
participant J as PrintJob (worker)
|
||||
participant A as Selected IPrinterAgent
|
||||
participant P as Printer
|
||||
|
||||
UI->>J: Start send
|
||||
opt eMMC eligible - off by default
|
||||
J->>P: Native Bambu tunnel attempt
|
||||
Note over J,P: Can hang on some printers
|
||||
end
|
||||
J->>A: verify_job preflight
|
||||
Note over J,A: A real upload, not a protocol probe -<br/>non-Bambu agents upload it too
|
||||
A->>P: Upload probe
|
||||
|
||||
alt Preflight and upload succeed
|
||||
J->>A: Upload the real job
|
||||
A->>P: Upload, then request print start
|
||||
Note over A,P: The start response may stay open while<br/>the printer prepares - a timeout is not<br/>proof of failure, so check reported print state
|
||||
A-->>UI: Result from the reported print state
|
||||
else Upload fails
|
||||
J-->>UI: Failure callback, marshalled to the UI thread
|
||||
Note over UI: Re-resolve the machine here.<br/>Never reuse a machine pointer captured<br/>on the worker - agent or machine may have changed
|
||||
alt Printer still connected
|
||||
UI->>UI: Explain that storage upload failed
|
||||
else Printer disconnected
|
||||
UI->>UI: Open the IP or access-code flow
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
An upload failure and a disconnected printer need different recovery:
|
||||
|
||||
| Condition | UI response |
|
||||
| --- | --- |
|
||||
| Printer is still connected | Explain that storage upload failed. |
|
||||
| Printer is disconnected | Open the IP or access-code flow. |
|
||||
|
||||
> **Do not retain a machine pointer from a worker callback.** The callback
|
||||
> that chooses between these two outcomes runs on the UI thread and
|
||||
> re-resolves the machine there, because the selected agent or machine can
|
||||
> change first. The connection check is adequate for choosing the message,
|
||||
> but is not a strong enough signal to authorize a reconnect.
|
||||
|
||||
## Moonraker upload and start
|
||||
|
||||
`MoonrakerPrinterAgent` uploads through Moonraker HTTP, then requests the
|
||||
print start separately. The start endpoint may keep its response open
|
||||
while the printer prepares the job. A timeout after that request is not
|
||||
automatically proof that the start failed: the agent checks the reported
|
||||
print state before deciding the result.
|
||||
|
||||
The legacy print-host Moonraker path implements its own upload and start
|
||||
logic. It is not the agent path and should not be changed as an implicit
|
||||
side effect of agent work.
|
||||
|
||||
## Maintenance checklist
|
||||
|
||||
- Test the selected connection path, not just another path on the same
|
||||
printer.
|
||||
- Preserve cancellation and progress callbacks across upload and start.
|
||||
- Treat `verify_job` as an actual upload when estimating storage effects.
|
||||
- Keep eMMC opt-in until its hanging behavior is resolved and verified.
|
||||
- Keep the connected-upload-failure dialog distinct from the disconnected
|
||||
recovery flow.
|
||||
137
docs/printer-agent/reference/capability-matrix.md
Normal file
137
docs/printer-agent/reference/capability-matrix.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Printer-agent capability matrix
|
||||
|
||||
This is a compact lookup for the built-in Moonraker family. It combines
|
||||
implementation state with recorded evidence. It is not a promise that every
|
||||
firmware configuration behaves the same way. Python plug-in behavior depends
|
||||
on the plug-in, not on this matrix.
|
||||
|
||||
Use [testing and troubleshooting](../testing.md) before calling a live-printer
|
||||
result complete.
|
||||
|
||||
For a quicker tour of the controls users actually see, open the
|
||||
[annotated Device-tab view](device-tab-annotations.html). The annotations
|
||||
explain the important routing constraints; this matrix remains the compact
|
||||
cross-agent reference.
|
||||
|
||||
## Status definitions
|
||||
|
||||
- Supported - implemented, with a relevant live-printer result recorded.
|
||||
- Partial - an important condition, limitation, or defect applies.
|
||||
- Unsupported - no applicable implementation, or deliberate refusal.
|
||||
- Not verified - implemented or source-inspected, but without a relevant live
|
||||
result.
|
||||
|
||||
`Base` means `MoonrakerPrinterAgent`. Qidi, Creality, and Snapmaker inherit
|
||||
from it unless a row identifies an override.
|
||||
|
||||
## Connection and status
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Direct LAN connection with API key | Supported | Supported | Not verified | Not verified |
|
||||
| WebSocket status updates | Supported | Supported | Not verified | Not verified |
|
||||
| Reconnect and fresh status | Partial | Partial | Partial | Partial |
|
||||
| Discovery and cloud binding | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
| Device identity with a configured port | Partial | Partial | Partial | Partial |
|
||||
|
||||
- Reconnect completion can leave the Device tab with stale status.
|
||||
- A bare IP and `host:port` can become separate device identities.
|
||||
- The Base and Qidi Supported grades come from prior hardware sessions. They
|
||||
were carried into this rewrite and not rerun.
|
||||
|
||||
## Controls
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Home and arbitrary G-code | Not verified | Not verified | Not verified | Not verified |
|
||||
| Bed and nozzle temperature | Not verified | Not verified | Not verified | Not verified |
|
||||
| Pause, resume, and cancel | Not verified | Not verified | Not verified | Not verified |
|
||||
| Configured chamber light | Partial | Partial | Partial | Partial |
|
||||
| Jog and manual extrusion | Partial | Partial | Not verified | Not verified |
|
||||
| Legacy part-fan speed control | Partial | Partial | Not verified | Not verified |
|
||||
| Structured fan, chamber, and AI controls | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
| AMS RFID, calibration, and tray control | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
|
||||
- Chamber light needs a recognised light object.
|
||||
- Base and Qidi jog works, but can leave relative positioning active. Do not
|
||||
use it as a general safe-control test until its G-code state is restored.
|
||||
- Base and Qidi part-fan control works through legacy `gcode_line` while
|
||||
`is_enable_np` is false. Adding `cfg`, `fun`, `aux`, and `stat` flips that
|
||||
flag and routes fan and extruder controls to unsupported structured commands.
|
||||
- Creality and Snapmaker inherit the source path but have no separate live
|
||||
evidence for jog or fan control.
|
||||
|
||||
## Printing
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Upload G-code without starting | Not verified | Not verified | Not verified | Not verified |
|
||||
| Upload and start a local print | Not verified | Not verified | Not verified | Not verified |
|
||||
| Mapped multi-material print | Unsupported | Not verified | Unsupported | Unsupported |
|
||||
| Cloud or SD-card start variants | Unsupported | Partial | Unsupported | Unsupported |
|
||||
| Cancel during upload | Not verified | Not verified | Not verified | Not verified |
|
||||
| Send with no nozzle identity | Not applicable | Not verified | Not applicable | Not applicable |
|
||||
|
||||
- Qidi applies mapping before it routes the real local print path.
|
||||
- Some Qidi print variants can reach base success stubs after mapping.
|
||||
- Qidi tolerates missing nozzle data in source, but that Send preflight is not
|
||||
hardware-verified.
|
||||
|
||||
## Filament
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Sync mode | Not verified | Not verified | Not verified | Not verified |
|
||||
| Read installed material and slots | Partial | Not verified | Not verified | Not verified |
|
||||
| Slot, material, and colour refresh | Unsupported | Not verified | Not verified | Not verified |
|
||||
| Cleanup after removed material | Not verified | Not verified | Not verified | Not verified |
|
||||
| Load, unload, or write a slot | Unsupported | Partial | Unsupported | Unsupported |
|
||||
| Auto Refill | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
|
||||
- All built-in agents use pull-mode sync.
|
||||
- Base reads Happy Hare or AFC data when present. Qidi has print-time mapping;
|
||||
Creality has CFS logic; Snapmaker reads printer arrays and NFC data.
|
||||
- The Base Device-tab slot refresh uses a proprietary AMS command and has no
|
||||
generic Moonraker translation.
|
||||
- Generic write-side macros stay unsupported until their printer contract is
|
||||
known and verified.
|
||||
|
||||
## Camera
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Discover a Moonraker webcam | Not verified | Not verified | Not verified | Unsupported |
|
||||
| Provide a camera source | Not verified | Not verified | Not verified | Not verified |
|
||||
| Live camera view | Not verified | Not verified | Not verified | Not verified |
|
||||
| Snapshot-only camera start | Unsupported | Unsupported | Unsupported | Not verified |
|
||||
| Camera start renewal and teardown | Unsupported | Unsupported | Unsupported | Partial |
|
||||
| Print thumbnail | Not verified | Not verified | Not verified | Not verified |
|
||||
|
||||
- Snapmaker bypasses webcam discovery with a local snapshot-polling page.
|
||||
- Its renewal and teardown path has lifetime risks without live evidence.
|
||||
- Moonraker thumbnail endpoint responses and filename-cache behavior need live
|
||||
coverage.
|
||||
|
||||
## Python plug-ins
|
||||
|
||||
| Capability | Python plug-in agent |
|
||||
| --- | --- |
|
||||
| Registration and re-registration | Not verified - lifecycle tests cover replacement |
|
||||
| Duplicate agent ID | Not verified - conflict is rejected and reported |
|
||||
| Disable or unload | Not verified - deregistration is tested; session teardown needs coverage |
|
||||
| Capability surface | Defined by the plug-in and exposed Python API |
|
||||
|
||||
## Reading the matrix safely
|
||||
|
||||
- `Partial` is not a softer form of `Supported`. It names a condition that must
|
||||
be checked before use. `Not verified` means the code was found, but no
|
||||
relevant live result is recorded.
|
||||
- Pair each claim with the evidence grades in the testing guide. This matters
|
||||
especially for CFS, Snapmaker camera, MMU macros, Qidi Send preflight, and
|
||||
thumbnail endpoints.
|
||||
|
||||
## Background - deliberate exclusions
|
||||
|
||||
The matrix excludes Bambu-specific cloud binding, RFID, calibration, and
|
||||
camera-control features from the Moonraker family. They use different protocol
|
||||
contracts and are deliberately refused when no safe Klipper equivalent exists.
|
||||
971
docs/printer-agent/reference/device-tab-annotations.html
Normal file
971
docs/printer-agent/reference/device-tab-annotations.html
Normal file
File diff suppressed because one or more lines are too long
280
docs/printer-agent/reference/manual-checklist.html
Normal file
280
docs/printer-agent/reference/manual-checklist.html
Normal file
@@ -0,0 +1,280 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Printer-agent manual checklist</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; --bg:#111827; --panel:#1f2937; --line:#4b5563;
|
||||
--text:#f9fafb; --muted:#cbd5e1; --pass:#34d399; --fail:#f87171; --block:#fbbf24; }
|
||||
* { box-sizing:border-box; }
|
||||
body { max-width:960px; margin:0 auto; padding:24px; font:15px/1.5 system-ui,sans-serif;
|
||||
color:var(--text); background:var(--bg); }
|
||||
h1 { margin:0 0 4px; } h2 { margin-top:32px; } p, li { max-width:78ch; }
|
||||
.muted { color:var(--muted); } .meta { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:12px; margin:20px 0; } label { display:grid; gap:4px; }
|
||||
input, textarea, button { font:inherit; } input, textarea { width:100%; padding:8px;
|
||||
border:1px solid var(--line); border-radius:6px; color:inherit; background:var(--panel); }
|
||||
textarea { min-height:58px; margin-top:8px; } .toolbar { display:flex; flex-wrap:wrap;
|
||||
gap:8px; align-items:center; margin:16px 0; } button { border:1px solid var(--line);
|
||||
border-radius:6px; padding:6px 10px; cursor:pointer; color:inherit; background:var(--panel); }
|
||||
button[data-state="pass"].active { color:#062d1d; background:var(--pass); }
|
||||
button[data-state="fail"].active { color:#3b0808; background:var(--fail); }
|
||||
button[data-state="blocked"].active { color:#3b2600; background:var(--block); }
|
||||
.progress { flex:1 1 240px; height:10px; overflow:hidden; border-radius:999px; background:var(--line); }
|
||||
.progress > div { height:100%; width:0; background:var(--pass); transition:width .15s; }
|
||||
.case { margin:10px 0; padding:12px; border:1px solid var(--line); border-left:4px solid var(--line);
|
||||
border-radius:6px; background:var(--panel); } .case.pass { border-left-color:var(--pass); }
|
||||
.case.fail { border-left-color:var(--fail); } .case.blocked { border-left-color:var(--block); }
|
||||
.case-head { display:flex; justify-content:space-between; gap:12px; align-items:start; }
|
||||
.case h3 { margin:0; font-size:1rem; } .actions { display:flex; gap:5px; white-space:nowrap; }
|
||||
.restore-warning { padding:8px 10px; border:1px solid var(--block); border-radius:6px;
|
||||
color:var(--block); background:var(--panel); }
|
||||
code { overflow-wrap:anywhere; } @media (max-width:600px) { body { padding:16px; }
|
||||
.meta { grid-template-columns:1fr; } .case-head { display:block; } .actions { margin-top:8px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Printer-agent manual checklist</h1>
|
||||
<p class="muted">Record what the printer did, not just what the UI displayed. Mark a case
|
||||
blocked when the required printer, configuration, or safe test condition is unavailable.</p>
|
||||
|
||||
<section class="meta" aria-label="Test context">
|
||||
<label>Agent and printer<input id="target" placeholder="for example: Moonraker - printer model"></label>
|
||||
<label>Firmware and configuration<input id="environment" placeholder="firmware, MMU, camera, API key setup"></label>
|
||||
<label>Build or package<input id="build" placeholder="version or build identifier"></label>
|
||||
<label>Tester and date<input id="tester" placeholder="name and date"></label>
|
||||
</section>
|
||||
|
||||
<div class="toolbar">
|
||||
<strong id="summary">0 of 0 cases marked</strong>
|
||||
<div class="progress" aria-label="Checklist progress"><div id="bar"></div></div>
|
||||
<button id="export" type="button">Export Markdown</button>
|
||||
<button id="reset" type="button">Reset checklist</button>
|
||||
</div>
|
||||
|
||||
<p id="restore-warning" class="restore-warning" hidden>Saved checklist data could not be
|
||||
restored. You can export the current blank checklist or use Reset to remove the saved data.</p>
|
||||
|
||||
<p class="muted">Use Pass only after observing the expected result. Fail needs enough evidence
|
||||
to reproduce it. Include response text, log markers, or firmware behavior in the note.</p>
|
||||
|
||||
<section>
|
||||
<h2>Connect and observe status</h2>
|
||||
<div class="case" data-id="connect">
|
||||
<div class="case-head"><div><h3>Connect to the selected printer</h3>
|
||||
<p>Expected: the Device tab receives a fresh status update after connection. Do not use a
|
||||
successful connection return alone as the result.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="status">
|
||||
<div class="case-head"><div><h3>Observe live status changes</h3>
|
||||
<p>Expected: temperature and target changes, fan state, print state, filename, progress,
|
||||
elapsed time, and homing state reach the UI while the printer changes state.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="reconnect">
|
||||
<div class="case-head"><div><h3>Disconnect and reconnect</h3>
|
||||
<p>Expected: a second connection produces new status messages and does not create a duplicate
|
||||
device. Record the post-reconnect status evidence.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="network-errors">
|
||||
<div class="case-head"><div><h3>Handle network and response failures</h3>
|
||||
<p>Expected: for discovery, status, G-code, upload, and print start, exercise controlled
|
||||
HTTP 401, 404, and 500 responses, invalid JSON, and refused sockets. Each operation must
|
||||
fail clearly or offer a retry, without a crash or a false success. Record the operation,
|
||||
injected failure, UI result, and any retry.</p></div><div class="actions"></div></div><textarea placeholder="Operation, injected failure, UI result, retry, and evidence"></textarea></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Controls and printing</h2>
|
||||
<div class="case" data-id="controls">
|
||||
<div class="case-head"><div><h3>Exercise safe controls</h3>
|
||||
<p>Expected: home, bed and nozzle temperature, and a harmless G-code command take effect on
|
||||
the printer. Do not use Moonraker jog as a safe control test while it can leave relative
|
||||
positioning active.</p></div><div class="actions"></div></div><textarea placeholder="Commands sent and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="fifo-order">
|
||||
<div class="case-head"><div><h3>Verify queued command order under latency</h3>
|
||||
<p>Expected: queue three harmless, uniquely marked commands while a proxy, network shaper,
|
||||
or request log introduces or records latency. The printer-side log must show the markers in
|
||||
the same order they were queued. Record the latency method and observed order.</p></div><div class="actions"></div></div><textarea placeholder="Queued markers, latency method, printer-side order, and result"></textarea></div>
|
||||
<div class="case" data-id="send-only">
|
||||
<div class="case-head"><div><h3>Send a file without starting it</h3>
|
||||
<p>Expected: the file appears on the printer and no print starts.</p></div><div class="actions"></div></div><textarea placeholder="Filename and observed result"></textarea></div>
|
||||
<div class="case" data-id="print">
|
||||
<div class="case-head"><div><h3>Start a small print</h3>
|
||||
<p>Expected: upload completes, the printer starts the selected file, and status transitions
|
||||
to printing.</p></div><div class="actions"></div></div><textarea placeholder="Filename, response, and observed result"></textarea></div>
|
||||
<div class="case" data-id="active-print-controls">
|
||||
<div class="case-head"><div><h3>Pause, resume, and cancel an active print</h3>
|
||||
<p>Expected: after the small print is actively printing, pause it and observe a paused state
|
||||
on both printer and UI; resume it and observe printing again; then cancel it and observe the
|
||||
printer stop and the UI leave the active or paused state.</p></div><div class="actions"></div></div><textarea placeholder="State transitions and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="print-failure">
|
||||
<div class="case-head"><div><h3>Check upload failure handling</h3>
|
||||
<p>Expected: cancellation during upload and a missing input fail clearly and do not begin a
|
||||
partial or unintended print.</p></div><div class="actions"></div></div><textarea placeholder="Failure path and observed result"></textarea></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Filament, camera, and agent-specific checks</h2>
|
||||
<div class="case" data-id="filament-read">
|
||||
<div class="case-head"><div><h3>Refresh material-system data</h3>
|
||||
<p>Expected: populated slots, empty slots, material, colour, and a live change are represented
|
||||
correctly. Moonraker-family agents pull this data; do not expect a subscription callback.</p></div><div class="actions"></div></div><textarea placeholder="MMU or box configuration and observed result"></textarea></div>
|
||||
<div class="case" data-id="filament-cleanup">
|
||||
<div class="case-head"><div><h3>Clear material data when filament or the system is absent</h3>
|
||||
<p>Expected: remove filament or disable the material system, refresh, and confirm the UI no
|
||||
longer shows obsolete slots, material names, or colours.</p></div><div class="actions"></div></div><textarea placeholder="Change made, refresh evidence, and remaining or cleared data"></textarea></div>
|
||||
<div class="case" data-id="filament-write">
|
||||
<div class="case-head"><div><h3>Verify print-time mapping where supported</h3>
|
||||
<p>Expected: only agents with a documented mapping implementation change printer-side mapping.
|
||||
Do not attempt load, unload, or slot-setting macros unless their printer-specific contract is
|
||||
known and safe.</p></div><div class="actions"></div></div><textarea placeholder="Mapping path, printer configuration, and result"></textarea></div>
|
||||
<div class="case" data-id="qidi-nozzle-preflight">
|
||||
<div class="case-head"><div><h3>Check Qidi Send preflight with missing nozzle identity</h3>
|
||||
<p>Expected: on a Qidi agent and compatible single-nozzle slice, Send proceeds when the
|
||||
Device tab has no reported nozzle diameter or type. It must not stop with
|
||||
<code>PrintStatusNozzleDataInvalid</code>. Record any reported identity and any mismatch
|
||||
result separately; this does not approve a known mismatch.</p></div><div class="actions"></div></div><textarea placeholder="Slice, reported nozzle data, preflight result, and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="camera">
|
||||
<div class="case-head"><div><h3>Verify a camera feed</h3>
|
||||
<p>Expected: frames advance and switching printers does not display a stale feed. For Snapmaker,
|
||||
observe immediately before and after 300 seconds in one open view. The renewal result is
|
||||
unknown until hardware evidence exists. Then swap agents and shut down the app to exercise
|
||||
teardown around the detached callback's raw-<code>this</code> lifetime risk.</p></div><div class="actions"></div></div><textarea placeholder="Camera type, timestamps, agent swap or shutdown result, and evidence"></textarea></div>
|
||||
<div class="case" data-id="thumbnail">
|
||||
<div class="case-head"><div><h3>Verify the print thumbnail</h3>
|
||||
<p>Expected: test a reused filename after its thumbnail changes, response payloads with both
|
||||
<code>thumbnail_path</code> and <code>relative_path</code>, and a path below the G-code
|
||||
root. The displayed image must match the current file in each case.</p></div><div class="actions"></div></div><textarea placeholder="Filename, endpoint key and path, displayed image, and observed result"></textarea></div>
|
||||
<div class="case" data-id="plugin">
|
||||
<div class="case-head"><div><h3>Reload a Python printer-agent plug-in</h3>
|
||||
<p>Expected: the capability registers once, duplicate agent IDs are rejected visibly, and
|
||||
disable or unload removes the agent cleanly.</p></div><div class="actions"></div></div><textarea placeholder="Plug-in identifier, actions, and observed result"></textarea></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const storageKey = 'orca-printer-agent-manual-checklist-v1';
|
||||
const cases = [...document.querySelectorAll('.case')];
|
||||
const inputs = [...document.querySelectorAll('input')];
|
||||
const restoreWarning = document.querySelector('#restore-warning');
|
||||
|
||||
function emptyState() {
|
||||
return { meta: {}, cases: {} };
|
||||
}
|
||||
function loadState() {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (!saved) return emptyState();
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object' ||
|
||||
(parsed.meta !== undefined && (Array.isArray(parsed.meta) || typeof parsed.meta !== 'object')) ||
|
||||
(parsed.cases !== undefined && (Array.isArray(parsed.cases) || typeof parsed.cases !== 'object'))) {
|
||||
throw new Error('incompatible saved checklist state');
|
||||
}
|
||||
return { meta: parsed.meta || {}, cases: parsed.cases || {} };
|
||||
} catch (error) {
|
||||
restoreWarning.hidden = false;
|
||||
return emptyState();
|
||||
}
|
||||
}
|
||||
const state = loadState();
|
||||
|
||||
function caseText(caseElement) {
|
||||
return caseElement.querySelector('h3').textContent.trim();
|
||||
}
|
||||
function caseCriteria(caseElement) {
|
||||
return [...caseElement.querySelectorAll('.case-head p')]
|
||||
.map(paragraph => markdownParagraph(paragraph.textContent))
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
}
|
||||
function save() {
|
||||
const data = { meta: {}, cases: {} };
|
||||
inputs.forEach(input => { data.meta[input.id] = input.value; });
|
||||
cases.forEach(item => {
|
||||
data.cases[item.dataset.id] = { status: item.dataset.status || '', note: item.querySelector('textarea').value };
|
||||
});
|
||||
localStorage.setItem(storageKey, JSON.stringify(data));
|
||||
updateProgress();
|
||||
}
|
||||
function renderStatus(item, status) {
|
||||
item.dataset.status = status;
|
||||
item.classList.toggle('pass', status === 'pass');
|
||||
item.classList.toggle('fail', status === 'fail');
|
||||
item.classList.toggle('blocked', status === 'blocked');
|
||||
item.querySelectorAll('button[data-state]').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.state === status);
|
||||
});
|
||||
}
|
||||
function applyStatus(item, status) {
|
||||
renderStatus(item, status);
|
||||
save();
|
||||
}
|
||||
function updateProgress() {
|
||||
const marked = cases.filter(item => item.dataset.status).length;
|
||||
document.querySelector('#summary').textContent = `${marked} of ${cases.length} cases marked`;
|
||||
document.querySelector('#bar').style.width = `${cases.length ? marked * 100 / cases.length : 0}%`;
|
||||
}
|
||||
function markdown() {
|
||||
const meta = stateFromInputs();
|
||||
const lines = ['# Printer-agent manual verification', '',
|
||||
`Target: ${markdownInline(meta.target) || 'not recorded'}`,
|
||||
`Firmware and configuration: ${markdownInline(meta.environment) || 'not recorded'}`,
|
||||
`Build or package: ${markdownInline(meta.build) || 'not recorded'}`,
|
||||
`Tester and date: ${markdownInline(meta.tester) || 'not recorded'}`, ''];
|
||||
cases.forEach(item => {
|
||||
const status = item.dataset.status || 'unmarked';
|
||||
const note = markdownInline(item.querySelector('textarea').value);
|
||||
lines.push(`## ${markdownInline(caseText(item))}`, '',
|
||||
`Status: ${status}`, '',
|
||||
'### Test criteria and expected result', '',
|
||||
caseCriteria(item) || 'Not recorded', '',
|
||||
'### Notes', '', note || 'None', '');
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
function markdownParagraph(value) {
|
||||
return markdownInline(value);
|
||||
}
|
||||
function markdownInline(value) {
|
||||
return String(value || '').replace(/\r\n?|\n/g, ' ').replace(/[\\`*_{}[\]<>#+!|]/g, '\\$&')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function stateFromInputs() {
|
||||
return Object.fromEntries(inputs.map(input => [input.id, input.value]));
|
||||
}
|
||||
function downloadMarkdown() {
|
||||
const blob = new Blob([markdown()], { type: 'text/markdown;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = 'printer-agent-manual-verification.md';
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
cases.forEach(item => {
|
||||
const actions = item.querySelector('.actions');
|
||||
['pass', 'fail', 'blocked'].forEach(status => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.dataset.state = status;
|
||||
button.textContent = status[0].toUpperCase() + status.slice(1);
|
||||
button.addEventListener('click', () => applyStatus(item, status));
|
||||
actions.appendChild(button);
|
||||
});
|
||||
const saved = state.cases[item.dataset.id] || {};
|
||||
item.querySelector('textarea').value = typeof saved.note === 'string' ? saved.note : '';
|
||||
if (['pass', 'fail', 'blocked'].includes(saved.status)) renderStatus(item, saved.status);
|
||||
item.querySelector('textarea').addEventListener('input', save);
|
||||
});
|
||||
inputs.forEach(input => {
|
||||
input.value = typeof state.meta[input.id] === 'string' ? state.meta[input.id] : '';
|
||||
input.addEventListener('input', save);
|
||||
});
|
||||
document.querySelector('#export').addEventListener('click', downloadMarkdown);
|
||||
document.querySelector('#reset').addEventListener('click', () => {
|
||||
if (!confirm('Clear all saved checklist data for this browser?')) return;
|
||||
localStorage.removeItem(storageKey);
|
||||
location.reload();
|
||||
});
|
||||
updateProgress();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
249
docs/printer-agent/testing.md
Normal file
249
docs/printer-agent/testing.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Testing and troubleshooting printer agents
|
||||
|
||||
*Owns evidence grades, the automated and manual verification passes, and
|
||||
the open-defect register. Every "not hardware-verified" note elsewhere in
|
||||
this guide resolves to a grade defined here.*
|
||||
|
||||
This page describes how to assess a printer-agent change without treating
|
||||
source inspection as a hardware result. Use the
|
||||
[manual checklist](reference/manual-checklist.html) for a repeatable live-printer
|
||||
pass, and use the [capability matrix](reference/capability-matrix.md) to decide
|
||||
which cases apply to the agent being changed.
|
||||
|
||||
## Evidence grades
|
||||
|
||||
Keep these grades separate in reviews and release notes.
|
||||
|
||||
- Source-inspected - the current implementation was read. It establishes
|
||||
intended behavior, not printer compatibility.
|
||||
- Automated - a targeted test ran. It covers its inputs and assertions, not a
|
||||
printer, firmware version, or network failure that it does not model.
|
||||
- Hardware-verified - the stated behavior was observed on a named class of
|
||||
live printer. Record the model, firmware, configuration, and result with the
|
||||
test evidence.
|
||||
|
||||
Do not call a capability supported by hardware solely because the code compiles or
|
||||
a unit test passes. Conversely, a hardware observation should not be generalized
|
||||
to every Moonraker-family printer without checking its configuration.
|
||||
|
||||
## Carried hardware evidence
|
||||
|
||||
Prior hardware sessions verified direct Moonraker-family connection, live
|
||||
WebSocket status, and jog on both a Qidi/Moonraker printer and a generic
|
||||
Moonraker box. The jog moved the printer but could leave relative positioning
|
||||
active. This evidence establishes the Moonraker-base and Qidi grades in the
|
||||
capability matrix. It was carried into this rewrite and was not rerun here.
|
||||
|
||||
It does not establish those behaviors for Creality or Snapmaker, and it does
|
||||
not cover Qidi-specific filament discovery, box mapping, or print wrappers.
|
||||
|
||||
## Build and automated tests
|
||||
|
||||
Run the smallest relevant test target first, then broaden the run if the change
|
||||
crosses shared agent, plug-in, or Device-tab code. Set
|
||||
`<configured-build-dir>` to the CMake build tree that was already configured for
|
||||
the compiler, generator, and build type you intend to use. Do not replace it
|
||||
with the source directory or assume a `build` subdirectory exists.
|
||||
|
||||
```powershell
|
||||
cmake --build <configured-build-dir> --config RelWithDebInfo --target slic3rutils_tests
|
||||
cmake --build <configured-build-dir> --config RelWithDebInfo --target printer_agent_plugin_tests
|
||||
ctest --test-dir <configured-build-dir>/tests/libslic3r --output-on-failure
|
||||
```
|
||||
|
||||
`--config RelWithDebInfo` is needed for multi-config generators such as Visual
|
||||
Studio. Omit it only when the configured generator is single-config and its
|
||||
build type was selected at configure time. Parallel-build options belong to the
|
||||
generator: for example, pass `--parallel 6` to CMake when the generator
|
||||
supports it, or use the generator's own trailing arguments only when that
|
||||
generator documents them. Do not combine a changed working directory, a
|
||||
generator-specific flag, and an assumed build-tree layout in one command.
|
||||
|
||||
On Windows, start from an MSVC developer environment. A shell without the MSVC
|
||||
include paths can fail in dependencies before it compiles Orca code, with errors
|
||||
such as `C1083: Cannot open include file: 'stddef.h'`, `'time.h'`, or `'cstdint'`.
|
||||
Those signatures are environment failures, not evidence against the agent change.
|
||||
|
||||
If a machine exhausts MSVC precompiled-header memory, use the documented lower
|
||||
parallelism command:
|
||||
|
||||
```powershell
|
||||
cmake --build <configured-build-dir> --config RelWithDebInfo --target slic3rutils_tests --parallel 6
|
||||
```
|
||||
|
||||
Errors such as `C3859: Failed to create virtual memory for PCH` and `C1076:
|
||||
internal heap limit reached` are machine-specific resource failures. If a build
|
||||
appears hung and file operations are blocked, inspect for idle `cl.exe` processes
|
||||
holding locks before changing source.
|
||||
|
||||
Relevant automated coverage includes:
|
||||
|
||||
- `tests/slic3rutils/test_qidi_printer_agent.cpp` validates malformed and null
|
||||
Qidi slot responses without throwing.
|
||||
- `tests/slic3rutils/test_printer_agent.cpp` checks the public printer-agent
|
||||
surface, including filament-sync mode exposure.
|
||||
- `tests/slic3rutils/test_printer_agent_plugin.cpp` exercises plug-in
|
||||
registration, replacement, conflict handling, and deregistration.
|
||||
|
||||
Do not present a historic test count, failure count, or skipped-test count as the
|
||||
current state. Run the command above and attach its own output when a current
|
||||
result is needed.
|
||||
|
||||
## Manual hardware verification
|
||||
|
||||
Use a small, disposable model and a printer that can safely accept the actions.
|
||||
The checklist groups the work in the order below.
|
||||
|
||||
1. Confirm the printer accepts its configured URL and API key, then select it in
|
||||
the Device tab. Verify a fresh status update, not merely a successful connect
|
||||
return code.
|
||||
2. Observe temperatures, targets, fan state, print state, filename, progress,
|
||||
elapsed time, and axis homing while the printer changes state.
|
||||
3. Exercise safe idle controls first: home, bed and nozzle temperature, and a
|
||||
harmless G-code command. Verify the printer's action as well as the UI
|
||||
response.
|
||||
4. Send a small file without starting it, then start a small print. Once the
|
||||
print is active, pause it, confirm the printer and UI both enter a paused
|
||||
state, resume it, and confirm both return to printing. Cancel only after
|
||||
observing an active or paused print, then confirm that the printer stops and
|
||||
the UI leaves that state. Cancel an upload and retry a missing input so that
|
||||
failure handling is observed too.
|
||||
5. For Moonraker command workers, send three harmless, uniquely marked commands
|
||||
while a proxy, network shaper, or request log introduces or records latency.
|
||||
Pass only if the printer-side log records the markers in the same order they
|
||||
were queued. Record the latency method and the observed order.
|
||||
6. For a material system, verify populated slots, empty slots, material, colour,
|
||||
refresh after a change, and cleanup when the system is absent or filament is
|
||||
removed. The latter must remove obsolete slot or material data from the UI.
|
||||
Do not infer write support from read support.
|
||||
7. For Qidi, use a compatible single-nozzle slice and Send it while the Device
|
||||
tab has no reported nozzle diameter or type. Pass only if Send proceeds past
|
||||
preflight without `PrintStatusNozzleDataInvalid`; record any reported
|
||||
diameter/type and any mismatch message separately. This checks the intended
|
||||
tolerance for missing identity data, not that a mismatched known nozzle is
|
||||
safe.
|
||||
8. Verify the camera only on hardware that advertises or implements it. Check
|
||||
that frames advance, switching printers starts the newly selected camera, and
|
||||
closing or changing the view does not leave misleading stale output. For
|
||||
Snapmaker, also test immediately before and after 300 seconds in an
|
||||
uninterrupted view. The expected renewal result is unknown until hardware
|
||||
evidence exists. Swap agents and shut down the app after the camera cases to
|
||||
exercise teardown around the detached callback's raw-`this` lifetime risk.
|
||||
9. For Moonraker thumbnails, test a reused filename after its thumbnail changes,
|
||||
responses that use `thumbnail_path` and `relative_path`, and a thumbnail in a
|
||||
subdirectory below the G-code root. Record the endpoint payload and displayed
|
||||
image for each case.
|
||||
10. Disconnect and reconnect the printer, then confirm that new status messages
|
||||
still reach the UI. A reconnect completion alone is insufficient evidence.
|
||||
11. Test network and response failures for discovery, status, G-code, upload,
|
||||
and print start. For each operation, exercise HTTP 401, 404, and 500,
|
||||
invalid JSON, and a refused socket with a controlled proxy or test server.
|
||||
Each case must fail clearly or offer a retry, without a crash or a false
|
||||
success. Record the operation, injected failure, UI result, and any retry.
|
||||
|
||||
For Moonraker, record whether the thumbnail endpoint returns the response shape
|
||||
the agent expects. That response has not yet been verified across a live
|
||||
Moonraker deployment.
|
||||
|
||||
## Troubleshooting by symptom
|
||||
|
||||
### Connection appears successful but the Device tab stays stale
|
||||
|
||||
Treat status freshness as the connection result. Enable `ORCA_NETWORK_DEBUG` and
|
||||
look for a new `parse_json: dev_id=` entry after the connection or reconnection.
|
||||
The unresolved reconnect-delivery problem can complete the second connection
|
||||
without delivering any new parsed messages. Capture an instrumented second
|
||||
connection before changing dispatch or message-delay logic, because both remain
|
||||
plausible causes.
|
||||
|
||||
Check identity too. One path can use a bare IP address while another uses
|
||||
`host:port`; configuring a port can therefore create two machine objects. Do not
|
||||
diagnose a duplicate as a printer-agent failure until the identities are
|
||||
compared.
|
||||
|
||||
### A control reports success but the printer did not change
|
||||
|
||||
First establish that the command has a documented translation in the capability
|
||||
matrix. Unsupported commands are deliberately rejected rather than silently
|
||||
accepted. For Moonraker, queued controls are asynchronous, so wait for the
|
||||
printer-side result and capture the request or log before concluding it was lost.
|
||||
|
||||
Moonraker jog is a special case. The current path can leave the printer in
|
||||
relative positioning mode after a jog. Do not use it as a general verification
|
||||
control until it is changed to save state, issue `G91` and the move, then restore
|
||||
state with `SAVE_GCODE_STATE` and `RESTORE_GCODE_STATE`. Extruder-relative moves
|
||||
use a separate `M83` path.
|
||||
|
||||
### A thumbnail is missing or belongs to an earlier print
|
||||
|
||||
The thumbnail lookup accepts both `thumbnail_path` and `relative_path`, but the
|
||||
live endpoint response is not yet verified. The cache is keyed by filename, so
|
||||
reusing a common name can retain the previous image. Test a distinct filename
|
||||
before changing the lookup. Paths below the G-code root also need live coverage
|
||||
for the `relative_path` fallback.
|
||||
|
||||
### Filament looks stale, blank, or does not follow an edit
|
||||
|
||||
Moonraker-family agents use pull-mode filament sync. Verify the pull request and
|
||||
the resulting Device-tab update rather than expecting a subscription callback.
|
||||
Read-side discovery does not establish load, unload, slot-setting, or Auto Refill
|
||||
support. Happy Hare and AFC macro names are printer-side configuration; do not
|
||||
guess them. A guessed macro can silently do nothing or issue the wrong action.
|
||||
|
||||
### A Python agent disappears or cannot be enabled
|
||||
|
||||
Check its agent ID first. A duplicate ID is rejected and the conflicting
|
||||
capability is disabled rather than auto-promoted later, because automatic
|
||||
promotion could change the active printer implementation without user intent.
|
||||
Reload and teardown also need a live check: registration tests cover lifecycle
|
||||
logic, but a plug-in can still be exposed to API drift or a teardown race in a
|
||||
real session.
|
||||
|
||||
## Known defects and safeguards
|
||||
|
||||
- Reconnect delivery remains unresolved. Instrument the second connection before
|
||||
attempting a fix; the observed failure is stale data after a completed reconnect.
|
||||
- Moonraker jog can leave relative mode active. Keep the future state-save and
|
||||
restore sequence together so the jog cannot affect later G-code positioning.
|
||||
- Qidi's nozzle-data Send-preflight tolerance for unreported diameter and type
|
||||
has not received a hardware verification.
|
||||
- A configured `host:port` can coexist with a bare-IP machine identity. This can
|
||||
duplicate devices and confuse selection.
|
||||
- Moonraker thumbnail caching can show an old image when a filename is reused.
|
||||
- Moonraker filament data can be stale, and its pull/read path does not provide
|
||||
safe generic write-side MMU operations.
|
||||
- Duplicate plug-in agent IDs are rejected. There is no automatic fallback to a
|
||||
losing capability after the winner unloads.
|
||||
- Plug-in implementations can drift from the Python printer-agent API. Treat an
|
||||
import or interface error as a plug-in compatibility issue until proved otherwise.
|
||||
- Snapmaker camera callbacks can outlive their view during agent replacement or
|
||||
shutdown because the detached path retains a raw `this` pointer. Treat a crash
|
||||
or stale callback during those transitions as a source-derived use-after-free
|
||||
risk until the lifetime is made explicit.
|
||||
- Snapmaker writes an IP-specific local camera HTML file below the application
|
||||
cache. The source contains no cleanup path, so residual files can accumulate
|
||||
for each unique printer IP. This is source-derived and was not reproduced.
|
||||
|
||||
## Not yet hardware-verified
|
||||
|
||||
- Moonraker command-worker FIFO behavior under recorded or injected network
|
||||
latency.
|
||||
- Moonraker thumbnail responses: reused filenames, `thumbnail_path`,
|
||||
`relative_path`, and subdirectory paths.
|
||||
- Snapmaker camera behavior on a live U1: frames, renewal across a long-open
|
||||
view including the 300-second boundary, switching between printers, agent
|
||||
replacement, and shutdown teardown.
|
||||
- Whether Snapmaker's renewal cadence prevents a stale-frame interval. Do not
|
||||
shorten it as a workaround without resolving the printer-side behavior first.
|
||||
- Creality CFS detection and preset scoring on a real printer.
|
||||
- Qidi Send preflight when firmware omits nozzle diameter and type.
|
||||
- Moonraker-family write-side MMU commands. They remain blocked on verified,
|
||||
printer-specific macro contracts.
|
||||
|
||||
## Background - source locations for maintainers
|
||||
|
||||
The Moonraker command worker, status stream, print path, and thumbnail
|
||||
lookup live in `MoonrakerPrinterAgent`. Qidi maps its material box before routing
|
||||
to the Moonraker base. Snapmaker adds the camera start request and its snapshot
|
||||
page. Python agent registration and conflict handling live in
|
||||
`NetworkAgentFactory`.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,79 +0,0 @@
|
||||
#!/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)}")
|
||||
1243
resources/orca_plugins/BBLPrinterAgentPlugin.py
Normal file
1243
resources/orca_plugins/BBLPrinterAgentPlugin.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,9 @@
|
||||
{
|
||||
"name": "Custom Printer",
|
||||
"version": "02.04.00.04",
|
||||
"version": "02.04.00.01",
|
||||
"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"
|
||||
@@ -66,14 +62,6 @@
|
||||
"name": "0.16mm Optimal @MyKlipper",
|
||||
"sub_path": "process/0.16mm Optimal @MyKlipper.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @MyBeltPrinter",
|
||||
"sub_path": "process/0.12mm Fine @MyBeltPrinter.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"
|
||||
@@ -274,38 +262,18 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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": "MyRepetier 0.4 nozzle",
|
||||
"sub_path": "machine/MyRepetier 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "MyRRF 0.4 nozzle",
|
||||
"sub_path": "machine/MyRRF 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "MyToolChanger 0.2 nozzle",
|
||||
"sub_path": "machine/MyToolChanger 0.2 nozzle.json"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"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.4"
|
||||
],
|
||||
"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",
|
||||
"belt_slice_rotation_global": "1",
|
||||
"build_plate_tilt_x": "45",
|
||||
"purge_in_prime_tower": "0",
|
||||
"scan_first_layer": "0",
|
||||
"auxiliary_fan": "0"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "IdeaFormer",
|
||||
"version": "02.00.00.03",
|
||||
"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.
|
Before Width: | Height: | Size: 183 KiB |
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Generic PETG @IdeaFormer IR3 V2",
|
||||
"inherits": "Generic PETG @System",
|
||||
"from": "system",
|
||||
"setting_id": "n4zaXcUUzTqAxq5f",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"IdeaFormer IR3 V2 0.4 nozzle"
|
||||
],
|
||||
"filament_type": [
|
||||
"PETG"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Generic"
|
||||
],
|
||||
"filament_settings_id": [
|
||||
"Generic PETG @IdeaFormer IR3 V2"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.27"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.95"
|
||||
],
|
||||
"filament_cost": [
|
||||
"25"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"10"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"240"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"245"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"220"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"260"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"cool_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"40"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"60"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"25%"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"80"
|
||||
],
|
||||
"close_fan_the_first_x_layers": [
|
||||
"3"
|
||||
],
|
||||
"full_fan_speed_layer": [
|
||||
"8"
|
||||
],
|
||||
"slow_down_min_speed": [
|
||||
"20"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"100"
|
||||
],
|
||||
"reduce_fan_stop_start_freq": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"2"
|
||||
],
|
||||
"filament_retraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"filament_deretraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"filament_z_hop": [
|
||||
"0.4"
|
||||
],
|
||||
"filament_start_gcode": [
|
||||
"; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
|
||||
]
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Generic PLA @IdeaFormer IR3 V2",
|
||||
"inherits": "Generic PLA @System",
|
||||
"from": "system",
|
||||
"setting_id": "1xjycsEAFh6KQIhp",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"IdeaFormer IR3 V2 0.4 nozzle"
|
||||
],
|
||||
"filament_type": [
|
||||
"PLA"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Generic"
|
||||
],
|
||||
"filament_settings_id": [
|
||||
"Generic PLA @IdeaFormer IR3 V2"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.24"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"filament_cost": [
|
||||
"20"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"215"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"220"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"190"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"240"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"100"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"100"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"50%"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"100"
|
||||
],
|
||||
"close_fan_the_first_x_layers": [
|
||||
"3"
|
||||
],
|
||||
"full_fan_speed_layer": [
|
||||
"8"
|
||||
],
|
||||
"slow_down_min_speed": [
|
||||
"20"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"100"
|
||||
],
|
||||
"reduce_fan_stop_start_freq": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"1.5"
|
||||
],
|
||||
"filament_retraction_speed": [
|
||||
"35"
|
||||
],
|
||||
"filament_deretraction_speed": [
|
||||
"30"
|
||||
],
|
||||
"filament_z_hop": [
|
||||
"0.4"
|
||||
],
|
||||
"filament_start_gcode": [
|
||||
"; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "eSUN PLA @IdeaFormer IR3 V2",
|
||||
"inherits": "Generic PLA @IdeaFormer IR3 V2",
|
||||
"from": "system",
|
||||
"setting_id": "XqkviBmFHEglXueX",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
{
|
||||
"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_e": [
|
||||
"5000"
|
||||
],
|
||||
"machine_max_acceleration_extruding": [
|
||||
"5000"
|
||||
],
|
||||
"machine_max_acceleration_retracting": [
|
||||
"1000"
|
||||
],
|
||||
"machine_max_acceleration_travel": [
|
||||
"9000"
|
||||
],
|
||||
"machine_max_acceleration_x": [
|
||||
"5000"
|
||||
],
|
||||
"machine_max_acceleration_y": [
|
||||
"5000"
|
||||
],
|
||||
"machine_max_acceleration_z": [
|
||||
"100"
|
||||
],
|
||||
"machine_max_jerk_e": [
|
||||
"2.5"
|
||||
],
|
||||
"machine_max_jerk_x": [
|
||||
"10"
|
||||
],
|
||||
"machine_max_jerk_y": [
|
||||
"10"
|
||||
],
|
||||
"machine_max_jerk_z": [
|
||||
"0.4"
|
||||
],
|
||||
"machine_max_speed_e": [
|
||||
"60"
|
||||
],
|
||||
"machine_max_speed_x": [
|
||||
"500"
|
||||
],
|
||||
"machine_max_speed_y": [
|
||||
"500"
|
||||
],
|
||||
"machine_max_speed_z": [
|
||||
"20"
|
||||
],
|
||||
"retraction_length": [
|
||||
"2"
|
||||
],
|
||||
"retraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"deretraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"z_hop": [
|
||||
"0.4"
|
||||
],
|
||||
"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)"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"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.4"
|
||||
],
|
||||
"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",
|
||||
"belt_slice_rotation_global": "1",
|
||||
"build_plate_tilt_x": "45",
|
||||
"purge_in_prime_tower": "0",
|
||||
"scan_first_layer": "0",
|
||||
"auxiliary_fan": "0"
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"silent_mode": "0",
|
||||
"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"
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"silent_mode": "0",
|
||||
"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"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"type": "process",
|
||||
"name": "fdm_process_common",
|
||||
"from": "system",
|
||||
"instantiation": "false",
|
||||
"adaptive_layer_height": "0",
|
||||
"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",
|
||||
"tree_support_with_infill": "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,54 +0,0 @@
|
||||
{
|
||||
"name": "Printcepts",
|
||||
"version": "01.00.00.01",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Generic PETG @BabyBelt Pro",
|
||||
"inherits": "Generic PETG @System",
|
||||
"from": "system",
|
||||
"setting_id": "gCzHpDNgVwQR6tgk",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"BabyBelt Pro 0.4 nozzle"
|
||||
],
|
||||
"filament_type": [
|
||||
"PETG"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Generic"
|
||||
],
|
||||
"filament_settings_id": [
|
||||
"Generic PETG @BabyBelt Pro"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.27"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.95"
|
||||
],
|
||||
"filament_cost": [
|
||||
"25"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"10"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"240"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"245"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"220"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"260"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"cool_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"80"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"80"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"40"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"60"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"25%"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"80"
|
||||
],
|
||||
"close_fan_the_first_x_layers": [
|
||||
"3"
|
||||
],
|
||||
"full_fan_speed_layer": [
|
||||
"8"
|
||||
],
|
||||
"slow_down_min_speed": [
|
||||
"20"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"100"
|
||||
],
|
||||
"reduce_fan_stop_start_freq": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"2"
|
||||
],
|
||||
"filament_retraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"filament_deretraction_speed": [
|
||||
"40"
|
||||
],
|
||||
"filament_z_hop": [
|
||||
"0.4"
|
||||
],
|
||||
"filament_start_gcode": [
|
||||
"; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
|
||||
]
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Generic PLA @BabyBelt Pro",
|
||||
"inherits": "Generic PLA @System",
|
||||
"from": "system",
|
||||
"setting_id": "24PpcnhVx9v5f4fD",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"BabyBelt Pro 0.4 nozzle"
|
||||
],
|
||||
"filament_type": [
|
||||
"PLA"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Generic"
|
||||
],
|
||||
"filament_settings_id": [
|
||||
"Generic PLA @BabyBelt Pro"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.24"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"filament_cost": [
|
||||
"20"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"215"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"220"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"190"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"240"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"100"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"100"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"50%"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"100"
|
||||
],
|
||||
"close_fan_the_first_x_layers": [
|
||||
"3"
|
||||
],
|
||||
"full_fan_speed_layer": [
|
||||
"8"
|
||||
],
|
||||
"slow_down_min_speed": [
|
||||
"20"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"100"
|
||||
],
|
||||
"reduce_fan_stop_start_freq": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"1.5"
|
||||
],
|
||||
"filament_retraction_speed": [
|
||||
"35"
|
||||
],
|
||||
"filament_deretraction_speed": [
|
||||
"30"
|
||||
],
|
||||
"filament_z_hop": [
|
||||
"0.4"
|
||||
],
|
||||
"filament_start_gcode": [
|
||||
"; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "eSUN PLA @BabyBelt Pro",
|
||||
"inherits": "Generic PLA @BabyBelt Pro",
|
||||
"from": "system",
|
||||
"setting_id": "EH3X7oE0DU5tSpjW",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"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.4"
|
||||
],
|
||||
"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",
|
||||
"belt_slice_rotation_global": "1",
|
||||
"build_plate_tilt_x": "45",
|
||||
"purge_in_prime_tower": "0",
|
||||
"scan_first_layer": "0",
|
||||
"auxiliary_fan": "0"
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"silent_mode": "0",
|
||||
"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"
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"silent_mode": "0",
|
||||
"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"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"type": "process",
|
||||
"name": "fdm_process_common",
|
||||
"from": "system",
|
||||
"instantiation": "false",
|
||||
"adaptive_layer_height": "0",
|
||||
"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",
|
||||
"tree_support_with_infill": "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"
|
||||
}
|
||||
@@ -26,7 +26,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform vec4 uniform_color;
|
||||
|
||||
@@ -23,7 +23,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -74,8 +73,8 @@ void main()
|
||||
// Point in homogenous coordinates.
|
||||
world_pos = volume_world_matrix * vec4(v_position, 1.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;
|
||||
// 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;
|
||||
|
||||
gl_Position = projection_matrix * position;
|
||||
if (is_outline) {
|
||||
|
||||
@@ -37,7 +37,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
|
||||
@@ -86,7 +85,7 @@ void main()
|
||||
color = LightBlue;
|
||||
alpha = 1.0;
|
||||
}
|
||||
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
|
||||
else if( transformed_normal.z < slope.normal_z - EPSILON)
|
||||
{
|
||||
color = color * 0.5 + LightRed * 0.5;
|
||||
alpha = 1.0;
|
||||
|
||||
@@ -24,7 +24,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
void main()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#version 140
|
||||
// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline).
|
||||
// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture.
|
||||
#extension GL_ARB_texture_multisample : enable
|
||||
|
||||
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
|
||||
//BBS: add grey and orange
|
||||
@@ -26,7 +29,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform vec4 uniform_color;
|
||||
@@ -37,7 +39,14 @@ uniform SlopeDetection slope;
|
||||
|
||||
//BBS: add outline_color
|
||||
uniform bool is_outline;
|
||||
// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the
|
||||
// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer.
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
uniform sampler2DMS depth_tex;
|
||||
uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off
|
||||
#else
|
||||
uniform sampler2D depth_tex;
|
||||
#endif
|
||||
uniform vec2 screen_size;
|
||||
|
||||
#ifdef ENABLE_ENVIRONMENT_MAP
|
||||
@@ -100,45 +109,87 @@ float GetTolerance(float d, float k)
|
||||
return -k*(d+A)*(d+A)/B;
|
||||
}
|
||||
|
||||
float DetectSilho(vec2 fragCoord, vec2 dir)
|
||||
// Depth of sample s at integer pixel coord.
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
float FetchDepth(ivec2 coord, int s)
|
||||
{
|
||||
// texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE).
|
||||
ivec2 sz = textureSize(depth_tex);
|
||||
return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r);
|
||||
}
|
||||
#else
|
||||
float FetchDepth(ivec2 coord, int s)
|
||||
{
|
||||
return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r);
|
||||
}
|
||||
#endif
|
||||
|
||||
float DetectSilho(ivec2 coord, ivec2 dir, int s)
|
||||
{
|
||||
// -------------------------------------------
|
||||
// x0 ___ x1----o
|
||||
// :\ :
|
||||
// x0 ___ x1----o
|
||||
// :\ :
|
||||
// r0 : \ : r1
|
||||
// : \ :
|
||||
// : \ :
|
||||
// o---x2 ___ x3
|
||||
//
|
||||
// r0 and r1 are the differences between actual
|
||||
// and expected (as if x0..3 where on the same
|
||||
// plane) depth values.
|
||||
// -------------------------------------------
|
||||
|
||||
float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r);
|
||||
float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r);
|
||||
float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r);
|
||||
float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r);
|
||||
|
||||
float x0 = FetchDepth(coord + dir*-2, s);
|
||||
float x1 = FetchDepth(coord + dir*-1, s);
|
||||
float x2 = FetchDepth(coord, s);
|
||||
float x3 = FetchDepth(coord + dir* 1, s);
|
||||
|
||||
float d0 = (x1-x0);
|
||||
float d1 = (x2-x3);
|
||||
|
||||
|
||||
float r0 = x1 + d0 - x2;
|
||||
float r1 = x2 + d1 - x1;
|
||||
|
||||
float tol = GetTolerance(x2, 0.04);
|
||||
|
||||
return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0));
|
||||
|
||||
float tol = GetTolerance(x2, 0.04);
|
||||
|
||||
return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0));
|
||||
}
|
||||
|
||||
float DetectSilho(vec2 fragCoord)
|
||||
float DetectSilho(ivec2 coord, int s)
|
||||
{
|
||||
return max(
|
||||
DetectSilho(fragCoord, vec2(1,0)), // Horizontal
|
||||
DetectSilho(fragCoord, vec2(0,1)) // Vertical
|
||||
DetectSilho(coord, ivec2(1,0), s), // Horizontal
|
||||
DetectSilho(coord, ivec2(0,1), s) // Vertical
|
||||
);
|
||||
}
|
||||
|
||||
// Full response of one sample. Reduce the max() per sample and average only afterwards:
|
||||
// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines.
|
||||
float DetectSilhoSample(ivec2 coord, int s)
|
||||
{
|
||||
float v = DetectSilho(coord, s);
|
||||
// Makes silhouettes thicker.
|
||||
for (int i = 1; i <= INFLATE; ++i)
|
||||
{
|
||||
v = max(v, DetectSilho(coord + ivec2(i, 0), s));
|
||||
v = max(v, DetectSilho(coord + ivec2(0, i), s));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Average the per-sample coverage into the sub-pixel anti-aliasing of the line.
|
||||
float DetectSilho(vec2 fragCoord)
|
||||
{
|
||||
ivec2 coord = ivec2(fragCoord);
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
int n = max(msaa_samples, 1);
|
||||
#else
|
||||
const int n = 1;
|
||||
#endif
|
||||
float acc = 0.0;
|
||||
for (int s = 0; s < n; ++s)
|
||||
acc += DetectSilhoSample(coord, s);
|
||||
return acc / float(n);
|
||||
}
|
||||
|
||||
// Returns a lighting multiplier in [1 - shadow_intensity, 1]: < 1 where the fragment is
|
||||
// occluded from the light in the shadow map. 3x3 PCF softens the edges.
|
||||
float shadow_shade()
|
||||
@@ -225,14 +276,7 @@ void main()
|
||||
//BBS: add outline_color
|
||||
if (is_outline) {
|
||||
color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
|
||||
vec2 fragCoord = gl_FragCoord.xy;
|
||||
float s = DetectSilho(fragCoord);
|
||||
// Makes silhouettes thicker.
|
||||
for(int i=1;i<=INFLATE; i++)
|
||||
{
|
||||
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
|
||||
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
|
||||
}
|
||||
float s = DetectSilho(gl_FragCoord.xy);
|
||||
if (s < 0.01)
|
||||
discard;
|
||||
out_color = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);
|
||||
|
||||
@@ -23,7 +23,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -74,8 +73,8 @@ void main()
|
||||
// Point in homogenous coordinates.
|
||||
world_pos = volume_world_matrix * vec4(v_position, 1.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;
|
||||
// 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;
|
||||
|
||||
gl_Position = projection_matrix * position;
|
||||
if (is_outline) {
|
||||
|
||||
@@ -37,7 +37,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
|
||||
@@ -88,7 +87,7 @@ void main()
|
||||
color = LightBlue;
|
||||
alpha = 1.0;
|
||||
}
|
||||
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
|
||||
else if( transformed_normal.z < slope.normal_z - EPSILON)
|
||||
{
|
||||
color = color * 0.5 + LightRed * 0.5;
|
||||
alpha = 1.0;
|
||||
|
||||
@@ -24,7 +24,6 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
void main()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#version 140
|
||||
// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline).
|
||||
// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture.
|
||||
#extension GL_ARB_texture_multisample : enable
|
||||
|
||||
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
|
||||
const vec3 LightRed = vec3(0.78, 0.0, 0.0);
|
||||
@@ -51,7 +54,14 @@ uniform SlopeDetection slope;
|
||||
|
||||
//BBS: add outline_color
|
||||
uniform bool is_outline;
|
||||
// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the
|
||||
// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer.
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
uniform sampler2DMS depth_tex;
|
||||
uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off
|
||||
#else
|
||||
uniform sampler2D depth_tex;
|
||||
#endif
|
||||
uniform vec2 screen_size;
|
||||
|
||||
#ifdef ENABLE_ENVIRONMENT_MAP
|
||||
@@ -100,12 +110,27 @@ float GetTolerance(float d, float k)
|
||||
return -k*(d+A)*(d+A)/B;
|
||||
}
|
||||
|
||||
float DetectSilho(vec2 fragCoord, vec2 dir)
|
||||
// Depth of sample s at integer pixel coord.
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
float FetchDepth(ivec2 coord, int s)
|
||||
{
|
||||
float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r);
|
||||
float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r);
|
||||
float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r);
|
||||
float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r);
|
||||
// texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE).
|
||||
ivec2 sz = textureSize(depth_tex);
|
||||
return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r);
|
||||
}
|
||||
#else
|
||||
float FetchDepth(ivec2 coord, int s)
|
||||
{
|
||||
return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r);
|
||||
}
|
||||
#endif
|
||||
|
||||
float DetectSilho(ivec2 coord, ivec2 dir, int s)
|
||||
{
|
||||
float x0 = FetchDepth(coord + dir*-2, s);
|
||||
float x1 = FetchDepth(coord + dir*-1, s);
|
||||
float x2 = FetchDepth(coord, s);
|
||||
float x3 = FetchDepth(coord + dir* 1, s);
|
||||
|
||||
float d0 = (x1-x0);
|
||||
float d1 = (x2-x3);
|
||||
@@ -116,17 +141,45 @@ float DetectSilho(vec2 fragCoord, vec2 dir)
|
||||
float tol = GetTolerance(x2, 0.04);
|
||||
|
||||
return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0));
|
||||
|
||||
}
|
||||
|
||||
float DetectSilho(vec2 fragCoord)
|
||||
float DetectSilho(ivec2 coord, int s)
|
||||
{
|
||||
return max(
|
||||
DetectSilho(fragCoord, vec2(1,0)),
|
||||
DetectSilho(fragCoord, vec2(0,1))
|
||||
DetectSilho(coord, ivec2(1,0), s),
|
||||
DetectSilho(coord, ivec2(0,1), s)
|
||||
);
|
||||
}
|
||||
|
||||
// Full response of one sample. Reduce the max() per sample and average only afterwards:
|
||||
// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines.
|
||||
float DetectSilhoSample(ivec2 coord, int s)
|
||||
{
|
||||
float v = DetectSilho(coord, s);
|
||||
// Makes silhouettes thicker.
|
||||
for (int i = 1; i <= INFLATE; ++i)
|
||||
{
|
||||
v = max(v, DetectSilho(coord + ivec2(i, 0), s));
|
||||
v = max(v, DetectSilho(coord + ivec2(0, i), s));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Average the per-sample coverage into the sub-pixel anti-aliasing of the line.
|
||||
float DetectSilho(vec2 fragCoord)
|
||||
{
|
||||
ivec2 coord = ivec2(fragCoord);
|
||||
#ifdef GL_ARB_texture_multisample
|
||||
int n = max(msaa_samples, 1);
|
||||
#else
|
||||
const int n = 1;
|
||||
#endif
|
||||
float acc = 0.0;
|
||||
for (int s = 0; s < n; ++s)
|
||||
acc += DetectSilhoSample(coord, s);
|
||||
return acc / float(n);
|
||||
}
|
||||
|
||||
float compute_ssao_factor(vec3 normal, vec3 view_dir, vec3 eye_pos)
|
||||
{
|
||||
vec3 normal_dx = dFdx(normal);
|
||||
@@ -270,13 +323,7 @@ void main()
|
||||
if (is_outline) {
|
||||
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade;
|
||||
vec4 shaded_color = vec4(clamp(shaded_rgb, vec3(0.0), vec3(1.0)), color.a);
|
||||
vec2 fragCoord = gl_FragCoord.xy;
|
||||
float s = DetectSilho(fragCoord);
|
||||
for(int i=1;i<=INFLATE; i++)
|
||||
{
|
||||
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
|
||||
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
|
||||
}
|
||||
float s = DetectSilho(gl_FragCoord.xy);
|
||||
if (s < 0.01)
|
||||
discard;
|
||||
out_color = vec4(mix(shaded_color.rgb, getBackfaceColor(shaded_color.rgb), s), shaded_color.a);
|
||||
|
||||
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
|
||||
}
|
||||
|
||||
function IsPluginChecked(plugin) {
|
||||
return GetStatus(plugin) === "Activated";
|
||||
return plugin.is_loaded;
|
||||
}
|
||||
|
||||
function HasMixedCapabilityState(plugin) {
|
||||
@@ -1347,6 +1347,8 @@ function StatusDescription(plugin) {
|
||||
return "This plugin is still loading.";
|
||||
case "Error":
|
||||
return "This plugin is blocked until its error is fixed.";
|
||||
case "RuntimeError":
|
||||
return "This plugin is loaded but a capability reported an error.";
|
||||
case "Inactive":
|
||||
default:
|
||||
return "This plugin is inactive. Activate it to install or load it.";
|
||||
|
||||
@@ -424,6 +424,11 @@ body.pane-resizing {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-runtimeerror {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-loading {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
@@ -680,6 +685,11 @@ body.pane-resizing {
|
||||
color: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-runtimeerror {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-loading {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
|
||||
@@ -626,6 +626,12 @@ void AppConfig::set_defaults()
|
||||
set_bool("window_buttons_on_left", false);
|
||||
#endif
|
||||
|
||||
if (get("use_printer_agents").empty())
|
||||
{
|
||||
// false = legacy behavior using print hosts
|
||||
set_bool("use_printer_agents", false);
|
||||
}
|
||||
|
||||
// Remove legacy window positions/sizes
|
||||
erase("app", "main_frame_maximized");
|
||||
erase("app", "main_frame_pos");
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "BeltGCode.hpp"
|
||||
#include "BeltGCodeWriter.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Print.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers)
|
||||
{
|
||||
if (!print.config().belt_printer.value)
|
||||
return;
|
||||
|
||||
auto belt_writer = std::make_unique<BeltGCodeWriter>();
|
||||
belt_writer->set_is_bbl_machine(is_bbl_printers);
|
||||
// Axis remap and build volume max are set by base GCode after init_belt_writer returns.
|
||||
belt_writer->set_belt_back_transform(print.config());
|
||||
belt_writer->set_machine_frame_transform(print.config());
|
||||
m_writer = std::move(belt_writer);
|
||||
}
|
||||
|
||||
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
|
||||
{
|
||||
if (!print.config().belt_printer.value)
|
||||
return;
|
||||
|
||||
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);
|
||||
file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0);
|
||||
// Pre-slice remap configs
|
||||
file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str());
|
||||
file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str());
|
||||
file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str());
|
||||
file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0);
|
||||
file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0);
|
||||
// Machine-frame transform: shear (tan) + scale (1/cos) 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*/)
|
||||
{
|
||||
// Global pre-slice mode: adjust origin using computed correction.
|
||||
// Transform the origin through the belt pipeline so that
|
||||
// back_transform(T * origin) = origin (correct machine position).
|
||||
//
|
||||
// Flags that trigger this path:
|
||||
// belt_preslice_global — full pipeline (rotation * remap) is global
|
||||
// preslice_remap_global — only the pre-slice remap is global
|
||||
// belt_slice_rotation_global — slicing rotation treated as global (matches
|
||||
// the per-instance Z-offset added in PrintObjectSlice.cpp)
|
||||
// The XY origin adjustment uses the FULL forward transform, because the
|
||||
// back_transform applied during G-code emission is always the inverse of
|
||||
// the full pipeline.
|
||||
bool use_global = m_config.belt_preslice_global.value
|
||||
|| (m_config.preslice_remap_global.value
|
||||
&& BeltTransformPipeline::has_preslice_remap(m_config))
|
||||
|| (m_config.belt_slice_rotation_global.value
|
||||
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
|
||||
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON);
|
||||
if (!use_global || !m_config.belt_printer.value)
|
||||
return;
|
||||
|
||||
// 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
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GCode.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt-printer-specific GCode export.
|
||||
//
|
||||
// Inherits from GCode and overrides virtual hooks to:
|
||||
// - Create a BeltGCodeWriter instead of a plain GCodeWriter
|
||||
// - Write belt configuration to the G-code header
|
||||
// - Adjust the origin for global pre-slice transforms when switching instances
|
||||
// - Disable arc fitting (G2/G3 not supported on belt printers)
|
||||
class BeltGCode : public GCode
|
||||
{
|
||||
protected:
|
||||
void init_belt_writer(Print &print, bool is_bbl_printers) override;
|
||||
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
|
||||
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
|
||||
bool should_disable_arc_fitting() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,275 +0,0 @@
|
||||
#include "BeltGCodeWriter.hpp"
|
||||
#include "FirstLayerPlane.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
// Decide whether a particular destination point gets first-layer treatment.
|
||||
// When the plane evaluator is active, distance from the plane wins; otherwise
|
||||
// fall back to the layer-coarse m_is_first_layer flag set by the caller.
|
||||
inline bool belt_point_on_first_layer(
|
||||
const FirstLayerPlane *plane,
|
||||
double first_layer_thickness_mm,
|
||||
bool layer_first_flag,
|
||||
const Vec3d &point_slicing_mm)
|
||||
{
|
||||
if (plane && plane->is_active())
|
||||
return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm);
|
||||
return layer_first_flag;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Belt configuration ---------------------------------------------------
|
||||
|
||||
void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config)
|
||||
{
|
||||
m_belt_back_transform.init_from_config(config);
|
||||
}
|
||||
|
||||
void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config)
|
||||
{
|
||||
m_machine_frame_transform.init_from_config(config);
|
||||
}
|
||||
|
||||
Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const
|
||||
{
|
||||
// Step 1+2: To Cartesian (back_transform + axis_remap).
|
||||
// In world-coordinates mode (PA line / PA pattern calibration) the input
|
||||
// already describes a point relative to the belt surface, so the
|
||||
// slicer->world back-transform is skipped and only the machine kinematics
|
||||
// (axis remap + frame shear/scale) are applied.
|
||||
Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos);
|
||||
Vec3d result = apply_axis_remap(after_back);
|
||||
Vec3d after_remap = result;
|
||||
// Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts
|
||||
// as a global linear transform on the placed coords.
|
||||
Vec3d final = m_machine_frame_transform.apply(result);
|
||||
|
||||
// [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z
|
||||
// crosses an integer mm boundary) to keep the log volume manageable while
|
||||
// still capturing one sample per ~5 layers. Shows the full pipeline so
|
||||
// Case A vs Case B can be compared step-by-step.
|
||||
static thread_local int s_last_logged_z = std::numeric_limits<int>::min();
|
||||
int z_bucket = static_cast<int>(std::floor(pos.z() * 5.0)); // every 0.2mm
|
||||
if (z_bucket != s_last_logged_z) {
|
||||
s_last_logged_z = z_bucket;
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords"
|
||||
<< " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")"
|
||||
<< " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")"
|
||||
<< " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")"
|
||||
<< " final=(" << final.x() << "," << final.y() << "," << final.z() << ")"
|
||||
<< " mft_active=" << m_machine_frame_transform.is_active()
|
||||
<< " back_active=" << m_belt_back_transform.is_active();
|
||||
}
|
||||
return final;
|
||||
}
|
||||
|
||||
// ---- Overridden movement methods ------------------------------------------
|
||||
|
||||
std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment)
|
||||
{
|
||||
m_pos(0) = point(0);
|
||||
m_pos(1) = point(1);
|
||||
|
||||
this->set_current_position_clear(true);
|
||||
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
|
||||
|
||||
// Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation)
|
||||
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(machine);
|
||||
const bool first_layer_for_point = belt_point_on_first_layer(
|
||||
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
|
||||
Vec3d(point.x(), point.y(), m_pos.z()));
|
||||
auto speed = first_layer_for_point
|
||||
? 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);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
return w.string();
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
|
||||
{
|
||||
// Belt printer: force NormalLift since SpiralLift and SlopeLift compute
|
||||
// slope angles that don't account for the YZ coordinate rotation.
|
||||
return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase);
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::eager_lift(const LiftType type)
|
||||
{
|
||||
// Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation).
|
||||
return GCodeWriter::eager_lift(LiftType::NormalLift);
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment)
|
||||
{
|
||||
m_pos(2) = z;
|
||||
|
||||
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
|
||||
if (speed == 0.) {
|
||||
const bool first_layer_for_point = belt_point_on_first_layer(
|
||||
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
|
||||
Vec3d(m_pos.x(), m_pos.y(), z));
|
||||
speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
|
||||
: this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
}
|
||||
|
||||
// Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords.
|
||||
Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(machine);
|
||||
w.emit_f(speed * 60.0);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
return w.string();
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion)
|
||||
{
|
||||
m_pos(0) = point(0);
|
||||
m_pos(1) = point(1);
|
||||
if (std::abs(dE) <= std::numeric_limits<double>::epsilon())
|
||||
force_no_extrusion = true;
|
||||
|
||||
if (!force_no_extrusion)
|
||||
filament()->extrude(dE);
|
||||
|
||||
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
|
||||
|
||||
// Belt printer: transform and emit XYZ (Y and Z are coupled)
|
||||
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(machine);
|
||||
if (!force_no_extrusion)
|
||||
w.emit_e(filament()->E());
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
return w.string();
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion)
|
||||
{
|
||||
m_pos = point;
|
||||
m_lifted = 0;
|
||||
if (!force_no_extrusion)
|
||||
filament()->extrude(dE);
|
||||
|
||||
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
|
||||
point_on_plate = to_machine_coords(point_on_plate);
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(point_on_plate);
|
||||
if (!force_no_extrusion)
|
||||
w.emit_e(filament()->E());
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
return w.string();
|
||||
}
|
||||
|
||||
std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z)
|
||||
{
|
||||
// Belt-specific override of travel_to_xyz.
|
||||
// Key differences from base:
|
||||
// 1. All coordinates go through to_machine_coords()
|
||||
// 2. Always emit full XYZ (can't split XY and Z due to coupling)
|
||||
// 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides)
|
||||
|
||||
Vec3d dest_point = point;
|
||||
const bool first_layer_for_point = belt_point_on_first_layer(
|
||||
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point);
|
||||
auto travel_speed =
|
||||
first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
|
||||
: this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
|
||||
// Handle pending z_hop
|
||||
if (std::abs(m_to_lift) > EPSILON) {
|
||||
assert(std::abs(m_lifted) < EPSILON);
|
||||
if ((!this->is_current_position_clear() || m_pos != dest_point) &&
|
||||
m_to_lift + m_pos(2) > point(2)) {
|
||||
m_lifted = m_to_lift + m_pos(2) - point(2);
|
||||
dest_point(2) = m_to_lift + m_pos(2);
|
||||
}
|
||||
m_to_lift = 0.;
|
||||
|
||||
std::string slop_move;
|
||||
Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) };
|
||||
Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
|
||||
Vec3d delta = target - source;
|
||||
Vec2d delta_no_z = { delta(0), delta(1) };
|
||||
|
||||
if (delta(2) > 0 && delta_no_z.norm() != 0.0f) {
|
||||
// Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift),
|
||||
// but handle NormalLift and fallthrough.
|
||||
if (m_to_lift_type == LiftType::SlopeLift &&
|
||||
this->is_current_position_clear() &&
|
||||
atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) {
|
||||
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;
|
||||
slope_top_point = to_machine_coords(slope_top_point);
|
||||
GCodeG1Formatter w0;
|
||||
w0.emit_xyz(slope_top_point);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
slop_move = w0.string();
|
||||
}
|
||||
else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) {
|
||||
// Only lift-in-place when the current position is known. On a normal
|
||||
// printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled
|
||||
// to Y/X, so _travel_to_z re-emits the current m_pos through the belt
|
||||
// shear. At print start (and after custom gcode) m_pos.xy is still the
|
||||
// uninitialised origin (0,0), which shears into a bogus machine point
|
||||
// (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate
|
||||
// lift here is safe: there is nothing to lift over yet, and the
|
||||
// xy_z_move below travels straight to the destination with full XYZ,
|
||||
// establishing the correct position. This mirrors the SlopeLift branch
|
||||
// above, which already guards on is_current_position_clear().
|
||||
slop_move = _travel_to_z(target.z(), "normal lift Z");
|
||||
}
|
||||
}
|
||||
|
||||
std::string xy_z_move;
|
||||
{
|
||||
Vec3d emit_target = to_machine_coords(target);
|
||||
GCodeG1Formatter w0;
|
||||
// Belt mode: always emit full XYZ since Y and Z are coupled
|
||||
w0.emit_xyz(emit_target);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
xy_z_move = w0.string();
|
||||
}
|
||||
m_pos = dest_point;
|
||||
this->set_current_position_clear(true);
|
||||
return slop_move + xy_z_move;
|
||||
}
|
||||
else if (!force_z && !this->will_move_z(point(2))) {
|
||||
double nominal_z = m_pos(2) - m_lifted;
|
||||
m_lifted -= (point(2) - nominal_z);
|
||||
if (std::abs(m_lifted) < EPSILON)
|
||||
m_lifted = 0.;
|
||||
this->set_current_position_clear(true);
|
||||
return this->travel_to_xy(to_2d(point));
|
||||
}
|
||||
else {
|
||||
m_lifted = 0;
|
||||
}
|
||||
|
||||
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
|
||||
point_on_plate = to_machine_coords(point_on_plate);
|
||||
|
||||
// Belt mode: always emit full XYZ
|
||||
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_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
|
||||
m_pos = dest_point;
|
||||
this->set_current_position_clear(true);
|
||||
return w.string();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltBackTransform.hpp"
|
||||
#include "GCode/MachineFrameTransform.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class FirstLayerPlane;
|
||||
|
||||
// Belt-printer-specific GCode writer.
|
||||
//
|
||||
// Inherits from GCodeWriter and overrides movement methods to apply
|
||||
// coordinate transformation (back-transform, axis remap, machine-frame
|
||||
// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt).
|
||||
class BeltGCodeWriter : public GCodeWriter
|
||||
{
|
||||
public:
|
||||
BeltGCodeWriter() : GCodeWriter() {}
|
||||
|
||||
// Belt configuration (axis remap is inherited from GCodeWriter)
|
||||
void set_belt_back_transform(const PrintConfig &config);
|
||||
void set_machine_frame_transform(const PrintConfig &config);
|
||||
Vec3d to_machine_coords(const Vec3d &pos) const;
|
||||
|
||||
// World-coordinates mode: incoming coordinates are treated as points
|
||||
// relative to the physical belt surface (X across, Y along the belt,
|
||||
// Z height above it) instead of slicing-frame coordinates — the
|
||||
// slicer->world back-transform is skipped. Used by the PA line / PA
|
||||
// pattern calibration generators, whose logical bed coordinates describe
|
||||
// first-layer drawings on the build surface.
|
||||
void set_world_coordinates(bool enable) { m_world_coordinates = enable; }
|
||||
|
||||
// First-layer plane: when set to a non-null active evaluator, travel
|
||||
// speed selection consults the plane per-move and uses
|
||||
// initial_layer_travel_speed for points within first_layer_height_mm
|
||||
// of the plane (regardless of slicing layer index).
|
||||
void set_first_layer_plane(const FirstLayerPlane *plane,
|
||||
double first_layer_height_mm) {
|
||||
m_first_layer_plane = plane;
|
||||
m_first_layer_thickness_mm = first_layer_height_mm;
|
||||
}
|
||||
|
||||
// Overridden movement methods
|
||||
std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override;
|
||||
std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override;
|
||||
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
|
||||
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
|
||||
std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override;
|
||||
std::string eager_lift(const LiftType type) override;
|
||||
|
||||
protected:
|
||||
std::string _travel_to_z(double z, const std::string &comment) override;
|
||||
|
||||
private:
|
||||
BeltBackTransform m_belt_back_transform;
|
||||
MachineFrameTransform m_machine_frame_transform;
|
||||
bool m_world_coordinates = false;
|
||||
// Borrowed pointer; lifetime owned by GCode. null = inactive.
|
||||
const FirstLayerPlane *m_first_layer_plane = nullptr;
|
||||
double m_first_layer_thickness_mm = 0.;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,143 +0,0 @@
|
||||
#include "BeltSliceStrategy.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z)
|
||||
{
|
||||
// 1. Standalone pre-slice axis remap (works without belt mode).
|
||||
const bool has_remap = BeltTransformPipeline::has_preslice_remap(config);
|
||||
if (has_remap)
|
||||
trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo;
|
||||
|
||||
// 2. 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_remap && !has_rotation)
|
||||
return;
|
||||
|
||||
// 3. 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.
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
// Capture the incoming trafo for diagnostic logging.
|
||||
// This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift.
|
||||
const Transform3d trafo_pre_shift = trafo;
|
||||
auto log_mat = [](const Matrix3d &m) {
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(4);
|
||||
ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "],"
|
||||
<< "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "],"
|
||||
<< "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]";
|
||||
return ss.str();
|
||||
};
|
||||
auto log_vec3 = [](const Vec3d &v) {
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(4);
|
||||
ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")";
|
||||
return ss.str();
|
||||
};
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter"
|
||||
<< " has_rotation=" << has_rotation
|
||||
<< " has_remap=" << has_remap
|
||||
<< " trafo.linear=" << log_mat(trafo_pre_shift.linear())
|
||||
<< " trafo.translation=" << log_vec3(trafo_pre_shift.translation())
|
||||
<< " volumes=" << model_volumes.size();
|
||||
#endif
|
||||
|
||||
double min_z = std::numeric_limits<double>::max();
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
int vol_idx = 0;
|
||||
#endif
|
||||
for (const ModelVolume *mv : model_volumes) {
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
if (!mv->is_model_part()) { ++vol_idx; continue; }
|
||||
#else
|
||||
if (!mv->is_model_part()) continue;
|
||||
#endif
|
||||
Transform3d vol_trafo = trafo * mv->get_matrix();
|
||||
const auto &its = mv->mesh().its;
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
// Per-volume bbox in mesh-frame and post-trafo slicer-frame.
|
||||
Vec3d mesh_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
|
||||
Vec3d mesh_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
|
||||
Vec3d slicer_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
|
||||
Vec3d slicer_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
|
||||
double vol_min_z = std::numeric_limits<double>::max();
|
||||
#endif
|
||||
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());
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
mesh_min = mesh_min.cwiseMin(vm);
|
||||
mesh_max = mesh_max.cwiseMax(vm);
|
||||
slicer_min = slicer_min.cwiseMin(pt);
|
||||
slicer_max = slicer_max.cwiseMax(pt);
|
||||
vol_min_z = std::min(vol_min_z, pt.z());
|
||||
#endif
|
||||
}
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx
|
||||
<< "] id=" << mv->id().id << " name='" << mv->name << "'"
|
||||
<< " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max)
|
||||
<< " get_matrix.translation=" << log_vec3(mv->get_matrix().translation())
|
||||
<< " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max)
|
||||
<< " vol_min_z=" << vol_min_z;
|
||||
++vol_idx;
|
||||
#endif
|
||||
}
|
||||
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z
|
||||
<< " z_shift_val=" << z_shift_val;
|
||||
#endif
|
||||
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; the standalone-remap path
|
||||
// never reported it.
|
||||
if (out_belt_min_z && config.belt_printer.value) {
|
||||
const double new_val = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id()
|
||||
<< " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val;
|
||||
#endif
|
||||
*out_belt_min_z = new_val;
|
||||
}
|
||||
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit"
|
||||
<< " final_trafo.linear=" << log_mat(trafo.linear())
|
||||
<< " final_trafo.translation=" << log_vec3(trafo.translation());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,36 +0,0 @@
|
||||
#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 pre-slice mesh transforms applied before slicing:
|
||||
// 1. Pre-slice axis remap (standalone — works without belt mode)
|
||||
// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a
|
||||
// g-code-side stage, see MachineFrameTransform)
|
||||
// 3. Per-object Z-shift that lifts the mesh above the build plate
|
||||
//
|
||||
// Isolates this belt/remap-specific logic from the generic slicing pipeline in
|
||||
// PrintObjectSlice.cpp.
|
||||
class BeltSliceStrategy
|
||||
{
|
||||
public:
|
||||
// Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place.
|
||||
// No-op when neither a remap nor a belt rotation is configured.
|
||||
//
|
||||
// out_belt_min_z (if non-null) receives the minimum mesh Z after the
|
||||
// transforms, but only in belt-printer mode — the standalone-remap path
|
||||
// never reported it.
|
||||
static void apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z = nullptr);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,223 +0,0 @@
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---- Matrix builders ------------------------------------------------------
|
||||
|
||||
Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config)
|
||||
{
|
||||
Transform3d pre_remap = Transform3d::Identity();
|
||||
if (!has_preslice_remap(config))
|
||||
return pre_remap;
|
||||
|
||||
int pre_rx = int(config.preslice_remap_x.value);
|
||||
int pre_ry = int(config.preslice_remap_y.value);
|
||||
int pre_rz = int(config.preslice_remap_z.value);
|
||||
|
||||
// Each remap value selects a source axis and sign.
|
||||
auto remap_column = [](int r) -> Vec3d {
|
||||
int axis = r % 3;
|
||||
Vec3d col = Vec3d::Zero();
|
||||
if (r < 3) col[axis] = 1.0; // +axis
|
||||
else if (r < 6) col[axis] = -1.0; // -axis
|
||||
else col[axis] = -1.0; // Rev: max - pos = -(pos - max)
|
||||
return col;
|
||||
};
|
||||
|
||||
Matrix3d remap_lin;
|
||||
remap_lin.col(0) = remap_column(pre_rx);
|
||||
remap_lin.col(1) = remap_column(pre_ry);
|
||||
remap_lin.col(2) = remap_column(pre_rz);
|
||||
pre_remap.linear() = remap_lin;
|
||||
|
||||
// Translation for Rev modes (needs build volume extents).
|
||||
if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) {
|
||||
BoundingBoxf bbox_bed(config.printable_area.values);
|
||||
Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(),
|
||||
config.printable_height.value);
|
||||
Vec3d remap_trans = Vec3d::Zero();
|
||||
auto add_rev = [&](int r, int out) {
|
||||
if (r >= 6) remap_trans[out] = vol_max[r % 3];
|
||||
};
|
||||
add_rev(pre_rx, 0);
|
||||
add_rev(pre_ry, 1);
|
||||
add_rev(pre_rz, 2);
|
||||
pre_remap.translation() = remap_trans;
|
||||
}
|
||||
|
||||
return pre_remap;
|
||||
}
|
||||
|
||||
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: rotation applied after the pre-slice axis remap.
|
||||
// (Shear & scale are a g-code-side stage, not part of the mesh transform.)
|
||||
Transform3d pre_remap = build_preslice_remap(config);
|
||||
Matrix3d rot = build_rotation_matrix(config);
|
||||
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = rot;
|
||||
combined = combined * pre_remap;
|
||||
return combined;
|
||||
}
|
||||
|
||||
// ---- Bounding box remap ---------------------------------------------------
|
||||
|
||||
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config)
|
||||
{
|
||||
int pre_rx = int(config.preslice_remap_x.value);
|
||||
int pre_ry = int(config.preslice_remap_y.value);
|
||||
int pre_rz = int(config.preslice_remap_z.value);
|
||||
|
||||
if (pre_rx == int(RemapAxis::PosX) &&
|
||||
pre_ry == int(RemapAxis::PosY) &&
|
||||
pre_rz == int(RemapAxis::PosZ))
|
||||
return bb; // Identity remap.
|
||||
|
||||
auto remap_coord = [](int r, const Vec3d &v) -> double {
|
||||
int axis = r % 3;
|
||||
if (r < 3) return v[axis];
|
||||
return -v[axis];
|
||||
};
|
||||
|
||||
Vec3d mn = bb.min.cast<double>(), mx = bb.max.cast<double>();
|
||||
BoundingBoxf3 rbb;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Vec3d c((i & 1) ? mx.x() : mn.x(),
|
||||
(i & 2) ? mx.y() : mn.y(),
|
||||
(i & 4) ? mx.z() : mn.z());
|
||||
Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c));
|
||||
if (i == 0) rbb = BoundingBoxf3(rc, rc);
|
||||
else rbb.merge(rc);
|
||||
}
|
||||
return rbb;
|
||||
}
|
||||
|
||||
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config)
|
||||
{
|
||||
return remap_bbox(model_object.raw_bounding_box(), config);
|
||||
}
|
||||
|
||||
// ---- Belt floor parameters ------------------------------------------------
|
||||
|
||||
// Shared implementation for both PrintConfig and DynamicPrintConfig.
|
||||
// Template avoids duplicating the math for the two config types.
|
||||
namespace {
|
||||
|
||||
template<typename Config>
|
||||
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
|
||||
const Config &config, const BoundingBoxf3 &bb, double original_height)
|
||||
{
|
||||
BeltTransformPipeline::BeltHeightResult result;
|
||||
result.object_height = original_height;
|
||||
|
||||
// Extract the mesh rotation from config (the sole mesh-side belt transform).
|
||||
BeltRotationAxis rot_axis;
|
||||
double rot_angle;
|
||||
|
||||
if constexpr (std::is_same_v<Config, PrintConfig>) {
|
||||
rot_axis = config.belt_slice_rotation.value;
|
||||
rot_angle = config.belt_slice_rotation_angle.value;
|
||||
} else {
|
||||
// DynamicPrintConfig path
|
||||
auto get_float = [&](const char *key) {
|
||||
auto *opt = config.template option<ConfigOptionFloat>(key);
|
||||
return opt ? opt->value : 0.0;
|
||||
};
|
||||
auto get_rot_axis = [&](const char *key) {
|
||||
auto *opt = config.template option<ConfigOptionEnum<BeltRotationAxis>>(key);
|
||||
return opt ? opt->value : BeltRotationAxis::None;
|
||||
};
|
||||
rot_axis = get_rot_axis("belt_slice_rotation");
|
||||
rot_angle = get_float("belt_slice_rotation_angle");
|
||||
}
|
||||
|
||||
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();
|
||||
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());
|
||||
double z = (R * c).z();
|
||||
min_rz = std::min(min_rz, z);
|
||||
max_rz = std::max(max_rz, z);
|
||||
}
|
||||
result.object_height = max_rz - min_rz;
|
||||
|
||||
// Belt floor in slicer-frame is 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
|
||||
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X:
|
||||
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
|
||||
result.floor_params.from_axis = 1; // Y
|
||||
break;
|
||||
case BeltRotationAxis::Y:
|
||||
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
|
||||
result.floor_params.from_axis = 0; // X
|
||||
break;
|
||||
case BeltRotationAxis::Z:
|
||||
default:
|
||||
result.floor_params.shear_factor = 0.0;
|
||||
result.floor_params.from_axis = 1;
|
||||
break;
|
||||
}
|
||||
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 &remapped_bbox, double original_height)
|
||||
{
|
||||
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
|
||||
}
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
|
||||
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
|
||||
{
|
||||
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,152 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Geometry.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 * pre_remap * 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 --------------------------------------------------
|
||||
|
||||
static bool has_preslice_remap(const PrintConfig &config)
|
||||
{
|
||||
return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) ||
|
||||
int(config.preslice_remap_y.value) != int(RemapAxis::PosY) ||
|
||||
int(config.preslice_remap_z.value) != int(RemapAxis::PosZ);
|
||||
}
|
||||
|
||||
// Overload accepting DynamicPrintConfig (used in static slicing_parameters).
|
||||
static bool has_preslice_remap(const DynamicPrintConfig &config)
|
||||
{
|
||||
auto get_int = [&](const char *key) -> int {
|
||||
auto *opt = config.option<ConfigOptionEnum<RemapAxis>>(key);
|
||||
return opt ? int(opt->value) : 0;
|
||||
};
|
||||
return get_int("preslice_remap_x") != int(RemapAxis::PosX) ||
|
||||
get_int("preslice_remap_y") != int(RemapAxis::PosY) ||
|
||||
get_int("preslice_remap_z") != int(RemapAxis::PosZ);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static PhysicalTilt physical_tilt(const PrintConfig &config)
|
||||
{
|
||||
return physical_tilt(config.belt_slice_rotation.value,
|
||||
config.belt_slice_rotation_angle.value);
|
||||
}
|
||||
|
||||
// ---- Matrix builders --------------------------------------------------
|
||||
|
||||
// Build the pre-slice axis remap transform (includes Rev-mode translation).
|
||||
static Transform3d build_preslice_remap(const PrintConfig &config);
|
||||
|
||||
// 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);
|
||||
|
||||
// Combined forward transform (rotation * pre_remap) — 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);
|
||||
|
||||
// ---- Bounding box remap -----------------------------------------------
|
||||
|
||||
// Remap a bounding box through the pre-slice axis remap.
|
||||
// Returns the original bbox if remap is identity.
|
||||
static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config);
|
||||
static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config);
|
||||
|
||||
// ---- Belt floor parameters --------------------------------------------
|
||||
|
||||
struct BeltFloorParams {
|
||||
double shear_factor = 0.0;
|
||||
int from_axis = 1;
|
||||
double z_shift = 0.0;
|
||||
};
|
||||
|
||||
// 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 pre-remapped 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 &remapped_bbox,
|
||||
double original_height);
|
||||
|
||||
// Overload for DynamicPrintConfig (used by static slicing_parameters).
|
||||
static BeltHeightResult compute_belt_height_and_floor(
|
||||
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox,
|
||||
double original_height);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
min(p1), max(p1), defined(false) { merge(p2); merge(p3); }
|
||||
|
||||
template<class It, class = IteratorOnly<It>>
|
||||
BoundingBoxBase(It from, It to)
|
||||
BoundingBoxBase(It from, It to) : BoundingBoxBase()
|
||||
{ construct(*this, from, to); }
|
||||
|
||||
BoundingBoxBase(const PointsType &points)
|
||||
|
||||
@@ -349,7 +349,7 @@ static ExPolygons make_brim_ears_auto(const ExPolygons& obj_expoly, coord_t size
|
||||
return mouse_ears_ex;
|
||||
}
|
||||
|
||||
static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWidth, float brim_offset, Flow &flow, bool is_outer_brim)
|
||||
static ExPolygons make_brim_ears(const PrintObject* object)
|
||||
{
|
||||
ExPolygons mouse_ears_ex;
|
||||
BrimPoints brim_ear_points = object->model_object()->brim_points;
|
||||
@@ -373,12 +373,7 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
|
||||
Vec3f world_pos = pt.transform(trsf.get_matrix());
|
||||
if ( world_pos.z() > 0) continue;
|
||||
Polygon point_round;
|
||||
float brim_width = floor(scale_(pt.head_front_radius) / flowWidth / 2) * flowWidth * 2;
|
||||
if (is_outer_brim) {
|
||||
double flowWidthScale = flowWidth / SCALING_FACTOR;
|
||||
brim_width = floor(brim_width / flowWidthScale / 2) * flowWidthScale * 2;
|
||||
}
|
||||
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
|
||||
const coord_t size_ear = scale_(pt.head_front_radius);
|
||||
for (size_t i = 0; i < POLY_SIDE_COUNT; i++) {
|
||||
double angle = (2.0 * PI * i) / POLY_SIDE_COUNT;
|
||||
point_round.points.emplace_back(size_ear * cos(angle), size_ear * sin(angle));
|
||||
@@ -452,7 +447,8 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
bool has_brim_auto = object->config().brim_type == btAutoBrim;
|
||||
const bool use_auto_brim_ears = object->config().brim_type == btEar;
|
||||
const bool use_brim_ears = object->config().brim_type == btPainted;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears;
|
||||
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;
|
||||
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;
|
||||
@@ -531,7 +527,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION);
|
||||
ExPolygons outerExpoly;
|
||||
if (use_brim_ears) {
|
||||
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, true);
|
||||
outerExpoly = make_brim_ears(object);
|
||||
//outerExpoly = offset_ex(outerExpoly, brim_width_mod, jtRound, SCALED_RESOLUTION);
|
||||
} else if (use_auto_brim_ears) {
|
||||
coord_t size_ear = (brim_width_mod - brim_offset - flow.scaled_spacing());
|
||||
@@ -545,7 +541,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
ExPolygons outerExpoly;
|
||||
auto innerExpoly = offset_ex(ex_poly_holes_reversed, -brim_width - brim_offset);
|
||||
if (use_brim_ears) {
|
||||
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, false);
|
||||
outerExpoly = make_brim_ears(object);
|
||||
} else if (use_auto_brim_ears) {
|
||||
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
|
||||
outerExpoly = make_brim_ears_auto(offset_ex(ex_poly_holes_reversed, -brim_offset), size_ear, ear_detection_length, brim_ears_max_angle, false);
|
||||
@@ -868,10 +864,6 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
|
||||
std::vector<unsigned int>& printExtruders,
|
||||
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
|
||||
{
|
||||
// Belt printer: brim is not compatible with belt printing.
|
||||
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,
|
||||
|
||||
@@ -176,31 +176,6 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name();
|
||||
}
|
||||
|
||||
void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y)
|
||||
{
|
||||
m_is_belt_printer = enabled;
|
||||
m_belt_angle = angle_deg;
|
||||
m_belt_infinite_y = infinite_y;
|
||||
|
||||
// Restart from the unmodified bbox each call. Without this, toggling
|
||||
// belt mode off (or switching infinite_y true→false) would leave the
|
||||
// extents inflated and break collision / object_state checks.
|
||||
BoundingBoxf bboxf = get_extents(m_bed_shape);
|
||||
m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) };
|
||||
|
||||
if (enabled) {
|
||||
if (infinite_y) {
|
||||
// Extend the Y bound to a very large value for infinite belt.
|
||||
m_bboxf.max.y() = 100000.;
|
||||
}
|
||||
// Belt printer: the Z extent already equals printable_height (set above), which
|
||||
// is the usable vertical clearance above the belt. The gantry's axis range is
|
||||
// sized to reach height/cos(tilt), so no diagonal scaling is applied here — this
|
||||
// keeps the live "outside build volume" highlight in agreement with Print::validate().
|
||||
(void) angle_deg;
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Tests intersections of projected triangles, not just their vertices against a bounding box.
|
||||
// This test also correctly evaluates collision of a non-convex object with the bounding box.
|
||||
@@ -409,11 +384,6 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i
|
||||
build_volume.max.z() = std::numeric_limits<double>::max();
|
||||
if (ignore_bottom)
|
||||
build_volume.min.z() = -std::numeric_limits<double>::max();
|
||||
// Belt printer: extend Y bounds for infinite Y.
|
||||
if (m_is_belt_printer && m_belt_infinite_y) {
|
||||
build_volume.min.y() = -std::numeric_limits<double>::max();
|
||||
build_volume.max.y() = std::numeric_limits<double>::max();
|
||||
}
|
||||
BoundingBox3Base<Vec3f> build_volumef(build_volume.min.cast<float>(), build_volume.max.cast<float>());
|
||||
// The following test correctly interprets intersection of a non-convex object with a rectangular build volume.
|
||||
//return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z());
|
||||
|
||||
@@ -57,10 +57,6 @@ public:
|
||||
// Initialize from PrintConfig::printable_area and PrintConfig::printable_height
|
||||
BuildVolume(const std::vector<Vec2d> &printable_area, const double printable_height, const std::vector<std::vector<Vec2d>> &extruder_areas, const std::vector<double>& extruder_printable_heights);
|
||||
|
||||
// Belt printer configuration.
|
||||
void set_belt_printer(bool enabled, double angle_deg, bool infinite_y);
|
||||
bool is_belt_printer() const { return m_is_belt_printer; }
|
||||
|
||||
// Source data, unscaled coordinates.
|
||||
const std::vector<Vec2d>& printable_area() const { return m_bed_shape; }
|
||||
double printable_height() const { return m_max_print_height; }
|
||||
@@ -84,7 +80,7 @@ public:
|
||||
indexed_triangle_set bounding_mesh(bool scale=true) const;
|
||||
|
||||
// Center of the print bed, unscaled.
|
||||
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
|
||||
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
|
||||
// Convex hull of polygon(), scaled.
|
||||
const Polygon& convex_hull() const { return m_convex_hull; }
|
||||
// Smallest enclosing circle of polygon(), scaled.
|
||||
@@ -143,10 +139,6 @@ private:
|
||||
// Source definition of the print volume height (PrintConfig::printable_height)
|
||||
double m_max_print_height { 0.f };
|
||||
std::vector<double> m_extruder_printable_height;
|
||||
// Belt printer state.
|
||||
bool m_is_belt_printer { false };
|
||||
double m_belt_angle { 0. };
|
||||
bool m_belt_infinite_y { false };
|
||||
|
||||
// Derived values.
|
||||
BuildVolume_Type m_type { BuildVolume_Type::Invalid };
|
||||
|
||||
@@ -80,16 +80,6 @@ set(lisbslic3r_sources
|
||||
BoundingBox.hpp
|
||||
BridgeDetector.cpp
|
||||
BridgeDetector.hpp
|
||||
BeltGCode.cpp
|
||||
BeltGCode.hpp
|
||||
BeltGCodeWriter.cpp
|
||||
BeltGCodeWriter.hpp
|
||||
BeltSliceStrategy.cpp
|
||||
BeltSliceStrategy.hpp
|
||||
BeltTransform.cpp
|
||||
BeltTransform.hpp
|
||||
FirstLayerPlane.cpp
|
||||
FirstLayerPlane.hpp
|
||||
Brim.cpp
|
||||
BrimEarsPoint.hpp
|
||||
Brim.hpp
|
||||
@@ -220,10 +210,6 @@ 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/ConflictChecker.cpp
|
||||
GCode/ConflictChecker.hpp
|
||||
GCode/CoolingBuffer.cpp
|
||||
@@ -432,8 +418,6 @@ set(lisbslic3r_sources
|
||||
SlicingAdaptive.hpp
|
||||
Slicing.cpp
|
||||
Slicing.hpp
|
||||
Support/BeltFloorContext.cpp
|
||||
Support/BeltFloorContext.hpp
|
||||
Support/SupportCommon.cpp
|
||||
Support/SupportCommon.hpp
|
||||
Support/SupportLayer.hpp
|
||||
|
||||
@@ -2273,6 +2273,8 @@ public:
|
||||
plugin_picker,
|
||||
// Raw JSON string value, edited through a dialog behind a button rather than in the row.
|
||||
plugin_config,
|
||||
// PrinterAgentChoice
|
||||
printer_agent_select,
|
||||
};
|
||||
|
||||
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.
|
||||
|
||||
@@ -396,11 +396,6 @@ 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);
|
||||
|
||||
@@ -278,6 +278,9 @@ struct SurfaceFillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" wave.
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
|
||||
bool separated_infills{false};
|
||||
|
||||
@@ -316,6 +319,7 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
|
||||
@@ -348,6 +352,7 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
};
|
||||
@@ -964,6 +969,11 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value);
|
||||
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
|
||||
|
||||
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill.
|
||||
// FillHilbertCurve::generate clamps and validates the value itself.
|
||||
if (params.pattern == ipHilbertCurve)
|
||||
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
|
||||
} else {
|
||||
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
|
||||
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
|
||||
@@ -1328,6 +1338,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
params.flow = surface_fill.params.flow;
|
||||
@@ -1569,6 +1580,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||
|
||||
@@ -1857,12 +1857,12 @@ static inline void base_support_extend_infill_lines(Polylines &infill, BoundaryI
|
||||
const bool first = graph.first(cp);
|
||||
int extend_next_idx = -1;
|
||||
int extend_prev_idx = -1;
|
||||
coord_t dist_y_prev;
|
||||
coord_t dist_y_next;
|
||||
double arc_len_prev;
|
||||
double arc_len_next;
|
||||
coord_t dist_y_prev = 0;
|
||||
coord_t dist_y_next = 0;
|
||||
double arc_len_prev = 0;
|
||||
double arc_len_next = 0;
|
||||
|
||||
if (! graph.next_vertical(cp)){
|
||||
if (! graph.next_vertical(cp)) {
|
||||
size_t i = cp.point_idx;
|
||||
size_t j = next_idx_modulo(i, contour);
|
||||
while (j != cp.next_on_contour->point_idx) {
|
||||
|
||||
@@ -82,6 +82,9 @@ struct FillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" variant.
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
// For Lateral lattice
|
||||
coordf_t lateral_lattice_angle_1 { 0.f };
|
||||
coordf_t lateral_lattice_angle_2 { 0.f };
|
||||
|
||||
@@ -114,12 +114,12 @@ void FillPlanePath::_fill_surface_single(
|
||||
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
|
||||
snug_bounding_box.translate(-shift.x(), -shift.y());
|
||||
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||
polyline.points = std::move(output.result());
|
||||
} else {
|
||||
// Filling in a snug bounding box, no need to clip.
|
||||
InfillPolylineOutput output(distance_between_lines);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||
polyline.points = std::move(output.result());
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,147 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
|
||||
}
|
||||
}
|
||||
|
||||
using QuinticBezier = std::array<Vec2d, 6>;
|
||||
|
||||
static bool is_bezier_flat(const QuinticBezier &curve, const double deviation)
|
||||
{
|
||||
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
|
||||
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
|
||||
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
|
||||
// comparing squared values avoids a square root.
|
||||
const Vec2d chord = curve.back() - curve.front();
|
||||
const double chord_length_sq = chord.squaredNorm();
|
||||
const double max_cross_sq = deviation * deviation * chord_length_sq;
|
||||
|
||||
for (size_t i = 1; i + 1 < curve.size(); ++i) {
|
||||
const Vec2d offset = curve[i] - curve.front();
|
||||
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
|
||||
if (cross * cross > max_cross_sq)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
|
||||
{
|
||||
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
|
||||
// control point to the left half and one to the right half; the latter is filled backwards to keep
|
||||
// both resulting control polygons in their original parameter direction.
|
||||
QuinticBezier subdivision = curve;
|
||||
left.front() = subdivision.front();
|
||||
right.back() = subdivision.back();
|
||||
for (size_t level = 1; level < curve.size(); ++level) {
|
||||
for (size_t i = 0; i + level < curve.size(); ++i)
|
||||
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
|
||||
left[level] = subdivision.front();
|
||||
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
|
||||
}
|
||||
}
|
||||
|
||||
static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output)
|
||||
{
|
||||
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
|
||||
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
|
||||
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
|
||||
static constexpr size_t max_depth = 16;
|
||||
|
||||
std::vector<QuinticBezier> subcurves(2);
|
||||
subdivide_bezier(curve, subcurves[0], subcurves[1]);
|
||||
|
||||
for (size_t depth = 1; depth < max_depth; ++depth) {
|
||||
bool all_flat = true;
|
||||
for (const QuinticBezier &c : subcurves)
|
||||
if (!is_bezier_flat(c, deviation)) {
|
||||
all_flat = false;
|
||||
break;
|
||||
}
|
||||
if (all_flat)
|
||||
break;
|
||||
std::vector<QuinticBezier> finer(subcurves.size() * 2);
|
||||
for (size_t i = 0; i < subcurves.size(); ++i)
|
||||
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
|
||||
subcurves = std::move(finer);
|
||||
}
|
||||
|
||||
// The curve start is deliberately omitted so consecutive curve pieces can share it without duplication.
|
||||
output.reserve(output.size() + subcurves.size());
|
||||
for (const QuinticBezier &c : subcurves)
|
||||
output.emplace_back(c.back());
|
||||
}
|
||||
|
||||
template<typename Output>
|
||||
static void generate_smooth_hilbert_curve(
|
||||
coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const double corner_distance, Output &output)
|
||||
{
|
||||
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed
|
||||
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output
|
||||
// clipper or the later region intersection removes the padded part of the traversal.
|
||||
size_t sz = 2;
|
||||
const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y);
|
||||
while (sz < sz0)
|
||||
sz <<= 1;
|
||||
|
||||
const size_t point_count = sz * sz;
|
||||
output.reserve(point_count);
|
||||
|
||||
// The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance
|
||||
// if this helper is invoked with an invalid resolution.
|
||||
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON;
|
||||
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance).
|
||||
// At each end, the first three control points are collinear and equally spaced: the tangent follows
|
||||
// the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore
|
||||
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten
|
||||
// it only once to the requested chordal-deviation tolerance.
|
||||
const QuinticBezier corner_curve {{
|
||||
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.},
|
||||
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance}
|
||||
}};
|
||||
std::vector<Vec2d> curve_coefficients;
|
||||
flatten_bezier(corner_curve, deviation, curve_coefficients);
|
||||
|
||||
auto translated_point = [min_x, min_y](size_t idx) {
|
||||
Point p = hilbert_n_to_xy(idx);
|
||||
return Point(p.x() + min_x, p.y() + min_y);
|
||||
};
|
||||
auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); };
|
||||
bool has_last_output = false;
|
||||
Vec2d last_output;
|
||||
// Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates
|
||||
// to avoid emitting zero-length extrusion segments.
|
||||
auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) {
|
||||
if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) {
|
||||
output.add_point(point);
|
||||
last_output = point;
|
||||
has_last_output = true;
|
||||
}
|
||||
};
|
||||
|
||||
Vec2d previous = to_vec2d(translated_point(0));
|
||||
Vec2d corner = to_vec2d(translated_point(1));
|
||||
add_point(previous);
|
||||
// Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of
|
||||
// its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline.
|
||||
for (size_t i = 1; i + 1 < point_count; ++i) {
|
||||
const Vec2d next = to_vec2d(translated_point(i + 1));
|
||||
const Vec2d incoming = (corner - previous).normalized();
|
||||
const Vec2d outgoing = (next - corner).normalized();
|
||||
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
|
||||
|
||||
if (std::abs(cross) < EPSILON) {
|
||||
add_point(corner);
|
||||
} else {
|
||||
add_point(corner - corner_distance * incoming);
|
||||
for (const Vec2d &coefficient : curve_coefficients)
|
||||
add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
|
||||
}
|
||||
|
||||
previous = corner;
|
||||
corner = next;
|
||||
}
|
||||
add_point(corner);
|
||||
}
|
||||
|
||||
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
|
||||
{
|
||||
if (output.clips())
|
||||
@@ -296,6 +437,24 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
|
||||
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
|
||||
}
|
||||
|
||||
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output)
|
||||
{
|
||||
const double smooth_factor = std::isfinite(params.smooth_factor) ?
|
||||
std::clamp(params.smooth_factor, 0., 1.) : 0.;
|
||||
if (smooth_factor == 0.) {
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
return;
|
||||
}
|
||||
|
||||
const double corner_distance = 0.5 * smooth_factor;
|
||||
if (output.clips())
|
||||
generate_smooth_hilbert_curve(
|
||||
min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output));
|
||||
else
|
||||
generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output);
|
||||
}
|
||||
|
||||
template<typename Output>
|
||||
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
|
||||
{
|
||||
|
||||
@@ -53,6 +53,11 @@ protected:
|
||||
friend class InfillPolylineClipper;
|
||||
|
||||
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
|
||||
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams & /* params */, InfillPolylineOutput &output)
|
||||
{
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
}
|
||||
};
|
||||
|
||||
class FillArchimedeanChords : public FillPlanePath
|
||||
@@ -75,6 +80,8 @@ public:
|
||||
protected:
|
||||
bool centered() const override { return false; }
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output) override;
|
||||
};
|
||||
|
||||
class FillOctagramSpiral : public FillPlanePath
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
#include "FirstLayerPlane.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
// Build the row of the gcode-axis-remap matrix R that produces machine_Z,
|
||||
// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate
|
||||
// space. Without back-transform this is just R.row(2). With back-transform
|
||||
// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2).
|
||||
//
|
||||
// Returns a pair (gradient, constant) such that:
|
||||
// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant
|
||||
struct MachineZAffine {
|
||||
Vec3d gradient = Vec3d::UnitZ();
|
||||
double constant = 0.0;
|
||||
};
|
||||
|
||||
MachineZAffine compute_machine_z_affine(const PrintConfig &config)
|
||||
{
|
||||
MachineZAffine out;
|
||||
|
||||
// R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis
|
||||
// i picks one slicing-frame component (with sign + optional Rev mode
|
||||
// translation) based on m_remap_{x,y,z}. We only need row 2 (the z output)
|
||||
// since machine_Z is what defines the first-layer plane.
|
||||
int rz = int(config.gcode_remap_z.value);
|
||||
int axis = rz % 3;
|
||||
double sign;
|
||||
double trans;
|
||||
if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z
|
||||
sign = 1.0;
|
||||
trans = 0.0;
|
||||
} else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z
|
||||
sign = -1.0;
|
||||
trans = 0.0;
|
||||
} else { // 6..8 = RevX/Y/Z
|
||||
sign = -1.0;
|
||||
BoundingBoxf bbox_bed(config.printable_area.values);
|
||||
Vec3d vol_max(bbox_bed.max.x(),
|
||||
bbox_bed.max.y(),
|
||||
config.printable_height.value);
|
||||
trans = vol_max[axis];
|
||||
}
|
||||
|
||||
Vec3d r_row = Vec3d::Zero();
|
||||
r_row[axis] = sign;
|
||||
|
||||
// Without back-transform, machine_Z(slicing) = r_row · slicing + trans.
|
||||
out.gradient = r_row;
|
||||
out.constant = trans;
|
||||
|
||||
if (config.gcode_back_transform.value && config.belt_printer.value) {
|
||||
// BeltGCodeWriter applies F^-1 before R when back-transform is on.
|
||||
// So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans
|
||||
// = (r_row^T · F^-1) · slicing + trans
|
||||
// We need to compose r_row with F^-1 from the LEFT (treating r_row as
|
||||
// a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row.
|
||||
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
|
||||
Transform3d inverse = forward.inverse();
|
||||
// Note: forward.translation() is normally zero (per-print transforms
|
||||
// don't add a translation; the per-object z_shift is added separately
|
||||
// in PrintObjectSlice). We still incorporate inverse.translation() in
|
||||
// case a Rev-mode preslice_remap puts a translation in F.
|
||||
Vec3d composed_grad = inverse.linear().transpose() * r_row;
|
||||
double composed_trans =
|
||||
r_row.dot(inverse.translation()) + trans;
|
||||
out.gradient = composed_grad;
|
||||
out.constant = composed_trans;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FirstLayerPlane::FirstLayerPlane(const PrintConfig &config)
|
||||
{
|
||||
// -------- Resolve Auto -------------------------------------------------
|
||||
FirstLayerPlaneMode mode = config.first_layer_plane.value;
|
||||
if (mode == FirstLayerPlaneMode::Auto) {
|
||||
bool belt_affine_active = config.belt_printer.value &&
|
||||
config.belt_slice_rotation.value != BeltRotationAxis::None &&
|
||||
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
|
||||
mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine
|
||||
: FirstLayerPlaneMode::XY;
|
||||
}
|
||||
m_mode = mode;
|
||||
|
||||
// -------- Band thickness ----------------------------------------------
|
||||
// Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we
|
||||
// can't fall back to it from here. initial_layer_print_height is in
|
||||
// PrintConfig and is the right default anyway (the legacy first-layer
|
||||
// semantics used initial_layer_print_height, not the regular one).
|
||||
double thickness = config.first_layer_plane_thickness.value;
|
||||
if (thickness <= 0.0)
|
||||
thickness = config.initial_layer_print_height.value;
|
||||
if (thickness <= 0.0)
|
||||
thickness = 0.2;
|
||||
m_thickness_mm = thickness;
|
||||
|
||||
const double user_offset = config.first_layer_plane_offset.value;
|
||||
|
||||
// -------- Build the plane ---------------------------------------------
|
||||
auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) {
|
||||
m_normal = n_unit;
|
||||
m_offset = offset_along_n;
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
case FirstLayerPlaneMode::XY:
|
||||
// Legacy XY plane. Inactive: short-circuit to layer-index path.
|
||||
set_axis_aligned(Vec3d::UnitZ(), user_offset);
|
||||
m_active = false;
|
||||
return;
|
||||
|
||||
case FirstLayerPlaneMode::YZ:
|
||||
set_axis_aligned(Vec3d::UnitX(), user_offset);
|
||||
m_active = true;
|
||||
return;
|
||||
|
||||
case FirstLayerPlaneMode::XZ:
|
||||
set_axis_aligned(Vec3d::UnitY(), user_offset);
|
||||
m_active = true;
|
||||
return;
|
||||
|
||||
case FirstLayerPlaneMode::BeltAffine: {
|
||||
// Compute the slicing-frame plane that maps to machine_Z = user_offset
|
||||
// under the gcode axis remap (and optional back-transform).
|
||||
MachineZAffine mz = compute_machine_z_affine(config);
|
||||
double cmag = mz.gradient.norm();
|
||||
if (cmag < EPSILON) {
|
||||
// Degenerate: slicing point doesn't affect machine_Z. Fall back.
|
||||
set_axis_aligned(Vec3d::UnitZ(), user_offset);
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
// Plane equation: gradient · slicing = user_offset - constant
|
||||
const double K = user_offset - mz.constant;
|
||||
m_normal = mz.gradient / cmag;
|
||||
m_offset = K / cmag;
|
||||
m_active = true;
|
||||
return;
|
||||
}
|
||||
|
||||
case FirstLayerPlaneMode::Auto:
|
||||
// Should have been resolved above.
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const
|
||||
{
|
||||
return m_normal.dot(point_slicing_mm) - m_offset;
|
||||
}
|
||||
|
||||
bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm,
|
||||
double first_layer_height_mm) const
|
||||
{
|
||||
if (!m_active)
|
||||
return false;
|
||||
return distance_from_plane(point_slicing_mm) < first_layer_height_mm;
|
||||
}
|
||||
|
||||
int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const
|
||||
{
|
||||
if (!m_active)
|
||||
return INT_MAX / 2; // Effectively "way past first layer".
|
||||
double d = distance_from_plane(point_slicing_mm);
|
||||
if (d <= 0.0)
|
||||
return 0;
|
||||
return int(std::floor(d / m_thickness_mm));
|
||||
}
|
||||
|
||||
int FirstLayerPlane::min_effective_index_for_xy_bbox(
|
||||
const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const
|
||||
{
|
||||
if (!m_active)
|
||||
return INT_MAX / 2;
|
||||
// For the rectangular bbox in (x, y) at fixed z, the smallest value of
|
||||
// (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four
|
||||
// corners, with the smaller component picked when the corresponding
|
||||
// normal coefficient is positive.
|
||||
const double x_for_min = (m_normal.x() >= 0.0)
|
||||
? xy_bbox_mm.min.x() : xy_bbox_mm.max.x();
|
||||
const double y_for_min = (m_normal.y() >= 0.0)
|
||||
? xy_bbox_mm.min.y() : xy_bbox_mm.max.y();
|
||||
const double dmin = m_normal.x() * x_for_min
|
||||
+ m_normal.y() * y_for_min
|
||||
+ m_normal.z() * slicing_z_mm
|
||||
- m_offset;
|
||||
if (dmin <= 0.0)
|
||||
return 0;
|
||||
return int(std::floor(dmin / m_thickness_mm));
|
||||
}
|
||||
|
||||
int FirstLayerPlane::min_effective_index_for_bbox3(
|
||||
const BoundingBoxf3 &bbox_mm) const
|
||||
{
|
||||
if (!m_active)
|
||||
return INT_MAX / 2;
|
||||
const double x_for_min = (m_normal.x() >= 0.0)
|
||||
? bbox_mm.min.x() : bbox_mm.max.x();
|
||||
const double y_for_min = (m_normal.y() >= 0.0)
|
||||
? bbox_mm.min.y() : bbox_mm.max.y();
|
||||
const double z_for_min = (m_normal.z() >= 0.0)
|
||||
? bbox_mm.min.z() : bbox_mm.max.z();
|
||||
const double dmin = m_normal.x() * x_for_min
|
||||
+ m_normal.y() * y_for_min
|
||||
+ m_normal.z() * z_for_min
|
||||
- m_offset;
|
||||
if (dmin <= 0.0)
|
||||
return 0;
|
||||
return int(std::floor(dmin / m_thickness_mm));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,76 +0,0 @@
|
||||
#ifndef slic3r_FirstLayerPlane_hpp_
|
||||
#define slic3r_FirstLayerPlane_hpp_
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Decides which extrusions get "first layer" treatment (no fan, slow speed,
|
||||
// initial-layer accel/jerk, deferred temperature drop) by reference to a
|
||||
// configurable plane in slicing-frame coordinates rather than the slicing
|
||||
// layer index.
|
||||
//
|
||||
// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the
|
||||
// evaluator is INACTIVE — every call site short-circuits back to the legacy
|
||||
// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the
|
||||
// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates
|
||||
// derived from the gcode axis remap, so layer-index-based detection no
|
||||
// longer matches the physical first printed surface.
|
||||
//
|
||||
// Plane representation: unit normal `n` (slicing frame) and offset along
|
||||
// the normal such that the plane equation is `n · p == offset`. Signed
|
||||
// perpendicular distance is `d(p) = n · p - offset`. Positive distance
|
||||
// means "away from the belt surface", negative means "below the plane".
|
||||
class FirstLayerPlane
|
||||
{
|
||||
public:
|
||||
explicit FirstLayerPlane(const PrintConfig &config);
|
||||
|
||||
// Inactive when the legacy XY layer-index path should be used. This
|
||||
// covers all non-belt printers and any belt printer where the user
|
||||
// explicitly picked XY mode.
|
||||
bool is_active() const { return m_active; }
|
||||
FirstLayerPlaneMode effective_mode() const{ return m_mode; }
|
||||
double band_thickness_mm() const { return m_thickness_mm; }
|
||||
const Vec3d & normal() const { return m_normal; }
|
||||
double plane_offset() const { return m_offset; }
|
||||
|
||||
// Signed perpendicular distance from a slicing-frame point to the plane.
|
||||
double distance_from_plane(const Vec3d &point_slicing_mm) const;
|
||||
|
||||
// True if perpendicular distance < first_layer_height_mm. When the
|
||||
// evaluator is inactive this returns false (call sites should fall back
|
||||
// to the legacy per-layer path before reaching this function).
|
||||
bool is_first_layer(const Vec3d &point_slicing_mm,
|
||||
double first_layer_height_mm) const;
|
||||
|
||||
// floor((distance - 0) / band_thickness), clamped to [0, +inf). Used
|
||||
// for "first N layers" thresholds (fan, slow_down_layers). Returns 0
|
||||
// for points within the band. Returns INT_MAX/2 when inactive.
|
||||
int effective_layer_index(const Vec3d &point_slicing_mm) const;
|
||||
|
||||
// Min effective index over a 2D bbox at a fixed slicing_Z. Used for
|
||||
// layer-level decisions (e.g. temperature transition gate) where we
|
||||
// don't want to walk every extrusion in the layer. For axis-aligned
|
||||
// planes this is exact; for tilted planes it's a tight lower bound
|
||||
// (the plane projection of the bbox's extreme corner).
|
||||
int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm,
|
||||
double slicing_z_mm) const;
|
||||
|
||||
// Same as above but the bbox spans a Z range too.
|
||||
int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const;
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY;
|
||||
Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame
|
||||
double m_offset = 0.0; // n·p == m_offset
|
||||
double m_thickness_mm = 0.0;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FirstLayerPlane_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,6 @@
|
||||
#include "libslic3r.h"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "BeltGCodeWriter.hpp"
|
||||
#include "FirstLayerPlane.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PlaceholderParser.hpp"
|
||||
@@ -186,7 +184,7 @@ struct LayerResult {
|
||||
// It is used for the pressure equalizer because it needs to buffer one layer back.
|
||||
bool nop_layer_result { false };
|
||||
|
||||
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<coord_t>::max(), false, false, true}; }
|
||||
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<size_t>::max(), false, false, true}; }
|
||||
};
|
||||
|
||||
class GCode {
|
||||
@@ -216,18 +214,16 @@ public:
|
||||
m_last_obj_copy(nullptr, Point(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max())),
|
||||
// BBS
|
||||
m_toolchange_count(0),
|
||||
m_nominal_z(0.),
|
||||
m_writer(std::make_unique<GCodeWriter>())
|
||||
m_nominal_z(0.)
|
||||
{}
|
||||
virtual ~GCode() = default;
|
||||
~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);
|
||||
void export_layer_filaments(GCodeProcessorResult* result);
|
||||
//BBS: set offset for gcode writer
|
||||
void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
|
||||
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
|
||||
|
||||
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
|
||||
const Vec2d& origin() const { return m_origin; }
|
||||
@@ -241,8 +237,8 @@ public:
|
||||
Vec3d point_to_gcode_quantized(const Point3& point) const;
|
||||
const FullPrintConfig &config() const { return m_config; }
|
||||
const Layer* layer() const { return m_layer; }
|
||||
GCodeWriter& writer() { return *m_writer; }
|
||||
const GCodeWriter& writer() const { return *m_writer; }
|
||||
GCodeWriter& writer() { return m_writer; }
|
||||
const GCodeWriter& writer() const { return m_writer; }
|
||||
PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; }
|
||||
const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; }
|
||||
// Process a template through the placeholder parser, collect error messages to be reported
|
||||
@@ -265,7 +261,7 @@ public:
|
||||
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
|
||||
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
|
||||
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
|
||||
std::string unretract(float extra_retract = 0.f) { return m_writer->unlift() + m_writer->unretract(extra_retract); }
|
||||
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
|
||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
|
||||
bool is_BBL_Printer();
|
||||
WipeTowerType wipe_tower_type();
|
||||
@@ -323,15 +319,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Public accessor for the first-layer plane evaluator. Used by
|
||||
// CoolingBuffer (which is constructed with a GCode reference and needs
|
||||
// to read the plane for per-segment fan re-evaluation). All other
|
||||
// first-layer-plane access points (on_first_layer overload, effective
|
||||
// index helper) are in the protected section since they're called from
|
||||
// GCode internals only.
|
||||
const FirstLayerPlane *first_layer_plane() const { return m_first_layer_plane.get(); }
|
||||
|
||||
protected:
|
||||
private:
|
||||
class GCodeOutputStream {
|
||||
public:
|
||||
GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {}
|
||||
@@ -359,17 +347,9 @@ protected:
|
||||
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, bool is_bbl_printers) {}
|
||||
virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {}
|
||||
virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {}
|
||||
virtual bool should_disable_arc_fitting() const { return false; }
|
||||
|
||||
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
|
||||
|
||||
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false);
|
||||
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object);
|
||||
static std::vector<std::pair<coordf_t, std::vector<LayerToPrint>>> collect_layers_to_print(const Print &print);
|
||||
|
||||
std::string generate_skirt(const Print &print,
|
||||
@@ -607,7 +587,7 @@ protected:
|
||||
DynamicConfig m_calib_config;
|
||||
// scaled G-code resolution
|
||||
double m_scaled_resolution;
|
||||
std::unique_ptr<GCodeWriter> m_writer;
|
||||
GCodeWriter m_writer;
|
||||
|
||||
struct PlaceholderParserIntegration {
|
||||
void reset();
|
||||
@@ -727,11 +707,6 @@ protected:
|
||||
|
||||
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
|
||||
std::unique_ptr<SpiralVase> m_spiral_vase;
|
||||
// First-layer plane evaluator. Constructed once per print from the
|
||||
// PrintConfig. is_active() == false on non-belt printers and on belt
|
||||
// printers without a Z-axis shear; in that case all per-path plane
|
||||
// checks short-circuit to the legacy Layer::id() == 0 path.
|
||||
std::unique_ptr<FirstLayerPlane> m_first_layer_plane;
|
||||
|
||||
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
|
||||
|
||||
@@ -797,25 +772,6 @@ protected:
|
||||
// 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. When the FirstLayerPlane evaluator is
|
||||
// active, the result depends on the supplied slicing-frame point;
|
||||
// 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 {
|
||||
if (m_first_layer_plane && m_first_layer_plane->is_active())
|
||||
return m_first_layer_plane->is_first_layer(
|
||||
point_slicing_mm, m_config.initial_layer_print_height.value);
|
||||
return on_first_layer();
|
||||
}
|
||||
// "Effective layer index" used to drive layer-count thresholds like
|
||||
// slow_down_layers. When the evaluator is active this returns the
|
||||
// perpendicular distance to the plane in band_thickness_mm units;
|
||||
// otherwise it returns the legacy slicing layer index.
|
||||
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
|
||||
if (m_first_layer_plane && m_first_layer_plane->is_active())
|
||||
return m_first_layer_plane->effective_layer_index(point_slicing_mm);
|
||||
return on_first_layer() ? 0 : layer_id();
|
||||
}
|
||||
int layer_id() const {
|
||||
if (m_layer == nullptr)
|
||||
return -1;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#include "BeltBackTransform.hpp"
|
||||
#include "../BeltTransform.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool BeltBackTransform::init_from_config(const PrintConfig &config)
|
||||
{
|
||||
m_active = false;
|
||||
m_inverse = Transform3d::Identity();
|
||||
|
||||
if (!config.belt_printer.value || !config.gcode_back_transform.value)
|
||||
return false;
|
||||
|
||||
// Require at least one active transform to proceed.
|
||||
bool has_global_rotation = config.belt_slice_rotation_global.value
|
||||
&& config.belt_slice_rotation.value != BeltRotationAxis::None;
|
||||
bool has_preslice_global = config.belt_preslice_global.value
|
||||
|| config.preslice_remap_global.value;
|
||||
if (!has_global_rotation && !has_preslice_global
|
||||
&& !BeltTransformPipeline::has_preslice_remap(config))
|
||||
return false;
|
||||
|
||||
// Build the forward pipeline (rotation * pre_remap) 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
|
||||
@@ -1,45 +0,0 @@
|
||||
#ifndef slic3r_BeltBackTransform_hpp_
|
||||
#define slic3r_BeltBackTransform_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Reverses the pre-slice remap + shear + scale transforms that
|
||||
// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code
|
||||
// coordinates from the sliced (remapped/sheared/scaled) frame back to the
|
||||
// machine's real coordinate space.
|
||||
//
|
||||
// Initialized once from PrintConfig, then applied per-point in
|
||||
// GCodeWriter::to_machine_coords() before axis remapping.
|
||||
//
|
||||
// Active when gcode_back_transform is true AND at least one of:
|
||||
// - a shear axis has global mode enabled, or
|
||||
// - a pre-slice axis remap is non-identity.
|
||||
class BeltBackTransform
|
||||
{
|
||||
public:
|
||||
BeltBackTransform() = default;
|
||||
|
||||
// Initialize from belt printer config. Rebuilds the same pre-slice remap,
|
||||
// shear, and scale matrices 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;
|
||||
|
||||
// True if a non-identity back-transform is active.
|
||||
bool is_active() const { return m_active; }
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
Transform3d m_inverse = Transform3d::Identity();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBackTransform_hpp_
|
||||
@@ -1,14 +1,10 @@
|
||||
#include "../GCode.hpp"
|
||||
#include "../FirstLayerPlane.hpp"
|
||||
#include "CoolingBuffer.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <float.h>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -32,12 +28,6 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t
|
||||
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
|
||||
m_extruder_ids.emplace_back(ex.id());
|
||||
}
|
||||
|
||||
// Borrow the first-layer plane from the GCode generator. When inactive
|
||||
// (non-belt printers and belt printers without Z shear), per-line fan
|
||||
// re-evaluation is skipped and behavior is bit-identical to the legacy
|
||||
// per-layer path.
|
||||
m_first_layer_plane = gcodegen.first_layer_plane();
|
||||
}
|
||||
|
||||
void CoolingBuffer::reset(const Vec3d &position)
|
||||
@@ -338,13 +328,6 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b
|
||||
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos);
|
||||
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
|
||||
out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
|
||||
// First-layer plane: per-segment fan re-evaluation post-pass. Walks
|
||||
// the cooled-down gcode and inserts inline M106 commands at band
|
||||
// crossings (where the path's perpendicular distance to the plane
|
||||
// crosses close_fan_the_first_x_layers thresholds). No-op when
|
||||
// the evaluator is inactive.
|
||||
if (m_first_layer_plane && m_first_layer_plane->is_active())
|
||||
out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched);
|
||||
m_gcode.clear();
|
||||
}
|
||||
return out;
|
||||
@@ -1075,214 +1058,4 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
return new_gcode;
|
||||
}
|
||||
|
||||
// Pure helper: compute the main fan speed for a given effective layer index.
|
||||
// Mirrors the inline logic in change_extruder_set_fan but is callable from
|
||||
// per-line code in apply_first_layer_plane_fan_eval.
|
||||
int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time,
|
||||
unsigned int extruder_id) const
|
||||
{
|
||||
#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id)
|
||||
float fan_min_speed = EXTRUDER_CFG(fan_min_speed);
|
||||
float fan_max_speed = EXTRUDER_CFG(fan_max_speed);
|
||||
bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq);
|
||||
int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers);
|
||||
int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer);
|
||||
float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time));
|
||||
float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time));
|
||||
#undef EXTRUDER_CFG
|
||||
|
||||
if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0)
|
||||
close_fan_the_first_x_layers = 1;
|
||||
|
||||
float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f;
|
||||
if (effective_layer_id >= close_fan_the_first_x_layers) {
|
||||
if (layer_time < slow_down_layer_time) {
|
||||
fan_speed_new = fan_max_speed;
|
||||
} else if (layer_time < fan_cooling_layer_time) {
|
||||
double t = (layer_time - slow_down_layer_time) /
|
||||
(fan_cooling_layer_time - slow_down_layer_time);
|
||||
fan_speed_new = float(int(floor(t * fan_min_speed +
|
||||
(1. - t) * fan_max_speed) + 0.5));
|
||||
}
|
||||
if (effective_layer_id + 1 < full_fan_speed_layer) {
|
||||
float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers)
|
||||
/ float(full_fan_speed_layer - close_fan_the_first_x_layers);
|
||||
fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255));
|
||||
}
|
||||
} else {
|
||||
fan_speed_new = 0.f;
|
||||
}
|
||||
return int(fan_speed_new);
|
||||
}
|
||||
|
||||
// Post-pass: walk the cooled-down gcode line by line, track XYZ position,
|
||||
// and insert M106 commands at first-layer-plane band crossings so the fan
|
||||
// follows perpendicular distance to the plane rather than the slicing-layer
|
||||
// index. Only invoked when the FirstLayerPlane evaluator is active.
|
||||
//
|
||||
// This implementation is intentionally minimal: it overrides only the MAIN
|
||||
// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc
|
||||
// special fans remain at their layer-level values from apply_layer_cooldown.
|
||||
// That keeps the per-line logic small while still giving the user precise
|
||||
// fan control near the belt surface, which is the main quality concern.
|
||||
std::string CoolingBuffer::apply_first_layer_plane_fan_eval(
|
||||
std::string &&gcode_in, size_t /*layer_id*/, float layer_time)
|
||||
{
|
||||
if (!m_first_layer_plane || !m_first_layer_plane->is_active())
|
||||
return std::move(gcode_in);
|
||||
|
||||
const std::string &gcode = gcode_in;
|
||||
std::string out;
|
||||
out.reserve(gcode.size() + 256);
|
||||
|
||||
// Match the PWM floor applied at every other set_fan call in this file so
|
||||
// band-crossing M106 emissions start the fan reliably at low speeds.
|
||||
const unsigned int part_cooling_fan_min_pwm = static_cast<unsigned int>(std::max(0, m_config.part_cooling_fan_min_pwm.value));
|
||||
|
||||
// Track position in slicing-frame mm. Seed from m_current_pos which the
|
||||
// CoolingBuffer keeps up-to-date across layers.
|
||||
Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]);
|
||||
|
||||
// Track current main fan speed by parsing M106 commands as we walk so
|
||||
// we can restore it after a band exit.
|
||||
int current_main_fan = m_fan_speed;
|
||||
int pre_band_main_fan = current_main_fan;
|
||||
// Implicit initial state: assume the layer started "out of the band"
|
||||
// (i.e., the layer-level fan setting from apply_layer_cooldown is in
|
||||
// effect). The first movement we encounter will reconcile this.
|
||||
bool in_first_layer_band = false;
|
||||
unsigned int active_extruder = m_current_extruder;
|
||||
|
||||
auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) {
|
||||
if (line_sv.size() < 3) return false;
|
||||
if (line_sv[0] != 'G') return false;
|
||||
if (line_sv[1] != '0' && line_sv[1] != '1') return false;
|
||||
if (line_sv[2] != ' ' && line_sv[2] != '\t') return false;
|
||||
const char *c = line_sv.data() + 3;
|
||||
const char *end = line_sv.data() + line_sv.size();
|
||||
bool any = false;
|
||||
while (c < end && *c != ';') {
|
||||
while (c < end && (*c == ' ' || *c == '\t')) ++c;
|
||||
if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break;
|
||||
char axis = *c;
|
||||
++c;
|
||||
if (axis == 'X' || axis == 'Y' || axis == 'Z') {
|
||||
char *next;
|
||||
double v = std::strtod(c, &next);
|
||||
if (next != c) {
|
||||
if (axis == 'X') p.x() = v;
|
||||
else if (axis == 'Y') p.y() = v;
|
||||
else p.z() = v;
|
||||
c = next;
|
||||
any = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Skip unrecognized word.
|
||||
while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n')
|
||||
++c;
|
||||
}
|
||||
return any;
|
||||
};
|
||||
|
||||
auto parse_m106 = [](const std::string_view &line_sv) -> int {
|
||||
// Returns -1 if not an M106, otherwise the S value (0..255).
|
||||
if (line_sv.size() < 4 || line_sv[0] != 'M') return -1;
|
||||
if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6'))
|
||||
return -1;
|
||||
// Find S<value>
|
||||
size_t s_pos = line_sv.find('S');
|
||||
if (s_pos == std::string_view::npos) return -1;
|
||||
const char *c = line_sv.data() + s_pos + 1;
|
||||
char *next;
|
||||
long v = std::strtol(c, &next, 10);
|
||||
if (next == c) return -1;
|
||||
return int(std::clamp<long>(v, 0, 255));
|
||||
};
|
||||
|
||||
auto parse_m107 = [](const std::string_view &line_sv) -> bool {
|
||||
return line_sv.size() >= 4 && line_sv[0] == 'M' &&
|
||||
line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7';
|
||||
};
|
||||
|
||||
auto parse_tool_change = [this](const std::string_view &line_sv) -> int {
|
||||
// Returns the new extruder id, or -1 if not a toolchange.
|
||||
if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1;
|
||||
if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0)
|
||||
return -1;
|
||||
const char *c = line_sv.data() + m_toolchange_prefix.size();
|
||||
char *next;
|
||||
long v = std::strtol(c, &next, 10);
|
||||
if (next == c) return -1;
|
||||
return int(v);
|
||||
};
|
||||
|
||||
const char *p = gcode.c_str();
|
||||
const char *end = gcode.c_str() + gcode.size();
|
||||
while (p < end) {
|
||||
const char *line_end = p;
|
||||
while (line_end < end && *line_end != '\n') ++line_end;
|
||||
const char *next_line = line_end;
|
||||
if (next_line < end) ++next_line; // include the '\n'
|
||||
|
||||
std::string_view line_sv(p, line_end - p);
|
||||
|
||||
// Track tool changes so the per-line fan eval uses the right extruder.
|
||||
int new_tool = parse_tool_change(line_sv);
|
||||
if (new_tool >= 0)
|
||||
active_extruder = unsigned(new_tool);
|
||||
|
||||
// Track existing fan commands so we can restore the right value when
|
||||
// exiting a band.
|
||||
int m106_speed = parse_m106(line_sv);
|
||||
if (m106_speed >= 0) {
|
||||
current_main_fan = m106_speed;
|
||||
if (!in_first_layer_band)
|
||||
pre_band_main_fan = m106_speed;
|
||||
} else if (parse_m107(line_sv)) {
|
||||
current_main_fan = 0;
|
||||
if (!in_first_layer_band)
|
||||
pre_band_main_fan = 0;
|
||||
}
|
||||
|
||||
// Movement line: parse XYZ, evaluate plane, possibly emit a fan
|
||||
// change BEFORE this line.
|
||||
bool moved = parse_xyz_into(line_sv, cur_pos_mm);
|
||||
if (moved) {
|
||||
const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm);
|
||||
const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder);
|
||||
const bool now_in_band = eff_idx < std::max(close_n, 1);
|
||||
if (now_in_band != in_first_layer_band) {
|
||||
// Band crossing: emit a M106 with the appropriate speed.
|
||||
int target_fan;
|
||||
if (now_in_band) {
|
||||
// Entering the first-layer band: fan off.
|
||||
pre_band_main_fan = current_main_fan;
|
||||
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
|
||||
} else {
|
||||
// Exiting the band: restore the layer's normal fan speed.
|
||||
// Use compute_main_fan_speed with the effective index so
|
||||
// the linear ramp factor (close_fan→full_fan_speed_layer)
|
||||
// also follows distance from the plane.
|
||||
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
|
||||
if (target_fan == 0)
|
||||
target_fan = pre_band_main_fan;
|
||||
}
|
||||
if (target_fan != current_main_fan) {
|
||||
out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm);
|
||||
current_main_fan = target_fan;
|
||||
m_fan_speed = target_fan;
|
||||
m_current_fan_speed = target_fan;
|
||||
}
|
||||
in_first_layer_band = now_in_band;
|
||||
}
|
||||
}
|
||||
|
||||
out.append(p, next_line - p);
|
||||
p = next_line;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace Slic3r {
|
||||
|
||||
class GCode;
|
||||
class Layer;
|
||||
class FirstLayerPlane;
|
||||
struct PerExtruderAdjustments;
|
||||
|
||||
// A standalone G-code filter, to control cooling of the print.
|
||||
@@ -19,7 +18,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 {
|
||||
@@ -37,21 +36,6 @@ private:
|
||||
// Returns the adjusted G-code.
|
||||
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
|
||||
|
||||
// First-layer plane: per-line fan re-evaluation post-pass. Walks the
|
||||
// post-cooldown gcode, tracks XYZ position, and inserts M106 commands at
|
||||
// band-crossing transitions in slicing-frame coordinates. Only runs
|
||||
// when m_first_layer_plane is active.
|
||||
std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in,
|
||||
size_t layer_id,
|
||||
float layer_time);
|
||||
|
||||
// Pure helper: compute the main fan speed for a given effective layer
|
||||
// index (layer-id units, mapped through the plane evaluator) and the
|
||||
// current extruder. Mirrors the inline logic in the change_extruder_set_fan
|
||||
// lambda but is callable from per-line code.
|
||||
int compute_main_fan_speed(int effective_layer_id, float layer_time,
|
||||
unsigned int extruder_id) const;
|
||||
|
||||
// G-code snippet cached for the support layers preceding an object layer.
|
||||
std::string m_gcode;
|
||||
// Internal data.
|
||||
@@ -74,9 +58,6 @@ private:
|
||||
unsigned int m_current_nozzle;
|
||||
//BBS: current fan speed
|
||||
int m_current_fan_speed;
|
||||
// First-layer plane evaluator, borrowed from GCode. Null = inactive
|
||||
// (legacy per-layer fan control).
|
||||
const FirstLayerPlane *m_first_layer_plane = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2530,7 +2530,6 @@ void GCodeProcessorResult::reset() {
|
||||
long_retraction_when_cut = false;
|
||||
timelapse_warning_code = 0;
|
||||
printable_height = 0.0f;
|
||||
machine_frame_transform_active = false;
|
||||
settings_ids.reset();
|
||||
filaments_count = 0;
|
||||
backtrace_enabled = false;
|
||||
@@ -2766,32 +2765,6 @@ 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 BeltGCodeWriter::to_machine_coords, so MoveVertex.position is
|
||||
// in the printer's machine frame. Undo it here so XY area and Z height
|
||||
// checks operate in the build-volume frame that printable_area /
|
||||
// printable_height are defined in. 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;
|
||||
@@ -2802,23 +2775,28 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos
|
||||
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.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/)
|
||||
if (move.extrusion_role == ExtrusionRole::erCustom) {
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp));
|
||||
/*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)].max_print_z_custom =
|
||||
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height);
|
||||
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z);
|
||||
} else {
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp));
|
||||
/*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)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
|
||||
z_for_height);
|
||||
move.print_z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool valid = true;
|
||||
@@ -3058,12 +3036,6 @@ 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();
|
||||
|
||||
auto filament_maps = config.option<ConfigOptionInts>("filament_map");
|
||||
if (filament_maps != nullptr) {
|
||||
m_filament_maps = filament_maps->values;
|
||||
@@ -4182,44 +4154,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
|
||||
return;
|
||||
}
|
||||
|
||||
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
|
||||
// angle header comment (used to enable the preview's belt view).
|
||||
if (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;
|
||||
}
|
||||
// Belt printer: parse pre-slice axis remap from header comments.
|
||||
{
|
||||
auto trim = [](const std::string &s) -> std::string {
|
||||
size_t start = s.find_first_not_of(" \t\r\n");
|
||||
size_t end = s.find_last_not_of(" \t\r\n");
|
||||
return (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
|
||||
};
|
||||
// Pre-slice axis remap
|
||||
auto parse_remap_axis = [](const std::string &s) -> RemapAxis {
|
||||
if (s == "pos_x") return RemapAxis::PosX;
|
||||
if (s == "pos_y") return RemapAxis::PosY;
|
||||
if (s == "pos_z") return RemapAxis::PosZ;
|
||||
if (s == "neg_x") return RemapAxis::NegX;
|
||||
if (s == "neg_y") return RemapAxis::NegY;
|
||||
if (s == "neg_z") return RemapAxis::NegZ;
|
||||
if (s == "rev_x") return RemapAxis::RevX;
|
||||
if (s == "rev_y") return RemapAxis::RevY;
|
||||
if (s == "rev_z") return RemapAxis::RevZ;
|
||||
return RemapAxis::PosX;
|
||||
};
|
||||
if (boost::starts_with(comment, " preslice_remap_x = ")) {
|
||||
m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
|
||||
}
|
||||
if (boost::starts_with(comment, " preslice_remap_y = ")) {
|
||||
m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
|
||||
}
|
||||
if (boost::starts_with(comment, " preslice_remap_z = ")) {
|
||||
m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
|
||||
}
|
||||
}
|
||||
// wipe start tag
|
||||
if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) {
|
||||
m_wiping = true;
|
||||
@@ -5992,8 +5926,11 @@ void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line)
|
||||
GCodeReader::GCodeLine g10;
|
||||
g10.set(Axis::E, -this->m_parser.config().retraction_length.get_at(m_extruder_id));
|
||||
g10.set(Axis::F, this->m_parser.config().retraction_speed.get_at(m_extruder_id) * 60);
|
||||
//Orca: Firmware retract emulation must not change the modal G1 feedrate.
|
||||
const float feedrate = m_feedrate;
|
||||
--m_g1_line_id;
|
||||
process_G1(g10);
|
||||
m_feedrate = feedrate;
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
|
||||
@@ -6002,8 +5939,11 @@ void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
|
||||
GCodeReader::GCodeLine g11;
|
||||
g11.set(Axis::E, this->m_parser.config().retraction_length.get_at(m_extruder_id) + this->m_parser.config().retract_restart_extra.get_at(m_extruder_id));
|
||||
g11.set(Axis::F, this->m_parser.config().deretraction_speed.get_at(m_extruder_id) * 60);
|
||||
// Orca: Firmware unretract emulation must not change the modal G1 feedrate.
|
||||
const float feedrate = m_feedrate;
|
||||
--m_g1_line_id;
|
||||
process_G1(g11);
|
||||
m_feedrate = feedrate;
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G20(const GCodeReader::GCodeLine& line)
|
||||
@@ -6082,13 +6022,6 @@ 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()) {
|
||||
@@ -7067,22 +7000,6 @@ 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 BeltGCodeWriter 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,
|
||||
@@ -7090,7 +7007,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, store_z) + m_extruder_offsets[filament_id],
|
||||
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],
|
||||
static_cast<float>(m_end_position[E] - m_start_position[E]),
|
||||
m_feedrate,
|
||||
0.0f, // actual feedrate
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user