Source:
AGENTS.md— this page is rendered directly from the file in the repository. Edit it there.
Slicer Engine - AI Agent Guidance
A high-performance 3D model slicer engine written in Rust, powered by Clipper2 for polygon clipping operations.
Quick Commands
# Build and run
cargo build
cargo run
# Test and lint
cargo test
cargo fmt && cargo clippy --all-targets --all-features -- -D warnings
# Cross-platform builds
cargo build --target x86_64-pc-windows-msvc # Windows
cargo build --target x86_64-apple-darwin # macOS Intel
cargo build --target aarch64-apple-darwin # macOS ARM
wasm-pack build --target web # WebAssemblyOr use Makefile targets (Linux/macOS):
make build-release build-windows build-macos build-wasm test lint fmtArchitecture & Design
Core Components
| Component | Location | Purpose |
|---|---|---|
| SliceLayer / ExtrusionRole | src/core/types.rs | Core data structures for a single layer |
| Mesh Slicer | src/core/slicer.rs | Triangle→layer contour extraction (slice_mesh) |
| Surface Generation | src/core/surfaces.rs | Top/bottom solid surface detection and infill |
| Wall Restrictions | src/core/walls.rs | Single-wall first/top-layer constraints |
| Infill Boundary | src/core/infill.rs | Interior region calculation and sparse infill |
| Pipeline | src/core/pipeline.rs | process_mesh — orchestrates the full slicing pipeline |
| Scene Engine | src/scene/ | Single source of truth for object placement (CLI / WS / WASM all consume SceneState::apply) |
| Clipper2 Integration | src/core/ | Geometric polygon clipping operations throughout |
| Library Interface | src/lib.rs | Public API exposing core functionality |
| CLI Layer | src/cli/ | User-friendly command-line interface bridging library API to commands |
| Build Configuration | build.rs | Platform detection and environment setup |
Module Organization
src/
├── cli/ # CLI layer (user-friendly commands)
│ ├── mod.rs # CLI entry point, command dispatcher
│ ├── commands/ # Command implementations
│ │ ├── slice.rs # Slice operation command
│ │ └── info.rs # Information command
│ ├── io/ # File I/O layer
│ │ ├── validation.rs # Path/file validation
│ │ ├── reader.rs # File reader implementations
│ │ └── writer.rs # File writer implementations
│ ├── output.rs # Output formatting (JSON, GCode)
│ ├── error.rs # CLI error types
│ └── adapters.rs # Library API adapters
├── core/ # Core slicing operations (split by concern)
│ ├── mod.rs # Re-exports public API + integration tests
│ ├── types.rs # SliceLayer, ExtrusionRole
│ ├── slicer.rs # slice_mesh, segment chaining
│ ├── surfaces.rs # generate_top_bottom_surfaces*, rectilinear infill fill
│ ├── walls.rs # apply_single_wall_restrictions (per-island), compute_per_island_strip_masks
│ ├── infill.rs # calculate_interior_region, add_infill_to_layers
│ └── pipeline.rs # process_mesh (full pipeline orchestrator)
├── scene/ # Unified scene engine (issue #51 — SSOT for object placement)
│ ├── mod.rs # Re-exports public API
│ ├── transform.rs # Transform { translation, rotation: Quat, scale }; apply_transform; Euler-XYZ deg helpers
│ ├── bed.rs # BedConfig (width/depth/height/origin offsets); From<&MachineConfig>
│ ├── loader.rs # MeshFormat enum + load_bytes / load_path
│ ├── state.rs # SceneState, SceneObject, ObjectId
│ ├── ops.rs # SceneOp (Add/Remove/Translate/Rotate/Scale/SetTransform/Center/Drop/AlignFace) + apply()
│ └── wasm.rs # SceneHandle wasm-bindgen exports (cfg(target_arch="wasm32"))
├── lib.rs # Public library root
└── main.rs # Application entry point (uses CLI)- lib.rs: Public library root - re-exports core module and CLI
- core/: Core data structures and operations split by concern;
mod.rsre-exports the public API so all external callers (crate::core::*) remain unchanged - cli/: CLI layer providing user-friendly commands and file I/O
- main.rs: Application entry point that delegates to CLI layer
Cross-Platform Strategy
The build script (build.rs) detects target platform and sets environment variables:
- Windows: x86_64-pc-windows-msvc
- macOS: x86_64-apple-darwin (Intel), aarch64-apple-darwin (Silicon)
- WebAssembly: wasm32-unknown-unknown with wasm-pack
Development Conventions
CLI Layer Architecture
The CLI layer uses the adapter pattern to bridge the library API to user-friendly commands:
- Separation of Concerns: CLI commands in
src/cli/don't modify core library code - Error Handling: Custom
CliErrortype provides user-friendly error messages - Output Formatting: Pluggable formatters support JSON and human-readable outputs
- File I/O: Dedicated
io/submodule handles all file operations with validation - Backward Compatibility: Library API remains unchanged; CLI is purely additive
Example CLI Usage:
# Slice a model with 0.2mm layer height
slicer-engine slice --input model.stl --layer-height 0.2 --output result.gcode
# Display build information
slicer-engine info --verbose
# Get help on any command
slicer-engine slice --helpCode Style
- Follow Rust Edition 2021 conventions
- Use
cargo fmtfor formatting (enforced by CI) - Run
cargo clippy -- -D warningsbefore committing - Write inline tests with
#[cfg(test)]in the same module
Performance Priorities
- Development builds prioritized locally: Use
cargo build(debug/opt-level 1) for fast iteration (~5-10s)- Release builds with LTO/opt-level 3/codegen-units 1 are reserved for CI/distribution only
- Profile edge cases with
cargo flamegraphif performance regressions suspected
- CI builds: GitHub Actions builds with
--releaseto test final optimized product - Minimize allocations in hot paths (especially in slicing operations)
- Consider compile-time vs runtime tradeoffs for polygon operations
Documentation
- Use doc comments (
///) for public types and functions - Include usage examples in doc comments for core APIs
- Update README.md for user-facing changes
Module READMEs — house style
Long-form module docs (src/<module>/README.md) follow the Diátaxis Explanation quadrant — they discuss what something is and why it is that way, not how to call every function (that's what /// doc comments are for). Reference src/scene/README.md as the canonical example. Conventions:
- Open with a one-sentence answer to "what does this module exist for?" followed by the single rule or invariant the rest of the doc defends.
- Lead with motivation, then contract, then anatomy. Why → rules → shapes → catalog → role in the wider system → lifecycle → non-goals.
- Sprinkle small Mermaid diagrams where a picture saves a paragraph. Prefer several focused diagrams (one
flowchart, oneclassDiagram, onesequenceDiagram) over one monster graph. Keep node labels short. - Compact tables for catalogs (ops, variants, flags) — three or four columns max; one-line cells.
- State the non-goals explicitly. A "what this module deliberately does not do" section prevents future drift back into anti-patterns.
- Plain language over jargon. Assume a contributor who knows Rust but is new to this subsystem. Define a term the first time it appears.
- End with a "See also" pointing at the source files, the relevant AGENTS.md section, and the originating issue/PR.
Testing
- Write tests inline with
#[cfg(test)]modules - Use
cargo testto verify release build compatibility - Test all three platforms: native, WASM, and cross-compilation
Project Dependencies
| Crate | Version | Usage |
|---|---|---|
| clipper2 | 0.5 | Polygon clipping, geometric operations |
Note: Keep clipper2 dependency current for bug fixes and performance improvements.
Common Tasks
Adding a CLI Command
- Create command module in
src/cli/commands/your_command.rs - Define command struct with
#[derive(Parser)]from clap - Implement command logic using library API adapters
- Register in
src/cli/commands/mod.rsand main dispatcher - Add error handling with
CliErrorconversions - Test with
cargo run -- your-command --help
See architecture-cli-layer-1.md for detailed implementation phases.
Adding a New Data Structure
- Create in appropriate module (usually core.rs)
- Implement
DebugandClonetraits for inspection and flexibility - Add inline tests within
#[cfg(test)]block - Document with
///doc comments including examples - Re-export from lib.rs if part of public API
Implementing Geometric Operations
- Leverage Clipper2 API for polygon clipping (avoid reimplementing)
- Define clear input/output types using SliceLayer or similar structures
- Write tests covering edge cases (empty paths, degenerate polygons, etc.)
- Profile performance on large datasets (>10k paths)
- Document assumptions about coordinate precision
Cross-Platform Testing
- Use conditional compilation (
#[cfg(...)]) for platform-specific code - Test locally:
cargo test - Test WASM builds with
wasm-pack test --headless --firefox - Verify CI passes all platform targets before merging
Adding a Database Migration
- Use the
sea-orm-clitool to scaffold the migration file:bashsea-orm-cli migrate generate "your_migration_name" -d src/db - Implement the schema changes in the generated file's
upanddownmethods. - Register the new module in
src/db/migrations/mod.rs. - Add the migration to the
migrations()vector insrc/db/migrator.rs.
CI/CD Pipeline
GitHub Actions (.github/workflows/build.yml) automatically:
- Runs on push and pull requests
- Builds all three platform targets
- Runs linting (clippy) and formatting checks (fmt)
- Executes test suite
Do not bypass CI checks. All builds must pass before merge.
Known Constraints & Pitfalls
- Clipper2 Coordinate System: Uses integer-based
Centi(centimeter precision). Be aware when converting from floating-point models. - CLI Framework: Uses clap v4 for argument parsing. Keep derive macros in sync with command requirements.
- WASM Memory: Be mindful of WebAssembly memory limits when processing large 3D models.
- File I/O in WASM: CLI file operations require JavaScript bindings; not all features available in WASM target.
- LTO Compilation: Release builds are slower due to LTO. Use debug builds during iterative development.
- Cross-compilation: Requires appropriate target toolchains installed. CI verifies these work.
apply_single_wall_restrictionsis per-island: Inner walls are stripped only from the specific island whose top-surface run ends on that layer; other islands on the same layer are untouched. Thepre_strip_infill_regionssnapshot is still taken before this step to guard against future regressions — keep that order.
Scene Engine — SSOT Contract
src/scene/ is the single source of truth for object placement, orientation, and transforms. Issue #51 introduced it; CLI, WS server, and the Angular UI (via WASM) all consume the same SceneState::apply() code path. Every CLI flag and every UI gesture must translate to a SceneOp.
- Math:
glam::{Vec3, Quat, Mat4}. Quaternions internally; Euler-XYZ degrees only at protocol/CLI boundaries (seeTransform::from_euler_xyz_deg/to_euler_xyz_deg). - Ops (
SceneOp):Add,Remove,Translate,SetTransform,Rotate,Scale,CenterOnBed,DropToFloor,AlignFaceToFloor. Eachapplyreturns anOpReceipt { inverse }— sets up undo without implementing it. - AlignFaceToFloor: picks face by index, computes
Quat::from_rotation_arc(world_normal, -Z), then drops to floor. - Bake at the slicer boundary only:
apply_transform(&Mesh, &Transform) -> Meshis called once before the slicing pipeline runs. Never bake mid-pipeline. - Object IDs:
ObjectId(u64)is monotonically allocated and never reused. UUIDs are reserved for the WS protocol's upload tokens, not for scene objects. - Server scenes are ephemeral per WS connection (no DB persistence). UI uploads bytes via the file-upload endpoint, then dispatches
Scene { ops: [Add { file_id }, …] }. - WASM (
src/scene/wasm.rs,cfg(target_arch="wasm32")): exposesSceneHandlewithaddMesh,applyOp,getRenderBuffer,getMatrix,snapshot. JS bindings build viamake build-wasm→ui/src/generated/scene-wasm/. - Wasm vs native deps:
clipper2,zip,uuid,rayon,tobj,actix-*,tokio,rusqliteare gatedcfg(not(target_arch="wasm32")). The wasm build only shipsmesh,scene,logging, plus wasm-onlywasm-bindgen/js-sys/serde-wasm-bindgen. Module-level#[cfg]s inlib.rsenforce this. - Deprecated CLI flags:
--center/--drop-to-floorare kept as aliases that log a deprecation warning and dispatch the equivalentSceneOp. Do not add new flags that bypass the scene engine. - Don't add a parallel mesh placement path. The temptation to "just translate this mesh real quick" in
mesh::transformsis exactly what issue #51 set out to eliminate.
Slicing Pipeline — Deep Knowledge
This section records hard-won understanding of how the slicing pipeline works and why specific design decisions were made. Read this before touching anything in src/core/ or src/arachne/mod.rs.
Pipeline Execution Order
slice_mesh() — raw mesh → OuterWall contours per layer
generate_arachne_walls() — replaces OuterWall contours with bead paths
pre_strip_infill_regions computed — interior regions snapshotted before wall stripping
apply_single_wall_restrictions() — strips inner walls from first/last layers if configured
interior_regions computed — per-layer interior (for surfaces), post-strip
generate_top_bottom_surfaces_with_interior() — top/bottom solid infill within interior
add_infill_to_layers() — sparse infill using pre-strip regions minus solid regionsOrder matters critically. Surfaces are computed after Arachne walls so that calculate_interior_region sees the correct bead geometry. Infill is computed after surfaces so it can subtract solid_regions.
pre_strip_infill_regions must be computed before apply_single_wall_restrictions.apply_single_wall_restrictions now operates per island: an outer-wall path P at layer i has its associated inner walls stripped only when P's footprint has an exposed top surface AND P does not appear in layer i+1 (the island ends here). The large body island on the same layer is unaffected. The pre_strip_infill_regions snapshot is still taken before this step as a defensive measure — the snapshot preserves the correct walls_per_island count for every island in case future changes ever re-introduce a layer-wide strip.
Arachne Wall Paths — What They Are and Are Not
Arachne emits centerline paths, not filled polygons. Each path is a closed polygon whose vertices are the center of the extrusion bead, not its edge.
OuterWallpaths sit at inward depthd/2from the raw mesh contour.InnerWallpaths sit at3d/2,5d/2, … from the outer contour.path_widths[i]carries the actual extrusion width for variable-width beads.- For a mesh with holes (donut, hollow cylinder) the hole boundary also gets an
OuterWalltag (is_outer = truein Arachne, because it is the outermost bead of that contour's shrink sequence). There is no separate "hole wall" tag.
Consequence: you cannot tell an outer solid contour from a hole contour by role alone. Use signed area (path.signed_area()): CCW (positive) = solid island, CW (negative) = hole.
Clipper2 Fill Rules — When to Use Which
| Operation | Fill rule | Why |
|---|---|---|
| Surface detection (intersect/difference of layer perimeters) | EvenOdd | Mesh slicer does not guarantee consistent winding; EvenOdd is winding-independent |
Infill interior subtraction (difference of infill area − solid regions) | Positive | Infill area from calculate_interior_region uses consistent Clipper2 winding; Positive is more predictable for non-overlapping inputs |
| Arachne bead union (old approach, now removed) | NonZero | Would require all CCW; don't use unless winding is normalised first |
Do not union Arachne bead paths with EvenOdd. Tightly nested concentric closed paths under EvenOdd produce alternating in/out bands instead of a single solid region.
perimeter_paths_of — OuterWall Only
perimeter_paths_of() intentionally returns only OuterWall paths, even though layers also have InnerWall paths.
Why: Surface detection compares adjacent layer geometries with Clipper2 intersect/difference using EvenOdd. If InnerWall beads are included, each bead boundary toggles EvenOdd inside/outside. With e.g. 3 inner walls, the inter-bead gaps register as alternating "exposed" strips → spurious BottomSurface or TopSurface paths appear between the wall beads, indistinguishable from real surfaces. The OuterWall paths alone faithfully represent the solid cross-section of each island.
calculate_interior_region — How the Infill/Surface Boundary Is Computed
Uses OuterWall paths directly as the gross outline of each island (winding preserved — do not normalise to CCW). Deflates inward by:
total_inward = (walls_per_island - 0.5) × nozzle_diameter - overlap_distanceThe −0.5 × d term accounts for the fact that OuterWall centerlines are already inset d/2 from the model surface. Without this correction the interior region is over-shrunk by half a bead width.
walls_per_island = ceil(total_wall_bead_count / outer_contour_count) gives the number of wall shells per island. This works because Arachne places the same number of beads on every island (parameters are global, not per-island).
Do not normalise all wall paths to CCW before the inflate. Hole boundary beads have CW winding. Flipping them to CCW makes Clipper2 treat holes as solid material → infill is generated inside the hole (through the void).
Infill Boundary vs. Surface Region
add_infill_to_layers calls calculate_interior_region(layer, 0.0, nozzle_diameter_mm) (overlap = 0) to get the infill area, then subtracts layer.solid_regions with FillRule::Positive.
generate_top_bottom_surfaces_with_interior clips surface regions to interior_regions[i] (computed ahead of time with calculate_interior_region(layer, infill_overlap_percent, nozzle_diameter_mm)) before generating solid infill lines.
Both use calculate_interior_region — but with different overlap_percent values. Keep them consistent if the signature changes.
generate_rectilinear_infill — Scanline Even-Odd Fill
The scanline fills cells using an even-odd intersection count (pairs of sorted X crossings per scan line). This is correct for both simple polygons and for Clipper2-output Paths whose hole sub-paths have CW winding, because the CW hole adds an extra edge crossing that naturally toggles the parity.
No special handling is needed for holes in the input Paths — the algorithm is correct as-is as long as the input Paths has proper Clipper2 winding (CCW solids, CW holes).
Infill for Shapes with Holes
For a layer that contains a hole (e.g. a hollow box cross-section), the calculate_interior_region output is a Clipper2 Paths with:
- One or more CCW sub-paths (solid ring interior)
- One or more CW sub-paths (the hole voids)
The inflate call with a negative delta correctly shrinks the solid ring inward while simultaneously growing the CW hole outward (toward the ring), preserving the annular region where infill should go. The scanline in generate_rectilinear_infill then correctly generates lines only inside the annulus because the hole sub-path's edges produce crossing events that close the infill within the ring.
Related Documentation
- README.md - User guide and feature overview
- SETUP_COMPLETE.md - Initial setup record
- architecture-cli-layer-1.md - CLI layer implementation plan
- Clipper2 Documentation - Polygon clipping reference
Last Updated: 2026-04-27 (per-island wall-strip fix)
Maintainer Guidance: Keep this file in sync with project structure changes, new conventions, or significant architectural decisions.