From c5ade61679704afbcdbc0907da2ed2c485a0ccf8 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 15:37:06 +0300 Subject: [PATCH 01/22] Refactor OrcaSlicer logic: replace `ImportService` with `MapService`, update import commands, add `WithVendor` concern, `SourceType` enum, `maps` table, and corresponding model. --- app/Concerns/WithVendor.php | 17 +++ .../Commands/OrcaSlicer/ImportCommand.php | 25 ++++ app/Console/Commands/OrcaSlicerCommand.php | 25 ---- app/Enums/SourceType.php | 11 ++ app/Models/Map.php | 25 ++++ app/Services/OrcaSlicer/ImportService.php | 1 + app/Services/OrcaSlicer/MapService.php | 126 ++++++++++++++++++ .../2026_01_03_111601_create_maps_table.php | 23 ++++ .../2026_01_01_000001_sync_orca_slicer.php | 4 +- 9 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 app/Concerns/WithVendor.php create mode 100644 app/Console/Commands/OrcaSlicer/ImportCommand.php delete mode 100644 app/Console/Commands/OrcaSlicerCommand.php create mode 100644 app/Enums/SourceType.php create mode 100644 app/Models/Map.php create mode 100644 app/Services/OrcaSlicer/MapService.php create mode 100644 database/migrations/2026_01_03_111601_create_maps_table.php diff --git a/app/Concerns/WithVendor.php b/app/Concerns/WithVendor.php new file mode 100644 index 0000000..c6af24e --- /dev/null +++ b/app/Concerns/WithVendor.php @@ -0,0 +1,17 @@ +vendors[$name] ??= Vendor::firstOrCreate(['title' => $name]); + } +} diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php new file mode 100644 index 0000000..e635741 --- /dev/null +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -0,0 +1,25 @@ +components->task('Clean up', fn () => $download->cleanup()); + //$this->components->task('Download', fn () => $download->download()); + //$this->components->task('Extract', fn () => $download->extract()); + //$this->components->task('Release', fn () => $download->release()); + $this->components->task('Import map', fn () => $map->import()); + } +} diff --git a/app/Console/Commands/OrcaSlicerCommand.php b/app/Console/Commands/OrcaSlicerCommand.php deleted file mode 100644 index a1335b7..0000000 --- a/app/Console/Commands/OrcaSlicerCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -components->task('Clean up', fn () => $download->cleanup()); - $this->components->task('Download', fn () => $download->download()); - $this->components->task('Extract', fn () => $download->extract()); - $this->components->task('Release', fn () => $download->release()); - $this->components->task('Import profiles', fn () => $import->profiles()); - } -} diff --git a/app/Enums/SourceType.php b/app/Enums/SourceType.php new file mode 100644 index 0000000..e171ff4 --- /dev/null +++ b/app/Enums/SourceType.php @@ -0,0 +1,11 @@ + SourceType::class, + ]; + } +} diff --git a/app/Services/OrcaSlicer/ImportService.php b/app/Services/OrcaSlicer/ImportService.php index b42f7be..6094725 100644 --- a/app/Services/OrcaSlicer/ImportService.php +++ b/app/Services/OrcaSlicer/ImportService.php @@ -27,6 +27,7 @@ use function in_array; use function json_decode; +/** @deprecated */ class ImportService { protected array $filamentTypes = []; diff --git a/app/Services/OrcaSlicer/MapService.php b/app/Services/OrcaSlicer/MapService.php new file mode 100644 index 0000000..fc8bebc --- /dev/null +++ b/app/Services/OrcaSlicer/MapService.php @@ -0,0 +1,126 @@ +profiles() as $file) { + if ($this->skip($file)) { + continue; + } + + $profile = $this->load( + $file->getRealPath() + ); + + $profileName = $this->profileName($file); + + $this->machines( + $profileName, + $profile['name'], + $profile['machine_model_list'] + ); + + $this->filaments( + $profileName, + $profile['name'], + $profile['filament_list'] + ); + } + } + + protected function machines(string $profile, string $vendor, array $machines): void + { + foreach ($machines as $machine) { + $this->store(SourceType::Machine, $profile, $vendor, $machine['name'], $machine['sub_path']); + } + } + + protected function filaments(string $profile, string $vendor, array $filaments): void + { + foreach ($filaments as $filament) { + $this->store(SourceType::Filament, $profile, $vendor, $filament['name'], $filament['sub_path']); + } + } + + protected function store(SourceType $type, string $profile, string $vendor, string $name, string $path): void + { + Map::updateOrCreate([ + 'type' => $type, + 'vendor' => $vendor, + 'key' => $name, + ], [ + 'path' => $profile . '/' . $path, + ]); + } + + /** + * @return SplFileInfo[] + */ + protected function profiles(): array + { + return File::files( + $this->profilesPath() + ); + } + + protected function profileName(SplFileInfo $file): string + { + $name = $file->getFilename(); + $extension = $file->getExtension(); + + return Str::before($name, '.' . $extension); + } + + protected function skip(SplFileInfo $file): bool + { + if ($file->getExtension() !== 'json') { + return true; + } + + return in_array($file->getFilename(), $this->exceptFiles, true); + } + + protected function load(string $path): array + { + return json_decode(file_get_contents($path), true); + } + + protected function profilesPath(): string + { + return $this->path('resources/profiles'); + } + + protected function path(string $filename): string + { + return $this->storage->path( + $this->directory . DIRECTORY_SEPARATOR . $filename + ); + } +} diff --git a/database/migrations/2026_01_03_111601_create_maps_table.php b/database/migrations/2026_01_03_111601_create_maps_table.php new file mode 100644 index 0000000..30576b7 --- /dev/null +++ b/database/migrations/2026_01_03_111601_create_maps_table.php @@ -0,0 +1,23 @@ +id(); + + $table->string('type'); + $table->string('vendor'); + $table->string('key'); + $table->string('path'); + + $table->timestamps(); + }); + } +}; diff --git a/operations/2026_01_01_000001_sync_orca_slicer.php b/operations/2026_01_01_000001_sync_orca_slicer.php index 7f6db87..378c58b 100644 --- a/operations/2026_01_01_000001_sync_orca_slicer.php +++ b/operations/2026_01_01_000001_sync_orca_slicer.php @@ -2,13 +2,13 @@ declare(strict_types=1); -use App\Console\Commands\OrcaSlicerCommand; +use App\Console\Commands\OrcaSlicer\ImportCommand; use DragonCode\LaravelDeployOperations\Operation; return new class extends Operation { public function __invoke(): void { - $this->artisan(OrcaSlicerCommand::class); + $this->artisan(ImportCommand::class); } public function shouldOnce(): bool From 51c00ac9c56a846d8cffef3c567f72abdf13e861 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 17:53:35 +0300 Subject: [PATCH 02/22] Extend OrcaSlicer `MapService`: integrate `WithVendor`, add support for `Process` source type, update `maps` table structure, adjust related models, migrations, and logic. --- app/Enums/SourceType.php | 1 + app/Models/Map.php | 7 ++- app/Models/Vendor.php | 6 +++ app/Services/OrcaSlicer/MapService.php | 43 +++++++++++++------ .../2026_01_03_111601_create_maps_table.php | 7 ++- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/app/Enums/SourceType.php b/app/Enums/SourceType.php index e171ff4..c633ba3 100644 --- a/app/Enums/SourceType.php +++ b/app/Enums/SourceType.php @@ -8,4 +8,5 @@ enum SourceType: string { case Machine = 'machine'; case Filament = 'filament'; + case Process = 'process'; } diff --git a/app/Models/Map.php b/app/Models/Map.php index 24cb541..4f859ee 100644 --- a/app/Models/Map.php +++ b/app/Models/Map.php @@ -6,12 +6,12 @@ use App\Enums\SourceType; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Map extends Model { protected $fillable = [ 'type', - 'vendor', 'key', 'path', ]; @@ -22,4 +22,9 @@ protected function casts(): array 'type' => SourceType::class, ]; } + + public function vendor(): BelongsTo + { + return $this->belongsTo(Vendor::class); + } } diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index c1a5918..20dccef 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -8,6 +8,7 @@ use App\Events\SluggableEvent; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletes; @@ -40,4 +41,9 @@ public function filaments(): Relation { return $this->hasMany(Filament::class); } + + public function maps(): HasMany + { + return $this->hasMany(Map::class); + } } diff --git a/app/Services/OrcaSlicer/MapService.php b/app/Services/OrcaSlicer/MapService.php index fc8bebc..19aea7c 100644 --- a/app/Services/OrcaSlicer/MapService.php +++ b/app/Services/OrcaSlicer/MapService.php @@ -4,8 +4,9 @@ namespace App\Services\OrcaSlicer; +use App\Concerns\WithVendor; use App\Enums\SourceType; -use App\Models\Map; +use App\Models\Vendor; use Illuminate\Container\Attributes\Config; use Illuminate\Container\Attributes\Storage; use Illuminate\Filesystem\FilesystemAdapter; @@ -15,6 +16,8 @@ class MapService { + use WithVendor; + public function __construct( #[Storage('orca_slicer')] protected FilesystemAdapter $storage, @@ -39,42 +42,58 @@ public function import(): void $file->getRealPath() ); + $vendor = $this->vendor( + $profile['name'] + ); + $profileName = $this->profileName($file); $this->machines( + $vendor, $profileName, - $profile['name'], $profile['machine_model_list'] ); $this->filaments( + $vendor, $profileName, - $profile['name'], $profile['filament_list'] ); + + $this->processes( + $vendor, + $profileName, + $profile['process_list'] + ); } } - protected function machines(string $profile, string $vendor, array $machines): void + protected function machines(Vendor $vendor, string $profile, array $machines): void { foreach ($machines as $machine) { - $this->store(SourceType::Machine, $profile, $vendor, $machine['name'], $machine['sub_path']); + $this->store(SourceType::Machine, $vendor, $profile, $machine['name'], $machine['sub_path']); } } - protected function filaments(string $profile, string $vendor, array $filaments): void + protected function filaments(Vendor $vendor, string $profile, array $filaments): void { foreach ($filaments as $filament) { - $this->store(SourceType::Filament, $profile, $vendor, $filament['name'], $filament['sub_path']); + $this->store(SourceType::Filament, $vendor, $profile, $filament['name'], $filament['sub_path']); + } + } + + protected function processes(Vendor $vendor, string $profile, array $processes): void + { + foreach ($processes as $process) { + $this->store(SourceType::Process, $vendor, $profile, $process['name'], $process['sub_path']); } } - protected function store(SourceType $type, string $profile, string $vendor, string $name, string $path): void + protected function store(SourceType $type, Vendor $vendor, string $profile, string $name, string $path): void { - Map::updateOrCreate([ - 'type' => $type, - 'vendor' => $vendor, - 'key' => $name, + $vendor->maps()->updateOrCreate([ + 'type' => $type, + 'key' => $name, ], [ 'path' => $profile . '/' . $path, ]); diff --git a/database/migrations/2026_01_03_111601_create_maps_table.php b/database/migrations/2026_01_03_111601_create_maps_table.php index 30576b7..d61026e 100644 --- a/database/migrations/2026_01_03_111601_create_maps_table.php +++ b/database/migrations/2026_01_03_111601_create_maps_table.php @@ -12,12 +12,17 @@ public function up(): void Schema::create('maps', function (Blueprint $table) { $table->id(); + $table->foreignId('vendor_id')->constrained('vendors')->cascadeOnDelete(); + $table->string('type'); - $table->string('vendor'); + $table->string('key'); $table->string('path'); $table->timestamps(); + + $table->index(['type', 'key']); + $table->unique(['vendor_id', 'type', 'key']); }); } }; From b0457cb2aa28230e51dc6e5791a7b98d9010119a Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 18:32:23 +0300 Subject: [PATCH 03/22] Remove `FilamentTypeTitleCast`: refactor filament type handling, clean up related models, services, and commands, introduce `WithFilaments` and `FilamentTypeService`, and add `WithNozzles` for nozzle imports. --- .../WithFilaments.php} | 27 +++------- app/Concerns/WithNozzles.php | 17 ++++++ .../Commands/OrcaSlicer/ImportCommand.php | 14 +++-- app/Models/FilamentType.php | 8 --- .../OrcaSlicer/FilamentTypeService.php | 32 +++++++++++ app/Services/OrcaSlicer/NozzleService.php | 53 +++++++++++++++++++ 6 files changed, 121 insertions(+), 30 deletions(-) rename app/{Casts/FilamentTypeTitleCast.php => Concerns/WithFilaments.php} (52%) create mode 100644 app/Concerns/WithNozzles.php create mode 100644 app/Services/OrcaSlicer/FilamentTypeService.php create mode 100644 app/Services/OrcaSlicer/NozzleService.php diff --git a/app/Casts/FilamentTypeTitleCast.php b/app/Concerns/WithFilaments.php similarity index 52% rename from app/Casts/FilamentTypeTitleCast.php rename to app/Concerns/WithFilaments.php index d20344d..4ca93ab 100644 --- a/app/Casts/FilamentTypeTitleCast.php +++ b/app/Concerns/WithFilaments.php @@ -2,34 +2,23 @@ declare(strict_types=1); -namespace App\Casts; +namespace App\Concerns; -use App\Exceptions\UnknownFilamentTypeException; -use Illuminate\Contracts\Database\Eloquent\CastsAttributes; -use Illuminate\Database\Eloquent\Model; +use App\Models\FilamentType; use Illuminate\Support\Str; -use function report; - -class FilamentTypeTitleCast implements CastsAttributes +trait WithFilaments { - public function get(Model $model, string $key, mixed $value, array $attributes): string - { - return $value; - } + protected array $filamentTypes = []; - public function set(Model $model, string $key, mixed $value, array $attributes): string + protected function filamentType(string $value): FilamentType { - if ($title = $this->perform($value)) { - return $title; - } - - report(new UnknownFilamentTypeException($value)); + $normalized = $this->filamentTypeNormalize($value); - return 'Unknown'; + return $this->filamentTypes[$value] ??= FilamentType::firstOrCreate(['title' => $normalized]); } - protected function perform(string $value): string + protected function filamentTypeNormalize(string $value): string { return Str::of($value) ->replace(['Generic', 'Value'], '') diff --git a/app/Concerns/WithNozzles.php b/app/Concerns/WithNozzles.php new file mode 100644 index 0000000..6bcdea2 --- /dev/null +++ b/app/Concerns/WithNozzles.php @@ -0,0 +1,17 @@ +nozzles[(string) $value] ??= Nozzle::firstOrCreate(['title' => $value]); + } +} diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php index e635741..eb7583a 100644 --- a/app/Console/Commands/OrcaSlicer/ImportCommand.php +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -5,7 +5,9 @@ namespace App\Console\Commands\OrcaSlicer; use App\Services\OrcaSlicer\DownloadService; +use App\Services\OrcaSlicer\FilamentTypeService; use App\Services\OrcaSlicer\MapService; +use App\Services\OrcaSlicer\NozzleService; use Illuminate\Console\Command; class ImportCommand extends Command @@ -14,12 +16,18 @@ class ImportCommand extends Command protected $description = 'Update resources from OrcaSlicer'; - public function handle(DownloadService $download, MapService $map): void - { + public function handle( + DownloadService $download, + MapService $map, + NozzleService $nozzle, + FilamentTypeService $filamentType + ): void { //$this->components->task('Clean up', fn () => $download->cleanup()); //$this->components->task('Download', fn () => $download->download()); //$this->components->task('Extract', fn () => $download->extract()); //$this->components->task('Release', fn () => $download->release()); - $this->components->task('Import map', fn () => $map->import()); + //$this->components->task('Import map', fn () => $map->import()); + //$this->components->task('Import nozzles', fn () => $nozzle->import()); + $this->components->task('Import filament types', fn () => $filamentType->import()); } } diff --git a/app/Models/FilamentType.php b/app/Models/FilamentType.php index faca8d6..77bdf7d 100644 --- a/app/Models/FilamentType.php +++ b/app/Models/FilamentType.php @@ -4,7 +4,6 @@ namespace App\Models; -use App\Casts\FilamentTypeTitleCast; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -16,11 +15,4 @@ class FilamentType extends Model protected $fillable = [ 'title', ]; - - protected function casts(): array - { - return [ - 'title' => FilamentTypeTitleCast::class, - ]; - } } diff --git a/app/Services/OrcaSlicer/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php new file mode 100644 index 0000000..fbcba55 --- /dev/null +++ b/app/Services/OrcaSlicer/FilamentTypeService.php @@ -0,0 +1,32 @@ +where('type', SourceType::Filament) + ->each(fn (Map $map) => $this->store($map)); + } + + protected function store(Map $map): void + { + if (! $value = $this->detect($map->key)) { + return; + } + + $this->filamentType($value); + } + + protected function detect(string $value): ?string {} +} diff --git a/app/Services/OrcaSlicer/NozzleService.php b/app/Services/OrcaSlicer/NozzleService.php new file mode 100644 index 0000000..5f96b98 --- /dev/null +++ b/app/Services/OrcaSlicer/NozzleService.php @@ -0,0 +1,53 @@ +whereIn('type', [ + SourceType::Filament, SourceType::Process, + ]) + ->each(fn (Map $map) => $this->store($map)); + } + + protected function store(Map $map): void + { + if ($value = $this->detect($map->key)) { + $this->nozzle($value); + + return; + } + + if ($value = $this->detect($map->path)) { + $this->nozzle($value); + } + } + + protected function detect(string $value): ?float + { + foreach ($this->patterns as $pattern) { + if (preg_match($pattern, $value, $matches)) { + return (float) $matches[1]; + } + } + + return null; + } +} From 24b7c26099514a28f4cc6eb491b699775fbbee40 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 19:08:23 +0300 Subject: [PATCH 04/22] Add `FilamentTypeServiceTest` with comprehensive test cases; enhance detection logic, update `WithFilaments` normalization, and refine handling in `FilamentTypeService`. --- .gitignore | 1 + app/Concerns/WithFilaments.php | 7 +- .../OrcaSlicer/FilamentTypeService.php | 16 ++- tests/Unit/FilamentTypeServiceTest.php | 103 ++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/FilamentTypeServiceTest.php diff --git a/.gitignore b/.gitignore index 8534da5..abc5cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ yarn-error.log /.zed /.junie /.ai +/.output.txt diff --git a/app/Concerns/WithFilaments.php b/app/Concerns/WithFilaments.php index 4ca93ab..a75ceb2 100644 --- a/app/Concerns/WithFilaments.php +++ b/app/Concerns/WithFilaments.php @@ -21,7 +21,7 @@ protected function filamentType(string $value): FilamentType protected function filamentTypeNormalize(string $value): string { return Str::of($value) - ->replace(['Generic', 'Value'], '') + ->replace(['Generic', 'Value'], '', false) ->replace(['High Speed', '@HS', ' HS'], '-HS', false) ->replace(['High Flow', '@HF', ' HF'], '-HF', false) ->replace(' plus', '+', false) @@ -29,11 +29,12 @@ protected function filamentTypeNormalize(string $value): string ->replace('-wood', ' Wood', false) ->before('@') ->squish() - ->match('/([^\d][A-Z]{2,4}[+\-\s]?([A-Z]{2,4}|Silk|Wood)?)/') + ->upper() + ->match('/([^\d][A-Z0-9]{2,5}[+\-\s]?([A-Z0-9]{2,4}|SILK|WOOD)?)/') ->replace(' CF', '-CF', false) ->squish() ->trim('-') - ->replaceMatches('/([A-Z]{2})[\s-]?([A-Z]{3,})/', '$2-$1') + ->replaceMatches('/([A-Z0-9]{2})[\s-]?([A-Z0-9]{3,})/', '$2-$1') ->toString(); } } diff --git a/app/Services/OrcaSlicer/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php index fbcba55..4c66a20 100644 --- a/app/Services/OrcaSlicer/FilamentTypeService.php +++ b/app/Services/OrcaSlicer/FilamentTypeService.php @@ -7,11 +7,14 @@ use App\Concerns\WithFilaments; use App\Enums\SourceType; use App\Models\Map; +use Illuminate\Support\Str; class FilamentTypeService { use WithFilaments; + protected string $pattern = '/\\b((?:E)?(?:PLA|ABS|ASA|PETG|PET|PCTG|PAHT|PPA|PA(?:\\d{0,3})?|PC-ABS|PC|PP|PVA|BVOH|PVB|TPU|TPE|HIPS|PEEK|PEKK|PPSU?|PCTPE))(?:[\\s-]?(?:\\+|CF|GF|HT|HF|HS|LW|SE|PRO|PLUS|ULTRA|TOUGH|BASIC|ELITE|SILK|WOOD|MATTE|GLOW|AERO|METAL|SPEED|FAST|HIGH\\s*FLOW|HIGH\\s*SPEED))*\\b/i'; + public function import(): void { Map::query() @@ -28,5 +31,16 @@ protected function store(Map $map): void $this->filamentType($value); } - protected function detect(string $value): ?string {} + protected function detect(string $value): ?string + { + if (! preg_match($this->pattern, $value, $matches)) { + return null; + } + + return Str::of($matches[0]) + ->replace('_', '-', false) + ->squish() + ->trim('-') + ->toString(); + } } diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeServiceTest.php new file mode 100644 index 0000000..09d1fae --- /dev/null +++ b/tests/Unit/FilamentTypeServiceTest.php @@ -0,0 +1,103 @@ +detect($value); + } + }; + + $this->assertSame($expected, $service->exposedDetect($input)); + } + + public static function detectCases(): array + { + return [ + ['Afinia ABS', 'ABS'], + ['Afinia ABS+', 'ABS+'], + ['Afinia ABS@HS', 'ABS-HS'], + ['Afinia PLA@HS', 'PLA-HS'], + ['Afinia TPU', 'TPU'], + ['Afinia Value ABS', 'ABS'], + ['Afinia Value ABS@HS', 'ABS-HS'], + ['Afinia Value PLA HS', 'PLA-HS'], + ['Afinia Value PLA-HS', 'PLA-HS'], + ['Afinia Value PLA@HS', 'PLA-HS'], + + ['Anker Generic ASA 0.2 nozzle', 'ASA'], + ['Anker Generic ASA 0.25 nozzle', 'ASA'], + ['Anker Generic ASA', 'ASA'], + ['Anker Generic PA-CF @base', 'PA-CF'], + ['Anker Generic PC @base', 'PC'], + ['Anker Generic PETG @base', 'PETG'], + ['Anker Generic PETG-CF @base', 'PETG-CF'], + ['Anker Generic PLA 0.25 nozzle', 'PLA'], + ['Anker Generic PLA Silk 0.2 nozzle', 'PLA Silk'], + ['Anker Generic PLA Silk 0.25 nozzle', 'PLA Silk'], + ['Anker Generic PLA Silk', 'PLA Silk'], + + ['Anycubic ABS @Anycubic Kobra S1 0.4 nozzle', 'ABS'], + ['Anycubic PLA @Anycubic Kobra S1 0.4 nozzle', 'PLA'], + ['Anycubic PLA High Speed @Anycubic Kobra S1 0.4 nozzle', 'PLA-HS'], + ['Anycubic PLA Silk @Anycubic Kobra S1 0.4 nozzle', 'PLA Silk'], + ['Anycubic PLA+ @Anycubic Kobra S1 0.4 nozzle', 'PLA+'], + + ['Artillery ASA @Artillery M1 Pro 0.8 nozzle', 'ASA'], + ['Artillery Generic PETG', 'PETG'], + ['Artillery Generic PLA', 'PLA'], + ['Artillery Generic PLA-CF', 'PLA-CF'], + ['Artillery PET @Artillery M1 Pro 0.2 nozzle', 'PET'], + ['Artillery PLA Basic @Artillery M1 Pro 0.2 nozzle', 'PLA'], + ['Artillery PLA Basic @Artillery M1 Pro 0.4 nozzle', 'PLA'], + + ['Bambu ABS-GF @base', 'ABS-GF'], + ['Bambu ASA-Aero @BBL H2D', 'ASA Aero'], + ['Bambu PET-CF @System', 'PET-CF'], + ['Bambu Support for ABS @base', 'ABS'], + + ['COEX PCTG PRIME @BBL A1M 0.8 nozzle', 'PCTG Prime'], + ['COEX PETG @BBL A1M 0.4 nozzle', 'PETG'], + ['COEX PLA @BBL X1C 0.2 nozzle', 'PLA'], + + ['Creality Generic PLA High Speed @Ender-3V3-all', 'PLA-HS'], + + ['eSUN ePLA-LW @System', 'ePLA-LW'], + + ['fdm_filament_abs', 'ABS'], + ['fdm_filament_bvoh', 'BVOH'], + ['fdm_filament_common', null], + ['fdm_filament_eva', 'EVA'], + ['fdm_filament_hips', 'HIPS'], + ['fdm_filament_pla', 'PLA'], + ['fdm_filament_tpu', 'TPU'], + + ['Fiberon PA12-CF @base', 'PA12-CF'], + + ['Generic BVOH @base', 'BVOH'], + ['Generic EVA @base', 'EVA'], + ['Generic HIPS @base', 'HIPS'], + ['Generic PPA-CF @BBL H2D', 'PPA-CF'], + + ['Panchroma PLA Silk @base', 'PLA Silk'], + ['Panchroma PLA Stain @base', 'PLA Stain'], + ['Panchroma PLA Starlight @base', 'PLA Starlight'], + ['Panchroma PLA Temp Shift @base', 'PLA Temp Shift'], + ['Panchroma PLA Translucent @base', 'PLA Translucent'], + + ['Unknown profile string', null], + ]; + } +} From b83fbae5265a8e8c8aacfce76fca1c1c6f2d83a2 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 19:08:36 +0300 Subject: [PATCH 05/22] Rename `test_detect` to `testDetect` in `FilamentTypeServiceTest` for method naming consistency. --- tests/Unit/FilamentTypeServiceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeServiceTest.php index 09d1fae..6a54210 100644 --- a/tests/Unit/FilamentTypeServiceTest.php +++ b/tests/Unit/FilamentTypeServiceTest.php @@ -11,7 +11,7 @@ class FilamentTypeServiceTest extends TestCase { #[DataProvider('detectCases')] - public function test_detect(string $input, ?string $expected): void + public function testDetect(string $input, ?string $expected): void { $service = new class extends FilamentTypeService { public function exposedDetect(string $value): ?string From 2046bd18d1314b1677ded8565aa7cd7a10defc98 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 19:14:02 +0300 Subject: [PATCH 06/22] Extend `FilamentTypeServiceTest` with additional PLA test cases for improved coverage. --- tests/Unit/FilamentTypeServiceTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeServiceTest.php index 6a54210..57a196f 100644 --- a/tests/Unit/FilamentTypeServiceTest.php +++ b/tests/Unit/FilamentTypeServiceTest.php @@ -97,6 +97,13 @@ public static function detectCases(): array ['Panchroma PLA Temp Shift @base', 'PLA Temp Shift'], ['Panchroma PLA Translucent @base', 'PLA Translucent'], + ['eSUN PLA+ @base', 'PLA+'], + ['SUNLU PLA Marble @base', 'PLA Marble'], + ['SUNLU PLA Matte @base', 'PLA Matte'], + ['SUNLU PLA+ 2.0 @base', 'PLA+'], + ['SUNLU Silk PLA+ @base', 'PLA+ Silk'], + ['SUNLU Wood PLA @base', 'PLA Wood'], + ['Unknown profile string', null], ]; } From 36ca06653de8d91fd22300bd7488c349aaf1b4e1 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 22:37:52 +0300 Subject: [PATCH 07/22] Refactor and extend filament type normalization: update detection logic, restructure test cases, integrate vendor handling, and clean up unused functionality. --- app/Concerns/WithFilaments.php | 25 +-- .../Commands/OrcaSlicer/ImportCommand.php | 2 + .../OrcaSlicer/FilamentTypeService.php | 177 ++++++++++++++++-- tests/Unit/FilamentTypeServiceTest.php | 172 +++++++++-------- 4 files changed, 258 insertions(+), 118 deletions(-) diff --git a/app/Concerns/WithFilaments.php b/app/Concerns/WithFilaments.php index a75ceb2..2cf3683 100644 --- a/app/Concerns/WithFilaments.php +++ b/app/Concerns/WithFilaments.php @@ -5,7 +5,6 @@ namespace App\Concerns; use App\Models\FilamentType; -use Illuminate\Support\Str; trait WithFilaments { @@ -13,28 +12,6 @@ trait WithFilaments protected function filamentType(string $value): FilamentType { - $normalized = $this->filamentTypeNormalize($value); - - return $this->filamentTypes[$value] ??= FilamentType::firstOrCreate(['title' => $normalized]); - } - - protected function filamentTypeNormalize(string $value): string - { - return Str::of($value) - ->replace(['Generic', 'Value'], '', false) - ->replace(['High Speed', '@HS', ' HS'], '-HS', false) - ->replace(['High Flow', '@HF', ' HF'], '-HF', false) - ->replace(' plus', '+', false) - ->replace('-silk', ' Silk', false) - ->replace('-wood', ' Wood', false) - ->before('@') - ->squish() - ->upper() - ->match('/([^\d][A-Z0-9]{2,5}[+\-\s]?([A-Z0-9]{2,4}|SILK|WOOD)?)/') - ->replace(' CF', '-CF', false) - ->squish() - ->trim('-') - ->replaceMatches('/([A-Z0-9]{2})[\s-]?([A-Z0-9]{3,})/', '$2-$1') - ->toString(); + return $this->filamentTypes[$value] ??= FilamentType::firstOrCreate(['title' => $value]); } } diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php index eb7583a..2b8f764 100644 --- a/app/Console/Commands/OrcaSlicer/ImportCommand.php +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -4,6 +4,7 @@ namespace App\Console\Commands\OrcaSlicer; +use App\Models\FilamentType; use App\Services\OrcaSlicer\DownloadService; use App\Services\OrcaSlicer\FilamentTypeService; use App\Services\OrcaSlicer\MapService; @@ -28,6 +29,7 @@ public function handle( //$this->components->task('Release', fn () => $download->release()); //$this->components->task('Import map', fn () => $map->import()); //$this->components->task('Import nozzles', fn () => $nozzle->import()); + FilamentType::truncate(); $this->components->task('Import filament types', fn () => $filamentType->import()); } } diff --git a/app/Services/OrcaSlicer/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php index 4c66a20..62b063a 100644 --- a/app/Services/OrcaSlicer/FilamentTypeService.php +++ b/app/Services/OrcaSlicer/FilamentTypeService.php @@ -7,40 +7,195 @@ use App\Concerns\WithFilaments; use App\Enums\SourceType; use App\Models\Map; +use App\Models\Vendor; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; class FilamentTypeService { use WithFilaments; - protected string $pattern = '/\\b((?:E)?(?:PLA|ABS|ASA|PETG|PET|PCTG|PAHT|PPA|PA(?:\\d{0,3})?|PC-ABS|PC|PP|PVA|BVOH|PVB|TPU|TPE|HIPS|PEEK|PEKK|PPSU?|PCTPE))(?:[\\s-]?(?:\\+|CF|GF|HT|HF|HS|LW|SE|PRO|PLUS|ULTRA|TOUGH|BASIC|ELITE|SILK|WOOD|MATTE|GLOW|AERO|METAL|SPEED|FAST|HIGH\\s*FLOW|HIGH\\s*SPEED))*\\b/i'; + protected array $reservedWords = [ + 'c1 generic', + + 'arena', + 'bambu', + 'basic', + 'bignozzle', + 'brand', + 'breakaway', + 'coprint', + 'direct drive', + 'esun', + 'fiberon', + 'fiberthree', + 'fusrock', + 'generic', + 'hatchbox', + 'impact', + 'lancer', + 'material4print', + 'nylex', + 'orcaarena', + 'other', + 'overture', + 'panchroma', + 'polylite', + 'polymaker', + 'polyterra', + 'prime', + 'pro', + 'prusament', + 'rapido', + 'standard', + 'sunlu', + 'support for', + 'tinmorry', + 'universal', + 'value', + 'yumi', + 'ultra', + + 'ams', + 'easy', + 'for', + 'fil', + 'ment', + 'orca', + 'rapid', + 'support', + ]; + + protected array $normalize = [ + '-aero' => ' Aero', + '-silk' => ' Silk', + '-wood' => ' Wood', + '-metal' => ' Metal', + '-dual' => ' Dual', + + 'aero' => 'Aero', + 'silk' => 'Silk', + 'wood' => 'Wood', + 'carbone' => 'CF', + 'metal' => 'Metal', + 'dual' => 'Dual', + + 'HF Speed' => 'HF', + 'S Nozzle' => 'S', + + '-HS' => ' HS', + '-HF' => ' HF', + '-CF' => ' CF', + ]; + + protected array $remove = [ + 'fdm_filament_', + 'fdm__filament_', + '_common', + + 'fdm filament ', + 'other', + 'punk', + ]; + + protected array $blacklist = [ + 'common', + ]; public function import(): void { Map::query() + ->with('vendor') ->where('type', SourceType::Filament) - ->each(fn (Map $map) => $this->store($map)); + ->each(fn (Map $map) => $this->store($map->vendor, $map)); } - protected function store(Map $map): void + protected function store(Vendor $vendor, Map $map): void { - if (! $value = $this->detect($map->key)) { + if (! $value = $this->detect($vendor->title, $map->key, $map->path)) { return; } $this->filamentType($value); } - protected function detect(string $value): ?string + protected function detect(string $vendor, string $filament, string $path): string { - if (! preg_match($this->pattern, $value, $matches)) { - return null; - } + return Str::of($filament) + ->when( + Str::substrCount($path, '/') === 2, + fn (Stringable $str) => $str->remove([$vendor, Str::beforeLast($path, '/')], false), + function (Stringable $str) use ($vendor, $path) { + $items = explode('/', $path); + + return $str->remove([$vendor, $items[2]], false); + }, + ) + ->pipe(fn (Stringable $str): string => $str + ->explode(' ') + ->reject(fn (string $word) => in_array(Str::lower($word), $this->reservedWords, true)) + ->implode(' ') + ) + ->remove($this->remove, false) + ->replace(['High Speed', 'Hyper', '@HS', ' HS'], ' HS', false) + ->replace(['High Flow', 'High-Flow', '@HF', ' HF'], ' HF', false) + ->replace('plus', '+', false) + ->replace([' -', ' +'], ['-', '+']) + ->before('@') + ->replaceMatches('/(\(.+\))/', '') + ->replaceMatches('/\d+\.\d+\s?_?nozzle/', '') + ->replaceMatches('/(SV\d+|Zero)/', '') + ->replaceMatches('/(VXL\d+\s[a-zA-Z0-9]+)/', '') + ->replaceMatches('/(Grauts\s[a-zA-Z0-9]+)/', '') + ->replaceMatches('/(FLEX\d+)/', '') + ->replaceMatches('/^\s*([STJ]\d+)/', '') + ->replaceMatches('/(\d+\.\d+[m]*)/', '') + ->replace(['/', '+', '_'], ['-', '+ ', ' ']) + ->squish() + ->trim('-_ ') + ->when( + fn (Stringable $str) => $str->doesntContain(' '), + fn (Stringable $str) => $str->upper(), + function (Stringable $str) { + $values = $str->explode(' '); + + if ($values->count() > 2) { + return $str; + } + + $first = $values->first(); + $last = $values->last(); + + $firstLength = Str::length($first); + $lastLength = Str::length($last); + + $firstIsType = in_array($firstLength, [3, 4], true); + $lastIsType = in_array($lastLength, [3, 4], true); + + if ($firstIsType) { + $first = Str::upper($first); + } + + if ($lastIsType) { + $last = Str::upper($last); + } - return Str::of($matches[0]) - ->replace('_', '-', false) + return new Stringable($first . ' ' . $last); + } + ) + ->replace(array_keys($this->normalize), array_values($this->normalize), false) + ->when( + fn (Stringable $str) => in_array($str->lower()->value(), $this->blacklist, true), + fn () => new Stringable + ) + ->replaceMatches('/^E([A-Z]{3,})/', 'e$1') + ->replaceMatches('/((?:[A-Z][a-z]+\s+){1,2})([A-Z+]{2,})/', '$2 $1') ->squish() - ->trim('-') + ->ltrim('+ ') + ->when( + fn (Stringable $str) => $str->isMatch('/[A-Z\d]{2,}\sCF/'), + fn (Stringable $str) => $str->replace(' ', '-'), + ) ->toString(); } } diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeServiceTest.php index 57a196f..cbf66da 100644 --- a/tests/Unit/FilamentTypeServiceTest.php +++ b/tests/Unit/FilamentTypeServiceTest.php @@ -11,100 +11,106 @@ class FilamentTypeServiceTest extends TestCase { #[DataProvider('detectCases')] - public function testDetect(string $input, ?string $expected): void + public function testDetect(?string $vendor, string $filament, ?string $expected): void { $service = new class extends FilamentTypeService { - public function exposedDetect(string $value): ?string + public function exposedDetect(?string $vendor, string $filament): ?string { - return $this->detect($value); + $path = $vendor . '/filament/' . $filament . '.json'; + + return $this->detect($vendor, $filament, $path); } }; - $this->assertSame($expected, $service->exposedDetect($input)); + $this->assertSame($expected, $service->exposedDetect($vendor, $filament)); } public static function detectCases(): array { return [ - ['Afinia ABS', 'ABS'], - ['Afinia ABS+', 'ABS+'], - ['Afinia ABS@HS', 'ABS-HS'], - ['Afinia PLA@HS', 'PLA-HS'], - ['Afinia TPU', 'TPU'], - ['Afinia Value ABS', 'ABS'], - ['Afinia Value ABS@HS', 'ABS-HS'], - ['Afinia Value PLA HS', 'PLA-HS'], - ['Afinia Value PLA-HS', 'PLA-HS'], - ['Afinia Value PLA@HS', 'PLA-HS'], - - ['Anker Generic ASA 0.2 nozzle', 'ASA'], - ['Anker Generic ASA 0.25 nozzle', 'ASA'], - ['Anker Generic ASA', 'ASA'], - ['Anker Generic PA-CF @base', 'PA-CF'], - ['Anker Generic PC @base', 'PC'], - ['Anker Generic PETG @base', 'PETG'], - ['Anker Generic PETG-CF @base', 'PETG-CF'], - ['Anker Generic PLA 0.25 nozzle', 'PLA'], - ['Anker Generic PLA Silk 0.2 nozzle', 'PLA Silk'], - ['Anker Generic PLA Silk 0.25 nozzle', 'PLA Silk'], - ['Anker Generic PLA Silk', 'PLA Silk'], - - ['Anycubic ABS @Anycubic Kobra S1 0.4 nozzle', 'ABS'], - ['Anycubic PLA @Anycubic Kobra S1 0.4 nozzle', 'PLA'], - ['Anycubic PLA High Speed @Anycubic Kobra S1 0.4 nozzle', 'PLA-HS'], - ['Anycubic PLA Silk @Anycubic Kobra S1 0.4 nozzle', 'PLA Silk'], - ['Anycubic PLA+ @Anycubic Kobra S1 0.4 nozzle', 'PLA+'], - - ['Artillery ASA @Artillery M1 Pro 0.8 nozzle', 'ASA'], - ['Artillery Generic PETG', 'PETG'], - ['Artillery Generic PLA', 'PLA'], - ['Artillery Generic PLA-CF', 'PLA-CF'], - ['Artillery PET @Artillery M1 Pro 0.2 nozzle', 'PET'], - ['Artillery PLA Basic @Artillery M1 Pro 0.2 nozzle', 'PLA'], - ['Artillery PLA Basic @Artillery M1 Pro 0.4 nozzle', 'PLA'], - - ['Bambu ABS-GF @base', 'ABS-GF'], - ['Bambu ASA-Aero @BBL H2D', 'ASA Aero'], - ['Bambu PET-CF @System', 'PET-CF'], - ['Bambu Support for ABS @base', 'ABS'], - - ['COEX PCTG PRIME @BBL A1M 0.8 nozzle', 'PCTG Prime'], - ['COEX PETG @BBL A1M 0.4 nozzle', 'PETG'], - ['COEX PLA @BBL X1C 0.2 nozzle', 'PLA'], - - ['Creality Generic PLA High Speed @Ender-3V3-all', 'PLA-HS'], - - ['eSUN ePLA-LW @System', 'ePLA-LW'], - - ['fdm_filament_abs', 'ABS'], - ['fdm_filament_bvoh', 'BVOH'], - ['fdm_filament_common', null], - ['fdm_filament_eva', 'EVA'], - ['fdm_filament_hips', 'HIPS'], - ['fdm_filament_pla', 'PLA'], - ['fdm_filament_tpu', 'TPU'], - - ['Fiberon PA12-CF @base', 'PA12-CF'], - - ['Generic BVOH @base', 'BVOH'], - ['Generic EVA @base', 'EVA'], - ['Generic HIPS @base', 'HIPS'], - ['Generic PPA-CF @BBL H2D', 'PPA-CF'], - - ['Panchroma PLA Silk @base', 'PLA Silk'], - ['Panchroma PLA Stain @base', 'PLA Stain'], - ['Panchroma PLA Starlight @base', 'PLA Starlight'], - ['Panchroma PLA Temp Shift @base', 'PLA Temp Shift'], - ['Panchroma PLA Translucent @base', 'PLA Translucent'], - - ['eSUN PLA+ @base', 'PLA+'], - ['SUNLU PLA Marble @base', 'PLA Marble'], - ['SUNLU PLA Matte @base', 'PLA Matte'], - ['SUNLU PLA+ 2.0 @base', 'PLA+'], - ['SUNLU Silk PLA+ @base', 'PLA+ Silk'], - ['SUNLU Wood PLA @base', 'PLA Wood'], - - ['Unknown profile string', null], + ['Afinia', 'Afinia ABS', 'ABS'], + ['Afinia', 'Afinia ABS+', 'ABS+'], + ['Afinia', 'Afinia ABS@HS', 'ABS HS'], + ['Afinia', 'Afinia PLA@HS', 'PLA HS'], + ['Afinia', 'Afinia TPU', 'TPU'], + ['Afinia', 'Afinia Value ABS', 'ABS'], + ['Afinia', 'Afinia Value ABS@HS', 'ABS HS'], + ['Afinia', 'Afinia Value PLA HS', 'PLA HS'], + ['Afinia', 'Afinia Value PLA-HS', 'PLA HS'], + ['Afinia', 'Afinia Value PLA@HS', 'PLA HS'], + + ['Anker', 'Anker Generic ASA 0.2 nozzle', 'ASA'], + ['Anker', 'Anker Generic ASA 0.25 nozzle', 'ASA'], + ['Anker', 'Anker Generic ASA', 'ASA'], + ['Anker', 'Anker Generic PA-CF @base', 'PA-CF'], + ['Anker', 'Anker Generic PC @base', 'PC'], + ['Anker', 'Anker Generic PETG @base', 'PETG'], + ['Anker', 'Anker Generic PETG-CF @base', 'PETG-CF'], + ['Anker', 'Anker Generic PLA 0.25 nozzle', 'PLA'], + ['Anker', 'Anker Generic PLA Silk 0.2 nozzle', 'PLA Silk'], + ['Anker', 'Anker Generic PLA Silk 0.25 nozzle', 'PLA Silk'], + ['Anker', 'Anker Generic PLA Silk', 'PLA Silk'], + + ['Anycubic', 'Anycubic ABS @Anycubic Kobra S1 0.4 nozzle', 'ABS'], + ['Anycubic', 'Anycubic PLA @Anycubic Kobra S1 0.4 nozzle', 'PLA'], + ['Anycubic', 'Anycubic PLA High Speed @Anycubic Kobra S1 0.4 nozzle', 'PLA HS'], + ['Anycubic', 'Anycubic PLA Silk @Anycubic Kobra S1 0.4 nozzle', 'PLA Silk'], + ['Anycubic', 'Anycubic PLA+ @Anycubic Kobra S1 0.4 nozzle', 'PLA+'], + + ['Artillery', 'Artillery ASA @Artillery M1 Pro 0.8 nozzle', 'ASA'], + ['Artillery', 'Artillery Generic PETG', 'PETG'], + ['Artillery', 'Artillery Generic PLA', 'PLA'], + ['Artillery', 'Artillery Generic PLA-CF', 'PLA-CF'], + ['Artillery', 'Artillery PET @Artillery M1 Pro 0.2 nozzle', 'PET'], + ['Artillery', 'Artillery PLA Basic @Artillery M1 Pro 0.2 nozzle', 'PLA'], + ['Artillery', 'Artillery PLA Basic @Artillery M1 Pro 0.4 nozzle', 'PLA'], + + ['Bambulab', 'Bambu ABS-GF @base', 'ABS-GF'], + ['Bambulab', 'Bambu ASA-Aero @BBL H2D', 'ASA Aero'], + ['Bambulab', 'Bambu PET-CF @System', 'PET-CF'], + ['Bambulab', 'Bambu Support for ABS @base', 'ABS'], + + ['COEX', 'COEX PCTG PRIME @BBL A1M 0.8 nozzle', 'PCTG'], + ['COEX', 'COEX PETG @BBL A1M 0.4 nozzle', 'PETG'], + ['COEX', 'COEX PLA @BBL X1C 0.2 nozzle', 'PLA'], + + ['Creality', 'Creality Generic PLA High Speed @Ender-3V3-all', 'PLA HS'], + + ['eSUN', 'eSUN ePLA-LW @System', 'ePLA-LW'], + + ['Afinia', 'fdm_filament_abs', 'ABS'], + ['Anker', 'fdm_filament_bvoh', 'BVOH'], + ['Anycubic', 'fdm_filament_common', ''], + ['Artillery', 'fdm_filament_eva', 'EVA'], + ['Bambu', 'fdm_filament_hips', 'HIPS'], + ['Creality', 'fdm_filament_tpu', 'TPU'], + ['COEX', 'fdm_filament_pla', 'PLA'], + + ['Fiberon', 'Fiberon PA12-CF @base', 'PA12-CF'], + + ['Afinia', 'Generic BVOH @base', 'BVOH'], + ['Anker', 'Generic EVA @base', 'EVA'], + ['Anycubic', 'Generic HIPS @base', 'HIPS'], + ['Artillery', 'Generic PPA-CF @BBL H2D', 'PPA-CF'], + + ['Panchroma', 'Panchroma PLA Silk @base', 'PLA Silk'], + ['Panchroma', 'Panchroma PLA Stain @base', 'PLA Stain'], + ['Panchroma', 'Panchroma PLA Starlight @base', 'PLA Starlight'], + ['Panchroma', 'Panchroma PLA Temp Shift @base', 'PLA Temp Shift'], + ['Panchroma', 'Panchroma Temp Shift PLA @base', 'PLA Temp Shift'], + ['Panchroma', 'Panchroma PLA Translucent @base', 'PLA Translucent'], + + ['eSUN', 'eSUN PLA+ @base', 'PLA+'], + ['SUNLU', 'SUNLU PLA Marble @base', 'PLA Marble'], + ['SUNLU', 'SUNLU PLA Matte @base', 'PLA Matte'], + ['SUNLU', 'SUNLU PLA+ 2.0 @base', 'PLA+'], + ['SUNLU', 'SUNLU Silk PLA+ @base', 'PLA+ Silk'], + ['SUNLU', 'SUNLU Wood PLA @base', 'PLA Wood'], + + ['SUNLU', 'SUNLU Wood PLA Other @base', 'PLA Wood'], + ['SUNLU', 'SUNLU EASY PLA @base', 'PLA'], + + ['Foo', 'Unknown profile string', 'Unknown profile string'], ]; } } From 2ede6c98f4fd96684125973c61789f078bee653b Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 22:47:21 +0300 Subject: [PATCH 08/22] Add `MachineService` for machine imports, update `MapService` with `profile` support, and adjust related commands, models, and migrations. --- .../Commands/OrcaSlicer/ImportCommand.php | 10 ++-- app/Models/Map.php | 2 + app/Services/OrcaSlicer/MachineService.php | 46 +++++++++++++++++++ app/Services/OrcaSlicer/MapService.php | 3 +- .../2026_01_03_111601_create_maps_table.php | 1 + 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 app/Services/OrcaSlicer/MachineService.php diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php index 2b8f764..0ea2475 100644 --- a/app/Console/Commands/OrcaSlicer/ImportCommand.php +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -7,6 +7,7 @@ use App\Models\FilamentType; use App\Services\OrcaSlicer\DownloadService; use App\Services\OrcaSlicer\FilamentTypeService; +use App\Services\OrcaSlicer\MachineService; use App\Services\OrcaSlicer\MapService; use App\Services\OrcaSlicer\NozzleService; use Illuminate\Console\Command; @@ -20,16 +21,17 @@ class ImportCommand extends Command public function handle( DownloadService $download, MapService $map, + MachineService $machine, NozzleService $nozzle, - FilamentTypeService $filamentType + FilamentTypeService $filamentType, ): void { //$this->components->task('Clean up', fn () => $download->cleanup()); //$this->components->task('Download', fn () => $download->download()); //$this->components->task('Extract', fn () => $download->extract()); //$this->components->task('Release', fn () => $download->release()); - //$this->components->task('Import map', fn () => $map->import()); + $this->components->task('Import map', fn () => $map->import()); + $this->components->task('Import machines', fn () => $machine->import()); //$this->components->task('Import nozzles', fn () => $nozzle->import()); - FilamentType::truncate(); - $this->components->task('Import filament types', fn () => $filamentType->import()); + //$this->components->task('Import filament types', fn () => $filamentType->import()); } } diff --git a/app/Models/Map.php b/app/Models/Map.php index 4f859ee..69cc1ca 100644 --- a/app/Models/Map.php +++ b/app/Models/Map.php @@ -12,6 +12,8 @@ class Map extends Model { protected $fillable = [ 'type', + 'profile', + 'key', 'path', ]; diff --git a/app/Services/OrcaSlicer/MachineService.php b/app/Services/OrcaSlicer/MachineService.php new file mode 100644 index 0000000..c413c41 --- /dev/null +++ b/app/Services/OrcaSlicer/MachineService.php @@ -0,0 +1,46 @@ +with('vendor') + ->where('type', SourceType::Machine) + ->each(fn (Map $map) => $this->store($map)); + } + + protected function store(Map $map): void + { + $map->vendor->machines()->updateOrCreate([ + 'title' => $map->vendor->title, + ], [ + 'cover' => $this->url($map->profile, $map->key), + ]); + } + + protected function url(string $vendor, string $machine): string + { + return str_replace([ + '{vendor}', + '{machine}', + ], [ + $this->encode($vendor), + $this->encode($machine), + ], $this->url); + } + + protected function encode(string $value): string + { + return rawurlencode($value); + } +} diff --git a/app/Services/OrcaSlicer/MapService.php b/app/Services/OrcaSlicer/MapService.php index 19aea7c..f24e90e 100644 --- a/app/Services/OrcaSlicer/MapService.php +++ b/app/Services/OrcaSlicer/MapService.php @@ -95,7 +95,8 @@ protected function store(SourceType $type, Vendor $vendor, string $profile, stri 'type' => $type, 'key' => $name, ], [ - 'path' => $profile . '/' . $path, + 'profile' => $profile, + 'path' => $profile . '/' . $path, ]); } diff --git a/database/migrations/2026_01_03_111601_create_maps_table.php b/database/migrations/2026_01_03_111601_create_maps_table.php index d61026e..526b315 100644 --- a/database/migrations/2026_01_03_111601_create_maps_table.php +++ b/database/migrations/2026_01_03_111601_create_maps_table.php @@ -16,6 +16,7 @@ public function up(): void $table->string('type'); + $table->string('profile'); $table->string('key'); $table->string('path'); From 007b3c9845fe9b47038258118801c30c4eb8c999 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 22:48:48 +0300 Subject: [PATCH 09/22] Remove deprecated `ImportService` from OrcaSlicer implementation. --- app/Services/OrcaSlicer/ImportService.php | 207 ---------------------- 1 file changed, 207 deletions(-) delete mode 100644 app/Services/OrcaSlicer/ImportService.php diff --git a/app/Services/OrcaSlicer/ImportService.php b/app/Services/OrcaSlicer/ImportService.php deleted file mode 100644 index 6094725..0000000 --- a/app/Services/OrcaSlicer/ImportService.php +++ /dev/null @@ -1,207 +0,0 @@ -profileFiles() as $profile) { - if ($this->needSkip($profile)) { - continue; - } - - $data = $this->load( - $profile->getRealPath(), - $this->profileDirectory($profile), - $this->profileName($profile) - ); - - $vendor = $this->vendor($data->title); - - $this->machines($vendor, $data->machines); - $this->filaments($vendor, $data->filaments); - } - } - - protected function machines(Vendor $vendor, Collection $items): void - { - $items->each(function (MachineData $machine) use ($vendor) { - $vendor->machines()->updateOrCreate([ - 'title' => $machine->title, - ], $machine->toArray()); - - $this->nozzles($machine->nozzleDiameters); - }); - } - - protected function filaments(Vendor $vendor, Collection $items): void - { - $items->each(function (FilamentData $filament) use ($vendor) { - $type = $this->filamentType($filament->title); - - $model = $vendor->filaments()->updateOrCreate([ - 'filament_type_id' => $type->id, - ], $filament->toArray()); - - // TODO: Добавить - //$machine = $this->machine($filament->title); - // - //if (! $machine) { - // dd( - // $filament->toArray() - // ); - // - // return; - //} - // - //$this->userFilament($this->commonUser(), $model, $filament); - }); - } - - protected function userFilament(User $user, Filament $filament, FilamentData $data): void - { - $user->filaments()->attach($filament, $data->toArray()); - } - - protected function filamentType(string $name): FilamentType - { - $cast = (new FilamentTypeTitleCast)->set(new FilamentType, '', $name, []); - - if ($model = $this->filamentTypes[$cast] ?? null) { - return $model; - } - - $model = FilamentType::firstWhere('title', $cast) - ?: FilamentType::create(['title' => $name]); - - return $this->filamentTypes[$cast] = $model; - } - - protected function machine(string $name): ?Machine - { - $name = Str::afterLast($name, '@'); - - return $this->machines[$name] ??= Machine::firstWhere('title', $name); - } - - protected function nozzles(array $diameters): void - { - foreach ($diameters as $diameter) { - Nozzle::firstOrCreate(['title' => $diameter]); - } - } - - protected function vendor(string $name): Vendor - { - return Vendor::firstOrCreate(['title' => $name]); - } - - protected function load(string $path, string $directory, string $profile): ProfileData - { - return ProfileData::from([ - 'data' => json_decode(file_get_contents($path), true), - 'meta' => [ - 'directory' => $directory, - 'profile' => $profile, - ], - ]); - } - - protected function needSkip(SplFileInfo $file): bool - { - if ($file->getExtension() !== 'json') { - return true; - } - - return in_array($file->getFilename(), $this->exceptFiles, true); - } - - /** - * @return SplFileInfo[] - */ - protected function profileFiles(): array - { - return File::files( - $this->getMachinesPath() - ); - } - - protected function profileDirectory(SplFileInfo $profile): string - { - $path = $profile->getRealPath(); - $extension = $profile->getExtension(); - - return Str::beforeLast($path, ".$extension"); - } - - protected function profileName(SplFileInfo $profile): string - { - $filename = $profile->getFilename(); - $extension = $profile->getExtension(); - - return Str::beforeLast($filename, ".$extension"); - } - - protected function commonUser(): User - { - return $this->user ??= User::firstWhere('email', config('user.common.email')); - } - - protected function getMachinesPath(): string - { - return $this->path('resources/profiles'); - } - - protected function path(string $filename): string - { - return $this->storage->path( - $this->directory . DIRECTORY_SEPARATOR . $filename - ); - } -} From 4e37f36b07b9f35cb79d6a502477a882f4365801 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 22:54:40 +0300 Subject: [PATCH 10/22] Refactor OrcaSlicer import commands: update filament and nozzle import logic, enhance `MachineService` with key-based `title` parsing, and integrate filament-vendor association in `FilamentTypeService`. --- app/Console/Commands/OrcaSlicer/ImportCommand.php | 7 +++---- app/Services/OrcaSlicer/FilamentTypeService.php | 8 +++++++- app/Services/OrcaSlicer/MachineService.php | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php index 0ea2475..4d63896 100644 --- a/app/Console/Commands/OrcaSlicer/ImportCommand.php +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -4,7 +4,6 @@ namespace App\Console\Commands\OrcaSlicer; -use App\Models\FilamentType; use App\Services\OrcaSlicer\DownloadService; use App\Services\OrcaSlicer\FilamentTypeService; use App\Services\OrcaSlicer\MachineService; @@ -23,7 +22,7 @@ public function handle( MapService $map, MachineService $machine, NozzleService $nozzle, - FilamentTypeService $filamentType, + FilamentTypeService $filament, ): void { //$this->components->task('Clean up', fn () => $download->cleanup()); //$this->components->task('Download', fn () => $download->download()); @@ -31,7 +30,7 @@ public function handle( //$this->components->task('Release', fn () => $download->release()); $this->components->task('Import map', fn () => $map->import()); $this->components->task('Import machines', fn () => $machine->import()); - //$this->components->task('Import nozzles', fn () => $nozzle->import()); - //$this->components->task('Import filament types', fn () => $filamentType->import()); + $this->components->task('Import nozzles', fn () => $nozzle->import()); + $this->components->task('Import filaments', fn () => $filament->import()); } } diff --git a/app/Services/OrcaSlicer/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php index 62b063a..ceb133a 100644 --- a/app/Services/OrcaSlicer/FilamentTypeService.php +++ b/app/Services/OrcaSlicer/FilamentTypeService.php @@ -6,6 +6,7 @@ use App\Concerns\WithFilaments; use App\Enums\SourceType; +use App\Models\Filament; use App\Models\Map; use App\Models\Vendor; use Illuminate\Support\Str; @@ -116,7 +117,12 @@ protected function store(Vendor $vendor, Map $map): void return; } - $this->filamentType($value); + $type = $this->filamentType($value); + + Filament::updateOrCreate([ + 'vendor_id' => $vendor->id, + 'filament_type_id' => $type->id, + ]); } protected function detect(string $vendor, string $filament, string $path): string diff --git a/app/Services/OrcaSlicer/MachineService.php b/app/Services/OrcaSlicer/MachineService.php index c413c41..cd85433 100644 --- a/app/Services/OrcaSlicer/MachineService.php +++ b/app/Services/OrcaSlicer/MachineService.php @@ -6,6 +6,7 @@ use App\Enums\SourceType; use App\Models\Map; +use Illuminate\Support\Str; class MachineService { @@ -22,7 +23,7 @@ public function import(): void protected function store(Map $map): void { $map->vendor->machines()->updateOrCreate([ - 'title' => $map->vendor->title, + 'title' => Str::of($map->key)->after($map->vendor->title)->trim()->value(), ], [ 'cover' => $this->url($map->profile, $map->key), ]); From 273a9bf7842eeff386dff8bf000f7f8d8f90556e Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 22:59:12 +0300 Subject: [PATCH 11/22] Remove `2026_01_02_142124_create_colors` operation: clean up unused migration. --- .../2026_01_02_142124_create_colors.php | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 operations/2026_01_02_142124_create_colors.php diff --git a/operations/2026_01_02_142124_create_colors.php b/operations/2026_01_02_142124_create_colors.php deleted file mode 100644 index 4e70e56..0000000 --- a/operations/2026_01_02_142124_create_colors.php +++ /dev/null @@ -1,44 +0,0 @@ -values as $item) { - Color::updateOrCreate( - ['title' => $item[0]], - ['hex' => $item[1]] - ); - } - } -}; From aeccef9c5de5e39ec53a6668e930c5bd2564fff4 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 23:07:28 +0300 Subject: [PATCH 12/22] Add `WithColor` trait for color handling and lazy initialization --- app/Concerns/WithColor.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/Concerns/WithColor.php diff --git a/app/Concerns/WithColor.php b/app/Concerns/WithColor.php new file mode 100644 index 0000000..51994b2 --- /dev/null +++ b/app/Concerns/WithColor.php @@ -0,0 +1,17 @@ +colors[$name] ??= Color::firstOrCreate(['title' => $name]); + } +} From 376ac529d360ac5551238e3a25677ca9b5d4292f Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 23:07:33 +0300 Subject: [PATCH 13/22] Add lazy initialization for filaments in `WithFilaments` trait, integrate vendor and filament type association. --- app/Concerns/WithFilaments.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Concerns/WithFilaments.php b/app/Concerns/WithFilaments.php index 2cf3683..176892f 100644 --- a/app/Concerns/WithFilaments.php +++ b/app/Concerns/WithFilaments.php @@ -4,14 +4,27 @@ namespace App\Concerns; +use App\Models\Filament; use App\Models\FilamentType; +use App\Models\Vendor; trait WithFilaments { protected array $filamentTypes = []; + protected array $filaments = []; + protected function filamentType(string $value): FilamentType { return $this->filamentTypes[$value] ??= FilamentType::firstOrCreate(['title' => $value]); } + + protected function filament(Vendor $vendor, FilamentType $filamentType): Filament + { + $key = $vendor->id . '-' . $filamentType->id; + + return $this->filaments[$key] ??= $vendor->filaments()->updateOrCreate([ + 'filament_type_id' => $filamentType->id, + ]); + } } From f4f6c9a7950568d627f8cea8046c28a0875ab81b Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 23:07:37 +0300 Subject: [PATCH 14/22] Add `external_id` to `Filament` model's fillable attributes for improved data handling. --- app/Models/Filament.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/Filament.php b/app/Models/Filament.php index 962c7c9..0551ed5 100644 --- a/app/Models/Filament.php +++ b/app/Models/Filament.php @@ -16,6 +16,7 @@ class Filament extends Model protected $fillable = [ 'vendor_id', 'filament_type_id', + 'external_id', ]; public function vendor(): Relation From f473d521f64c6a4d2b4c6a8dce268c1daca7dc35 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 23:07:41 +0300 Subject: [PATCH 15/22] Refactor `store_user_settings` operation: integrate `WithVendor`, `WithFilaments`, and `WithColor` traits; remove deprecated methods and attributes. --- .../2026_01_02_195202_store_user_settings.php | 43 +++---------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/operations/2026_01_02_195202_store_user_settings.php b/operations/2026_01_02_195202_store_user_settings.php index 78d2b03..85c516d 100644 --- a/operations/2026_01_02_195202_store_user_settings.php +++ b/operations/2026_01_02_195202_store_user_settings.php @@ -2,15 +2,20 @@ declare(strict_types=1); +use App\Concerns\WithColor; +use App\Concerns\WithFilaments; +use App\Concerns\WithVendor; use App\Models\Color; use App\Models\Filament; -use App\Models\FilamentType; use App\Models\Machine; use App\Models\User; -use App\Models\Vendor; use DragonCode\LaravelDeployOperations\Operation; return new class extends Operation { + use WithVendor; + use WithFilaments; + use WithColor; + protected array $items = [ [ 'vendor' => 'Bambulab', @@ -172,12 +177,6 @@ protected ?Machine $machine; - protected array $filaments = []; - - protected array $filamentTypes = []; - - protected array $vendors = []; - protected array $colors = []; public function __invoke(): void @@ -226,32 +225,4 @@ protected function machine(): Machine { return $this->machine ??= Machine::firstWhere('slug', 'k1-max'); } - - protected function vendor(string $name): Vendor - { - return $this->vendors[$name] ??= Vendor::firstOrCreate(['title' => $name]); - } - - protected function filament(Vendor $vendor, FilamentType $type): Filament - { - $key = $vendor->id . '-' . $type->id; - - return $this->filaments[$key] ??= Filament::firstOrCreate([ - 'vendor_id' => $vendor->id, - 'filament_type_id' => $type->id, - ], [ - 'external_id' => $key, - 'title' => $vendor->title . ' ' . $type->title, - ]); - } - - protected function filamentType(string $name): FilamentType - { - return $this->filamentTypes[$name] ??= FilamentType::firstOrCreate(['title' => $name]); - } - - protected function color(string $name): Color - { - return $this->colors[$name] ??= Color::firstWhere('title', $name); - } }; From 55272eb2452cf6ebee9bec1c3c820111f0a00826 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 3 Jan 2026 23:15:07 +0300 Subject: [PATCH 16/22] Extend `WithColor` trait: add `colorMap` support with default hex values and update `color` method for hex initialization. --- app/Concerns/WithColor.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Concerns/WithColor.php b/app/Concerns/WithColor.php index 51994b2..81b8dd2 100644 --- a/app/Concerns/WithColor.php +++ b/app/Concerns/WithColor.php @@ -10,8 +10,20 @@ trait WithColor { protected array $colors = []; + protected array $colorMap = [ + 'Yellow' => '#FFFF00', + 'Green' => '#00FF00', + 'Blue' => '#0000FF', + 'Grey' => '#808080', + 'White' => '#FFFFFF', + 'Gold' => '#FFCF40', + ]; + protected function color(string $name): Color { - return $this->colors[$name] ??= Color::firstOrCreate(['title' => $name]); + return $this->colors[$name] ??= Color::firstOrCreate([ + 'title' => $name, + 'hex' => $this->colorMap[$name] ?? '#000000', + ]); } } From a1a329b04048d78235cc1f6176a7fba6e6fc68c2 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 00:10:16 +0300 Subject: [PATCH 17/22] Add `HomeService` for homepage data handling and refactor `HomeController` to use service methods; update `welcome` page to include dynamic filters. --- app/Http/Controllers/HomeController.php | 21 +--- app/Models/Color.php | 6 + app/Models/Filament.php | 6 + app/Models/FilamentType.php | 13 ++ app/Models/Machine.php | 8 +- app/Models/UserFilament.php | 8 +- app/Services/Pages/HomeService.php | 55 ++++++++ resources/js/pages/welcome.tsx | 159 +++++++++++++++--------- 8 files changed, 199 insertions(+), 77 deletions(-) create mode 100644 app/Services/Pages/HomeService.php diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 144194d..be94146 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -4,27 +4,18 @@ namespace App\Http\Controllers; -use App\Models\UserFilament; +use App\Services\Pages\HomeService; use Inertia\Inertia; class HomeController { - public function __invoke() + public function __invoke(HomeService $home) { - $settings = UserFilament::query() - ->with([ - 'machine.vendor', - 'filament' => ['vendor', 'type'], - 'color', - ]) - ->orderBy('machine_id') - ->orderBy('filament_id') - ->orderBy('color_id') - ->orderBy('id') - ->get(); - return Inertia::render('welcome', [ - 'settings' => $settings, + 'userFilaments' => $home->userFilaments(), + 'machines' => $home->machines(), + 'filamentTypes' => $home->filamentTypes(), + 'colors' => $home->colors(), ]); } } diff --git a/app/Models/Color.php b/app/Models/Color.php index 6d4ee21..f322ba3 100644 --- a/app/Models/Color.php +++ b/app/Models/Color.php @@ -7,6 +7,7 @@ use App\Casts\HexCast; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class Color extends Model @@ -24,4 +25,9 @@ protected function casts(): array 'hex' => HexCast::class, ]; } + + public function userFilament(): HasMany + { + return $this->hasMany(UserFilament::class); + } } diff --git a/app/Models/Filament.php b/app/Models/Filament.php index 0551ed5..acb9b6e 100644 --- a/app/Models/Filament.php +++ b/app/Models/Filament.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletes; @@ -28,4 +29,9 @@ public function type(): Relation { return $this->belongsTo(FilamentType::class, 'filament_type_id', 'id'); } + + public function userFilament(): HasMany + { + return $this->hasMany(UserFilament::class); + } } diff --git a/app/Models/FilamentType.php b/app/Models/FilamentType.php index 77bdf7d..7536f0d 100644 --- a/app/Models/FilamentType.php +++ b/app/Models/FilamentType.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\SoftDeletes; class FilamentType extends Model @@ -15,4 +16,16 @@ class FilamentType extends Model protected $fillable = [ 'title', ]; + + public function userFilaments(): HasManyThrough + { + return $this->hasManyThrough( + UserFilament::class, + Filament::class, + 'filament_type_id', + 'filament_id', + 'id', + 'id' + ); + } } diff --git a/app/Models/Machine.php b/app/Models/Machine.php index 4c5da5e..8f18849 100644 --- a/app/Models/Machine.php +++ b/app/Models/Machine.php @@ -8,6 +8,7 @@ use App\Events\SluggableEvent; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletes; @@ -34,6 +35,11 @@ protected function casts(): array public function vendor(): Relation { - return $this->belongsTo(Vendor::class); + return $this->belongsTo(Vendor::class, 'vendor_id', 'id'); + } + + public function userFilament(): HasMany + { + return $this->hasMany(UserFilament::class); } } diff --git a/app/Models/UserFilament.php b/app/Models/UserFilament.php index 32ad0e4..578f8c3 100644 --- a/app/Models/UserFilament.php +++ b/app/Models/UserFilament.php @@ -43,21 +43,21 @@ protected function casts(): array public function user(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id', 'id'); } public function machine(): BelongsTo { - return $this->belongsTo(Machine::class); + return $this->belongsTo(Machine::class, 'machine_id', 'id'); } public function filament(): BelongsTo { - return $this->belongsTo(Filament::class); + return $this->belongsTo(Filament::class, 'filament_id', 'id'); } public function color(): BelongsTo { - return $this->belongsTo(Color::class); + return $this->belongsTo(Color::class, 'color_id', 'id'); } } diff --git a/app/Services/Pages/HomeService.php b/app/Services/Pages/HomeService.php new file mode 100644 index 0000000..b3d7193 --- /dev/null +++ b/app/Services/Pages/HomeService.php @@ -0,0 +1,55 @@ +whereHas('userFilament') + ->with('vendor') + ->orderBy('vendor_id') + ->orderBy('title') + ->get(['id', 'title', 'vendor_id']); + } + + public function filamentTypes(): Collection + { + return FilamentType::query() + ->whereHas('userFilaments') + ->orderBy('title') + ->get(['id', 'title']); + } + + public function colors(): Collection + { + return Color::query() + ->whereHas('userFilament') + ->orderBy('title') + ->get(['title', 'hex']); + } + + public function userFilaments(): Collection + { + return UserFilament::query() + ->with([ + 'machine.vendor', + 'filament' => ['vendor', 'type'], + 'color', + ]) + ->orderBy('machine_id') + ->orderBy('filament_id') + ->orderBy('color_id') + ->orderBy('id') + ->get(); + } +} diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index b6cacea..ebc64fe 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,68 +1,113 @@ import { Head } from '@inertiajs/react'; -export default function Welcome({ settings }) { +export default function Welcome({ userFilaments, machines, filamentTypes, colors }) { return ( <>
- - - - - - - - - - - - - - { settings.map((setting) => ( - - - - - - - - - - )) } - -
- Printer - - Filament - - Color - - Pressure Advance - - Flow Ratio - - Max Volumetric Speed - - Nozzle Temperature -
- { setting.machine.vendor.title }  - { setting.machine.title } - - { setting.filament.vendor.title }  - { setting.filament.type.title }
-
- { setting.color.title } - - { setting.pressure_advance } - - { setting.filament_flow_ratio } - - { setting.filament_max_volumetric_speed } - - { setting.nozzle_temperature } -
+
+ + + + + + + + + + + + + + + + + +
PrinterFilamentColor
+ + + + + +
+ + + + + + + + + + + + + + + { userFilaments.map((item) => ( + + + + + + + + + + )) } + +
+ Printer + + Filament + + Color + + Pressure Advance + + Flow Ratio + + Max Volumetric Speed + + Nozzle Temperature +
+ { item.machine.vendor.title }  + { item.machine.title } + + { item.filament.vendor.title }  + { item.filament.type.title }
+
+ { item.color.title } + + { item.pressure_advance } + + { item.filament_flow_ratio } + + { item.filament_max_volumetric_speed } + + { item.nozzle_temperature } +
+
From e3bfec12e08342ff66e8589d826a6ed8847e760e Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 00:52:52 +0300 Subject: [PATCH 18/22] Update `HomeService` to improve user filament statistics handling; adjust `welcome` page to display average metrics and user count. --- app/Services/Pages/HomeService.php | 17 ++++++++++++++--- resources/js/pages/welcome.tsx | 16 +++++++++++----- resources/js/types/index.d.ts | 11 ++++++++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/app/Services/Pages/HomeService.php b/app/Services/Pages/HomeService.php index b3d7193..0859cbf 100644 --- a/app/Services/Pages/HomeService.php +++ b/app/Services/Pages/HomeService.php @@ -35,12 +35,22 @@ public function colors(): Collection return Color::query() ->whereHas('userFilament') ->orderBy('title') - ->get(['title', 'hex']); + ->get(['id', 'title', 'hex']); } - public function userFilaments(): Collection + public function userFilaments(int $count = 100): Collection { return UserFilament::query() + ->select([ + 'machine_id', + 'filament_id', + 'color_id', + ]) + ->selectRaw('count(*) as users_count') + ->selectRaw('avg(pressure_advance) as pressure_advance') + ->selectRaw('avg(filament_flow_ratio) as filament_flow_ratio') + ->selectRaw('avg(filament_max_volumetric_speed) as filament_max_volumetric_speed') + ->selectRaw('avg(nozzle_temperature) as nozzle_temperature') ->with([ 'machine.vendor', 'filament' => ['vendor', 'type'], @@ -49,7 +59,8 @@ public function userFilaments(): Collection ->orderBy('machine_id') ->orderBy('filament_id') ->orderBy('color_id') - ->orderBy('id') + ->groupBy(['machine_id', 'filament_id', 'color_id']) + ->limit($count) ->get(); } } diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index ebc64fe..1e09ff7 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -64,22 +64,25 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors Color - Pressure Advance + Average Pressure Advance - Flow Ratio + Average Flow Ratio - Max Volumetric Speed + Average Max Volumetric Speed - Nozzle Temperature + Average Nozzle Temperature + + + User Profiles { userFilaments.map((item) => ( - + { item.machine.vendor.title }  { item.machine.title } @@ -103,6 +106,9 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors { item.nozzle_temperature } + + { item.users_count } + )) } diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 5160156..737fe7d 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -1,15 +1,19 @@ -export interface Auth { +export interface Auth +{ user: User; } -export interface SharedData { +export interface SharedData +{ name: string; quote: { message: string; author: string }; auth: Auth; + [key: string]: unknown; } -export interface User { +export interface User +{ id: number; name: string; email: string; @@ -17,5 +21,6 @@ export interface User { email_verified_at: string | null; created_at: string; updated_at: string; + [key: string]: unknown; // This allows for additional properties... } From 59cebd045d1c0f9c2c2852c98511672c86b565ef Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 00:53:33 +0300 Subject: [PATCH 19/22] Update `welcome` page: rename dropdown filters to `filamentTypes` and `colors` for clarity. --- resources/js/pages/welcome.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 1e09ff7..d13f2df 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -29,7 +29,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { filamentTypes.map((item) => ( @@ -39,7 +39,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { colors.map((item) => ( From b5ec55eea4fe4e2718db003a2ace5bb71b247f97 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 00:54:05 +0300 Subject: [PATCH 20/22] Remove redundant `id` attributes from dropdown filters in `welcome` page for cleaner markup. --- resources/js/pages/welcome.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index d13f2df..c15ed5d 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -19,7 +19,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { machines.map((item) => ( @@ -29,7 +29,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { filamentTypes.map((item) => ( @@ -39,7 +39,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { colors.map((item) => ( From d32e64ccd3c68c06fc58636cb095670f23db85f1 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 01:06:57 +0300 Subject: [PATCH 21/22] Add filtering support for user filaments on the homepage through new request validation and dynamic dropdown updates. --- app/Http/Controllers/HomeController.php | 10 ++- app/Http/Requests/HomeFilterRequest.php | 19 +++++ app/Services/Pages/HomeService.php | 8 +- resources/js/pages/welcome.tsx | 109 ++++++++++++++++++++++-- 4 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 app/Http/Requests/HomeFilterRequest.php diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index be94146..082f555 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -4,18 +4,24 @@ namespace App\Http\Controllers; +use App\Http\Requests\HomeFilterRequest; use App\Services\Pages\HomeService; use Inertia\Inertia; class HomeController { - public function __invoke(HomeService $home) + public function __invoke(HomeFilterRequest $request, HomeService $home) { return Inertia::render('welcome', [ - 'userFilaments' => $home->userFilaments(), + 'userFilaments' => $home->userFilaments( + $request->integer('machine_id'), + $request->integer('filament_type_id'), + $request->integer('color_id'), + ), 'machines' => $home->machines(), 'filamentTypes' => $home->filamentTypes(), 'colors' => $home->colors(), + 'filters' => $request->validated(), ]); } } diff --git a/app/Http/Requests/HomeFilterRequest.php b/app/Http/Requests/HomeFilterRequest.php new file mode 100644 index 0000000..742182e --- /dev/null +++ b/app/Http/Requests/HomeFilterRequest.php @@ -0,0 +1,19 @@ + ['nullable', 'integer', 'min:0'], + 'filament_type_id' => ['nullable', 'integer', 'min:0'], + 'color_id' => ['nullable', 'integer', 'min:0'], + ]; + } +} diff --git a/app/Services/Pages/HomeService.php b/app/Services/Pages/HomeService.php index 0859cbf..f30712d 100644 --- a/app/Services/Pages/HomeService.php +++ b/app/Services/Pages/HomeService.php @@ -8,6 +8,7 @@ use App\Models\FilamentType; use App\Models\Machine; use App\Models\UserFilament; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; class HomeService @@ -38,7 +39,7 @@ public function colors(): Collection ->get(['id', 'title', 'hex']); } - public function userFilaments(int $count = 100): Collection + public function userFilaments(int $machineId, int $filamentTypeId, int $colorId, int $count = 100): Collection { return UserFilament::query() ->select([ @@ -51,6 +52,11 @@ public function userFilaments(int $count = 100): Collection ->selectRaw('avg(filament_flow_ratio) as filament_flow_ratio') ->selectRaw('avg(filament_max_volumetric_speed) as filament_max_volumetric_speed') ->selectRaw('avg(nozzle_temperature) as nozzle_temperature') + ->when($machineId, fn (Builder $builder) => $builder->where('machine_id', $machineId)) + ->when($colorId, fn (Builder $builder) => $builder->where('color_id', $colorId)) + ->when($filamentTypeId, fn (Builder $builder) => $builder + ->whereRelation('filament', 'filament_type_id', $filamentTypeId) + ) ->with([ 'machine.vendor', 'filament' => ['vendor', 'type'], diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index c15ed5d..e0e2048 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,6 +1,79 @@ -import { Head } from '@inertiajs/react'; +import { Head, router } from '@inertiajs/react'; +import { type ChangeEvent, useEffect, useState } from 'react'; + +type Machine = { + id: number; + title: string; + vendor: { + title: string; + }; +}; + +type FilamentType = { + id: number; + title: string; +}; + +type Color = { + id: number; + title: string; +}; + +type UserFilament = { + machine_id: number; + filament_id: number; + color_id: number; + machine: Machine; + filament: { + vendor: { + title: string; + }; + type: FilamentType; + }; + color: Color; + pressure_advance: number; + filament_flow_ratio: number; + filament_max_volumetric_speed: number; + nozzle_temperature: number; + users_count: number; +}; + +type Filters = { + machine_id: number; + filament_type_id: number; + color_id: number; +}; + +type WelcomeProps = { + userFilaments: UserFilament[]; + machines: Machine[]; + filamentTypes: FilamentType[]; + colors: Color[]; + filters: Filters; +}; + +export default function Welcome({ userFilaments, machines, filamentTypes, colors, filters }: WelcomeProps) { + const [selectedFilters, setSelectedFilters] = useState(filters); + + useEffect(() => { + setSelectedFilters(filters); + }, [filters]); + + const handleSelectChange = (key: keyof Filters) => (event: ChangeEvent) => { + const value = Number(event.target.value); + const nextFilters: Filters = { ...selectedFilters, [key]: value }; + + setSelectedFilters(nextFilters); + + router.visit('/', { + method: 'get', + data: nextFilters, + preserveState: true, + preserveScroll: true, + replace: true + }); + }; -export default function Welcome({ userFilaments, machines, filamentTypes, colors }) { return ( <> @@ -19,31 +92,49 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors - { machines.map((item) => ( - + )) } - { filamentTypes.map((item) => ( - + )) } - { colors.map((item) => ( - + )) } @@ -82,7 +173,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors { userFilaments.map((item) => ( - + { item.machine.vendor.title }  { item.machine.title } From e516192c166f3a4c5caf78274962639a4d50607d Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sun, 4 Jan 2026 01:13:28 +0300 Subject: [PATCH 22/22] Update `HomeService` to include `cover` in query; enhance `welcome` page with new title, cover display, and updated layout. --- app/Services/Pages/HomeService.php | 2 +- resources/js/pages/welcome.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Services/Pages/HomeService.php b/app/Services/Pages/HomeService.php index f30712d..1dd0bcb 100644 --- a/app/Services/Pages/HomeService.php +++ b/app/Services/Pages/HomeService.php @@ -20,7 +20,7 @@ public function machines(): Collection ->with('vendor') ->orderBy('vendor_id') ->orderBy('title') - ->get(['id', 'title', 'vendor_id']); + ->get(['id', 'title', 'cover', 'vendor_id']); } public function filamentTypes(): Collection diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index e0e2048..24ef3a8 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -4,6 +4,7 @@ import { type ChangeEvent, useEffect, useState } from 'react'; type Machine = { id: number; title: string; + cover: string; vendor: { title: string; }; @@ -76,11 +77,15 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors return ( <> - +
+

+ 3D Printing Settings +

+