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/Casts/FilamentTypeTitleCast.php b/app/Casts/FilamentTypeTitleCast.php
deleted file mode 100644
index d20344d..0000000
--- a/app/Casts/FilamentTypeTitleCast.php
+++ /dev/null
@@ -1,50 +0,0 @@
-perform($value)) {
- return $title;
- }
-
- report(new UnknownFilamentTypeException($value));
-
- return 'Unknown';
- }
-
- protected function perform(string $value): string
- {
- return Str::of($value)
- ->replace(['Generic', 'Value'], '')
- ->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()
- ->match('/([^\d][A-Z]{2,4}[+\-\s]?([A-Z]{2,4}|Silk|Wood)?)/')
- ->replace(' CF', '-CF', false)
- ->squish()
- ->trim('-')
- ->replaceMatches('/([A-Z]{2})[\s-]?([A-Z]{3,})/', '$2-$1')
- ->toString();
- }
-}
diff --git a/app/Concerns/WithColor.php b/app/Concerns/WithColor.php
new file mode 100644
index 0000000..81b8dd2
--- /dev/null
+++ b/app/Concerns/WithColor.php
@@ -0,0 +1,29 @@
+ '#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,
+ 'hex' => $this->colorMap[$name] ?? '#000000',
+ ]);
+ }
+}
diff --git a/app/Concerns/WithFilaments.php b/app/Concerns/WithFilaments.php
new file mode 100644
index 0000000..176892f
--- /dev/null
+++ b/app/Concerns/WithFilaments.php
@@ -0,0 +1,30 @@
+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,
+ ]);
+ }
+}
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/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..4d63896
--- /dev/null
+++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php
@@ -0,0 +1,36 @@
+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 machines', fn () => $machine->import());
+ $this->components->task('Import nozzles', fn () => $nozzle->import());
+ $this->components->task('Import filaments', fn () => $filament->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..c633ba3
--- /dev/null
+++ b/app/Enums/SourceType.php
@@ -0,0 +1,12 @@
+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(
+ $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/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 962c7c9..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;
@@ -16,6 +17,7 @@ class Filament extends Model
protected $fillable = [
'vendor_id',
'filament_type_id',
+ 'external_id',
];
public function vendor(): Relation
@@ -27,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 faca8d6..7536f0d 100644
--- a/app/Models/FilamentType.php
+++ b/app/Models/FilamentType.php
@@ -4,9 +4,9 @@
namespace App\Models;
-use App\Casts\FilamentTypeTitleCast;
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
@@ -17,10 +17,15 @@ class FilamentType extends Model
'title',
];
- protected function casts(): array
+ public function userFilaments(): HasManyThrough
{
- return [
- 'title' => FilamentTypeTitleCast::class,
- ];
+ 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/Map.php b/app/Models/Map.php
new file mode 100644
index 0000000..69cc1ca
--- /dev/null
+++ b/app/Models/Map.php
@@ -0,0 +1,32 @@
+ SourceType::class,
+ ];
+ }
+
+ public function vendor(): BelongsTo
+ {
+ return $this->belongsTo(Vendor::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/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/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php
new file mode 100644
index 0000000..ceb133a
--- /dev/null
+++ b/app/Services/OrcaSlicer/FilamentTypeService.php
@@ -0,0 +1,207 @@
+ ' 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->vendor, $map));
+ }
+
+ protected function store(Vendor $vendor, Map $map): void
+ {
+ if (! $value = $this->detect($vendor->title, $map->key, $map->path)) {
+ return;
+ }
+
+ $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
+ {
+ 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 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()
+ ->ltrim('+ ')
+ ->when(
+ fn (Stringable $str) => $str->isMatch('/[A-Z\d]{2,}\sCF/'),
+ fn (Stringable $str) => $str->replace(' ', '-'),
+ )
+ ->toString();
+ }
+}
diff --git a/app/Services/OrcaSlicer/ImportService.php b/app/Services/OrcaSlicer/ImportService.php
deleted file mode 100644
index b42f7be..0000000
--- a/app/Services/OrcaSlicer/ImportService.php
+++ /dev/null
@@ -1,206 +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
- );
- }
-}
diff --git a/app/Services/OrcaSlicer/MachineService.php b/app/Services/OrcaSlicer/MachineService.php
new file mode 100644
index 0000000..cd85433
--- /dev/null
+++ b/app/Services/OrcaSlicer/MachineService.php
@@ -0,0 +1,47 @@
+with('vendor')
+ ->where('type', SourceType::Machine)
+ ->each(fn (Map $map) => $this->store($map));
+ }
+
+ protected function store(Map $map): void
+ {
+ $map->vendor->machines()->updateOrCreate([
+ 'title' => Str::of($map->key)->after($map->vendor->title)->trim()->value(),
+ ], [
+ '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
new file mode 100644
index 0000000..f24e90e
--- /dev/null
+++ b/app/Services/OrcaSlicer/MapService.php
@@ -0,0 +1,146 @@
+profiles() as $file) {
+ if ($this->skip($file)) {
+ continue;
+ }
+
+ $profile = $this->load(
+ $file->getRealPath()
+ );
+
+ $vendor = $this->vendor(
+ $profile['name']
+ );
+
+ $profileName = $this->profileName($file);
+
+ $this->machines(
+ $vendor,
+ $profileName,
+ $profile['machine_model_list']
+ );
+
+ $this->filaments(
+ $vendor,
+ $profileName,
+ $profile['filament_list']
+ );
+
+ $this->processes(
+ $vendor,
+ $profileName,
+ $profile['process_list']
+ );
+ }
+ }
+
+ protected function machines(Vendor $vendor, string $profile, array $machines): void
+ {
+ foreach ($machines as $machine) {
+ $this->store(SourceType::Machine, $vendor, $profile, $machine['name'], $machine['sub_path']);
+ }
+ }
+
+ protected function filaments(Vendor $vendor, string $profile, array $filaments): void
+ {
+ foreach ($filaments as $filament) {
+ $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, Vendor $vendor, string $profile, string $name, string $path): void
+ {
+ $vendor->maps()->updateOrCreate([
+ 'type' => $type,
+ 'key' => $name,
+ ], [
+ 'profile' => $profile,
+ '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/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;
+ }
+}
diff --git a/app/Services/Pages/HomeService.php b/app/Services/Pages/HomeService.php
new file mode 100644
index 0000000..1dd0bcb
--- /dev/null
+++ b/app/Services/Pages/HomeService.php
@@ -0,0 +1,72 @@
+whereHas('userFilament')
+ ->with('vendor')
+ ->orderBy('vendor_id')
+ ->orderBy('title')
+ ->get(['id', 'title', 'cover', '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(['id', 'title', 'hex']);
+ }
+
+ public function userFilaments(int $machineId, int $filamentTypeId, int $colorId, 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')
+ ->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'],
+ 'color',
+ ])
+ ->orderBy('machine_id')
+ ->orderBy('filament_id')
+ ->orderBy('color_id')
+ ->groupBy(['machine_id', 'filament_id', 'color_id'])
+ ->limit($count)
+ ->get();
+ }
+}
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..526b315
--- /dev/null
+++ b/database/migrations/2026_01_03_111601_create_maps_table.php
@@ -0,0 +1,29 @@
+id();
+
+ $table->foreignId('vendor_id')->constrained('vendors')->cascadeOnDelete();
+
+ $table->string('type');
+
+ $table->string('profile');
+ $table->string('key');
+ $table->string('path');
+
+ $table->timestamps();
+
+ $table->index(['type', 'key']);
+ $table->unique(['vendor_id', 'type', 'key']);
+ });
+ }
+};
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
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]]
- );
- }
- }
-};
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);
- }
};
diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx
index b6cacea..24ef3a8 100644
--- a/resources/js/pages/welcome.tsx
+++ b/resources/js/pages/welcome.tsx
@@ -1,68 +1,215 @@
-import { Head } from '@inertiajs/react';
+import { Head, router } from '@inertiajs/react';
+import { type ChangeEvent, useEffect, useState } from 'react';
+
+type Machine = {
+ id: number;
+ title: string;
+ cover: 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({ settings }) {
return (
<>
-
+
-
-
-
- |
- Printer
- |
-
- Filament
- |
-
- Color
- |
-
- Pressure Advance
- |
-
- Flow Ratio
- |
-
- Max Volumetric Speed
- |
-
- Nozzle Temperature
- |
-
-
-
- { settings.map((setting) => (
-
- |
- { 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 }
- |
-
- )) }
-
-
+
+
+ 3D Printing Settings
+
+
+
+
+
+ | Printer |
+ Filament |
+ Color |
+
+
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+ Printer
+ |
+
+ Filament
+ |
+
+ Color
+ |
+
+ Average Pressure Advance
+ |
+
+ Average Flow Ratio
+ |
+
+ Average Max Volumetric Speed
+ |
+
+ Average Nozzle Temperature
+ |
+
+ User Profiles
+ |
+
+
+
+ { userFilaments.map((item) => (
+
+ |
+ { 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 }
+ |
+
+ { 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...
}
diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeServiceTest.php
new file mode 100644
index 0000000..cbf66da
--- /dev/null
+++ b/tests/Unit/FilamentTypeServiceTest.php
@@ -0,0 +1,116 @@
+detect($vendor, $filament, $path);
+ }
+ };
+
+ $this->assertSame($expected, $service->exposedDetect($vendor, $filament));
+ }
+
+ public static function detectCases(): array
+ {
+ return [
+ ['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'],
+ ];
+ }
+}