pyrech/gcode-estimator
Estimate 3D print duration and filament consumption directly from G-code files with this PHP library. Parse toolpaths, feed rates, and extrusion data to provide accurate time and material usage estimates for your app.
pyrech/gcode-estimator is a framework-agnostic PHP library — there is no Laravel service provider, facade, or config file to publish. You install it and instantiate it yourself.
composer require pyrech/gcode-estimator
Where to look first: open vendor/pyrech/gcode-estimator/README.md and the src/ directory. The three classes that matter are the Estimator (the parser/accumulator), the Filament (diameter, density, price — the inputs that turn extruded millimetres into grams and money), and the set of G-code command handlers it dispatches to. Those handlers are the documentation of what the parser understands; skim them before trusting any number the estimator gives you.
Minimal first use case — estimate a file the user has already uploaded:
use Pyrech\GCodeEstimator\Estimator;
use Pyrech\GCodeEstimator\Filament\Filament;
$estimator = new Estimator();
$estimator->addFilament(0, new Filament('PLA', 1.75, 1.24, 19.99));
$estimator->parseFile($request->file('model')->getRealPath());
return response()->json([
'length' => $estimator->getLength(), // metres of filament
'weight' => $estimator->getWeight(), // grams
'cost' => $estimator->getCost(), // in your filament's currency
]);
The shape of the API (method names in particular) has been stable but is only documented by the README, so wrap it once in your own class rather than scattering estimator calls across controllers. Everything else in this guide assumes that thin wrapper.
Wrap it in a dedicated service. The estimator is stateful — you build it up, add filaments, then parse one file. Make that lifecycle obvious:
namespace App\Services;
use App\Data\Estimate;
use Pyrech\GCodeEstimator\Estimator;
use Pyrech\GCodeEstimator\Filament\Filament;
class GCodeEstimator
{
public function estimate(string $path, array $spools): Estimate
{
$estimator = new Estimator();
foreach ($spools as $tool => $spool) {
$estimator->addFilament($tool, new Filament(
name: $spool['material'],
diameter: $spool['diameter'], // mm
density: $spool['density'], // g/cm³
price: $spool['price'], // per spool
weight: $spool['weight'], // spool net weight, g
));
}
$estimator->parseFile($path);
return new Estimate(
length: $estimator->getLength(),
weight: $estimator->getWeight(),
cost: $estimator->getCost(),
);
}
}
Drive the filament inputs from config, not from user input. Material profiles are a fixed, small dataset. Put them in config/filament.php and let users pick a profile, not a density:
// config/filament.php
return [
'profiles' => [
'pla' => ['material' => 'PLA', 'diameter' => 1.75, 'density' => 1.24, 'price' => 19.99, 'weight' => 1000],
'petg' => ['material' => 'PETG', 'diameter' => 1.75, 'density' => 1.27, 'price' => 24.50, 'weight' => 1000],
'abs' => ['material' => 'ABS', 'diameter' => 1.75, 'density' => 1.04, 'price' => 21.00, 'weight' => 1000],
],
];
Cache the config array with config('filament.profiles') and resolve into Filament objects in the service. This keeps density/diameter typos out of your pricing pipeline — a wrong density silently produces a wrong quote.
Run it in a queued job for anything user-facing. Parsing is linear in file size and a 50 MB G-code file will block a request for seconds. Dispatch a job that writes the estimate onto the Upload model, then poll or broadcast:
class EstimateGCode implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public int $timeout = 120;
public function __construct(public Upload $upload) {}
public function handle(GCodeEstimator $estimator): void
{
$result = $estimator->estimate(
Storage::path($this->upload->path),
config('filament.profiles'),
);
$this->upload->update([
'filament_length_m' => round($result->length, 2),
'filament_weight_g' => round($result->weight, 2),
'filament_cost' => round($result->cost, 2),
]);
}
}
Store the rounded result, not the raw floats, and never recompute in a Blade view.
Always store the assumptions alongside the result. The estimate is only meaningful relative to the diameter/density/price you fed it. Persist those on the row (or a JSON column) so a quote issued last month can be explained when filament prices change.
Multi-extruder / multi-material files are handled by addFilament($tool, ...) — the index matches the T0/T1 tool changes in the G-code. If the file references a tool you didn't register, the estimator cannot price it; validate up front by scanning for T commands, or just register every profile you support.
Testing. Commit two or three tiny .gcode fixtures under tests/Fixtures/ (a cube, a file with retractions, a two-tool file) and assert against slicer-reported values with a tolerance, not exact equality. That is the only reliable regression test for a parser.
Diameter and density must match the slicer's assumptions, not the spool's marketing copy. A 1.75 mm nominal filament is often 1.72–1.78 mm; using the wrong value shifts weight by a few percent. Density is in g/cm³, diameter in mm — mixing units is the single most common source of a 100× wrong answer.
Price is per spool weight, not per gram. If your Filament is constructed with a spool price, the weight argument must be the net filament weight (usually 1000 g). If the library expects price-per-kg instead, your cost will be off by the spool size; check the constructor signature against the README of the exact version you install — this is the parameter most likely to have changed.
The parser only understands what it was taught. It was last released in February 2020, so G-code dialects it has never seen — newer PrusaSlicer/OrcaSlicer output, Klipper macros, firmware retraction (G10/G11), non-standard slicer comments — may be silently ignored rather than erroring. A file that yields 0.0 length is almost always a dialect/relative-extrusion problem, not an empty file. Sanity-check one output against your slicer's own "filament used" figure before shipping.
Watch M82/M83 (absolute vs relative extrusion). Relative-mode files are the classic failure case; if your estimates look implausibly small, inspect the fixture for M83.
Retraction and travel moves are not filament. Retraction moves are tiny and usually excluded; if you're comparing against a slicer, expect your number to be slightly lower, and don't chase a 100% match.
PHP version constraints. This library's composer.json predates PHP 8.1. On a modern Laravel (10/11, PHP 8.2+) you may hit an install conflict. Prefer pinning a tag and testing, over --ignore-platform-reqs, which hides real incompatibilities; if the constraint blocks you, composer require --ignore-platform-req=php in a sandbox first and run the test suite before committing.
No maintenance = your problem. With 15 stars and no release since 2020, treat it as vendored logic. If you fork a bug fix, either vendor the single file you patched or point Composer at your fork — do not rely on upstream.
Extension points are the command handlers. Adding support for an unrecognised command means adding a handler class, not editing the parser internals. That is also the cleanest way to add a custom filament model or a different cost formula without forking the estimator: keep the parsing library pure and do pricing in your own service.
File size and memory. If parseFile accepts a stream/path it will stream; if you pass the whole file as a string you will load 50 MB into memory per request. Always pass a path, and prefer Storage::path() over get().
Debugging tip: log the first and last 50 lines of any file that produces suspicious output. The first line usually reveals the slicer (and therefore the dialect); a truncated tail means a partially uploaded file, which the estimator will happily parse into a too-low number.
How can I help you explore Laravel packages today?