diff --git a/.ai/.gitignore b/.ai/.gitignore new file mode 100644 index 0000000..de05057 --- /dev/null +++ b/.ai/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!/guidelines +!/guidelines/** diff --git a/.ai/guidelines/general.blade.php b/.ai/guidelines/general.blade.php new file mode 100644 index 0000000..009942c --- /dev/null +++ b/.ai/guidelines/general.blade.php @@ -0,0 +1,20 @@ +## General Information + +Основные правила описаны в файле `.aiassistant/rules/general.md` этого проекта. +Внимательно изучи их перед началом работы. + +### **Build/Configuration Instructions** + +- Database: Postgres 16 +- Установка зависимостей: `composer install` +- ОС разработчиков: Windows, Ubuntu, MacOS +- ОС веб-сервера: Ubuntu 24.04 +- При апгрейде зависимостей выполняй вызов скриптов только после внесения изменений в проект. +- При установке и обновлении PHP зависимостей всегда подставляй параметры `--ignore-platform-req=ext-pcntl --ignore-platform-req=ext-posix`, так как эти расширения недоступны в Windows. + +### **Additional Development Information** + +- Проект придерживается код-стайла PER 3.0. +- Всегда отвечай на русском языке, если не указано иное. +- Лог-файлы: `storage/logs/*.log`. Перед началом работ всегда удаляй эти файлы, чтобы иметь актуальное представление о возможных ошибках. Удаляй файлы только с расширением log. +- Не оставляй комментарии в коде. diff --git a/.aiassistant/rules/commit_message.md b/.aiassistant/rules/commit_message.md new file mode 100644 index 0000000..940bde3 --- /dev/null +++ b/.aiassistant/rules/commit_message.md @@ -0,0 +1,19 @@ +--- +apply: always +--- + +## Commit message rules + +- Keep it concise. Skip unnecessary details. +- Review the diff first, then compose the message. +- Specify exactly what was changed in the code. +- Use a single-line header only; do not add a body. +- Write in English only. +- Be as brief as you can. +- Aim for up to 70 characters (including any markup). +- Absolute cap: 100 characters for the header (including any markup). +- Do not end the header line with a dot. +- Enclose class names, variables, and attributes in backticks `like_this`. Example: Changed the type of `$foo` in class `Bar`. +- Start the header with an action verb that fits the change: added, changed, updated, fixed, improved, translated, removed, deleted, etc. +- Describe the direct edits made to the code, not their outcomes or effects. +- For class names and namespaces, omit leading and trailing backslashes. diff --git a/.aiassistant/rules/general.md b/.aiassistant/rules/general.md new file mode 100644 index 0000000..cfee094 --- /dev/null +++ b/.aiassistant/rules/general.md @@ -0,0 +1,129 @@ +--- +apply: always +--- + +# IDENTITY + +- Ты - экспертный веб-разработчик. Твой уровень - senior PHP backend developer. +- Твой основной стек технологий: PHP, Laravel Framework, Redis 8, но не ограничиваешься им. + +# LANGUAGE + +- Твой native language — русский, и ты предпочитаешь его, но при необходимости без проблем понимаешь Technical English. + +# CODING WORKFLOW + +## Canvas + +1. Всегда открывай один канвас на сессию (если не указано иное). +2. Пиши и запускай весь код внутри канваса. +3. Соблюдай код-стайл PER 3.0. + +## Код: + +- Отдавай полный, исполняемый код без заглушек. +- Перед публикацией сделай self‑review и минимальные правки. +- Для сложной логики добавляй базовое логирование и/или unit‑тесты. +- Не добавляй комментарии + +## KEY REFERENCES + +- PER Coding Style: https://www.php-fig.org/per/coding-style/ +- Laravel 12: https://laravel.com/docs/12.x +- PHP: https://www.php.net/docs.php +- Composer: https://getcomposer.org/doc/ +- Filament Materials: https://3dfilamentprofiles.com/materials +- Filament List: https://3dfilamentprofiles.com/filaments + +## Среда + +- При выполнении кода на своей стороне предварительно установи зависимости. + +# CHART STYLE + +- Заголовок сверху. +- Легенда одной строкой под заголовком. +- Единицы измерения через запятую. +- Сетка внутри графика отсутствует. +- Пастельные тона; фирменный цвет – бирюзовый. + +# FACT‑CHECK & CITATIONS + +- Если данные сомнительны, начинай предложение с [не проверено], [умозаключение] или [предположение]. +- При невозможности проверки отвечай: «Я не могу это подтвердить…». +- Не додумывай факты; запрашивай уточнение. + +# TONE + +- Деловой, без лишних emoji. + +# INSTRUCTION PRIORITY + +1. Настоящий файл +2. System / Dev сообщения +3. Сообщения пользователя +4. Поведение модели по умолчанию + +# TRIGGER / ACTION PAIRS + +Trigger: "напиши код ..." → Открой канвас, доставь код по правилам §CODING WORKFLOW. + +# EXAMPLES + +**Пример исправления непроверенного факта:** +Поправка: Ранее я сделал непроверенное утверждение. Оно было неверным и должно было быть помечено. + + +## ПРАВИЛА ЕСТЕСТВЕННОГО ТЕКСТА + +### ЯЗЫК + +- **Простые слова:** пиши так, будто общаешься с другом; избегай сложной лексики. +- **Короткие предложения и абзацы:** разбивай сложные мысли на удобоваримые части; абзац — 1-3 строки. +- **Избегай ИИ-штампов:** не используй «давайте погрузимся», «раскроем потенциал», «игру-меняющее», «революционный», + «трансформационный», «использовать потенциал», «оптимизировать», «разблокировать возможности». +- **Будь прямым:** говори, что имеешь в виду, без лишних слов. +- **Естественный поток:** нормально начинать фразы с «и», «но» или «так что». +- **Живой голос:** не искусственно дружелюбничай и не притворяйся восторженным. +- **Разговорная грамматика:** простые конструкции, а не академический стиль. + +### СТИЛЬ + +- **Убирай воду:** сокращай лишние прилагательные и наречия. +- **Примеры вместо абстракций:** показывай на конкретных случаях. +- **Честность:** признай ограничения, не переусердствуй с продажностью. +- **Как в мессенджере:** пиши так же прямо и просто, как в чате. +- **Плавные переходы:** используй простые связки вроде «смотри», «и», «но». +- **Избегай маркетинговых клише:** «инновационный», «лучший в классе», «прорывной» и т. п. + +### ЗАПРЕЩЁННЫЕ ФРАЗЫ + +- «Давайте погрузимся…» +- «Раскройте свой потенциал» +- «Игру-меняющее решение» +- «Революционный подход» +- «Трансформируйте свою жизнь» +- «Разблокируйте секреты» +- «Используйте эту стратегию» +- «Оптимизируйте рабочий процесс» + +### ЛУЧШЕ ИСПОЛЬЗОВАТЬ + +- «Вот как это работает» +- «Это может вам помочь» +- «Вот что я нашёл» +- «Это может сработать у вас» +- «Смотри, какая штука» +- «Вот почему это важно» +- «Но есть проблема» +- «Так что произошло вот что» + +### ФИНАЛЬНАЯ ПРОВЕРКА + +Перед отправкой убедись, что текст: + +- Звучит так, будто ты говоришь вслух. +- Использует слова, которыми говорит обычный человек. +- Не похож на маркетинговый слоган. +- Честен и искренен. +- Быстро переходит к сути. diff --git a/.github/images/list.png b/.github/images/list.png new file mode 100644 index 0000000..18dce05 Binary files /dev/null and b/.github/images/list.png differ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5d7b6ac..5060c22 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,9 +4,6 @@ on: release: types: - released - push: - branches: - - main workflow_dispatch: diff --git a/.gitignore b/.gitignore index abc5cfc..1d33fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,5 @@ yarn-error.log /.vscode /.zed /.junie -/.ai /.output.txt +/routes/playground.php diff --git a/README.md b/README.md index 1e738ed..a334ad6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,65 @@ -# 3D Printing +# 3D Filament Settings -Coming Soon +## Installation + +```bash +composer install +npm install + +composer migrate +composer operations +``` + +After that, you need to run the following commands on your local computer: + +```bash +php artisan orca-slicer:import + +# to run dev server +composer dev +``` + +### Development + +Create the `routes/playground.php` file and insert the following data into it: + +```php +use App\Data\OrcaSlicer\FilamentData; +use App\Models\Machine; +use App\Models\User; +use App\Services\OrcaSlicer\UserProfileService; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\File; + +Artisan::command('foo', function () { + $files = File::allFiles( + storage_path('app/private') + ); + + $service = app(UserProfileService::class); + + $user = User::firstWhere('email', 'your-email@example.com'); + + $machine = Machine::firstWhere('slug', 'k1-max'); + + foreach ($files as $file) { + if ($file->getExtension() !== 'json') { + continue; + } + + $content = file_get_contents($file->getRealPath()); + + $profile = FilamentData::from($content); + + dump( + $profile->externalId + ); + + $service->import($user, $machine, $profile); + } +}); +``` + +## List + +[list](.github/images/list.png) diff --git a/app/Concerns/WithColor.php b/app/Concerns/WithColor.php index 81b8dd2..089c163 100644 --- a/app/Concerns/WithColor.php +++ b/app/Concerns/WithColor.php @@ -5,6 +5,7 @@ namespace App\Concerns; use App\Models\Color; +use Illuminate\Support\Str; trait WithColor { @@ -21,9 +22,11 @@ trait WithColor protected function color(string $name): Color { - return $this->colors[$name] ??= Color::firstOrCreate([ - 'title' => $name, - 'hex' => $this->colorMap[$name] ?? '#000000', - ]); + return $this->colors[$name] ??= Color::query() + ->whereRaw('lower(title) = ?', [Str::lower($name)]) + ->firstOrCreate(values: [ + 'title' => $name, + 'hex' => $this->colorMap[$name] ?? '#000000', + ]); } } diff --git a/app/Concerns/WithFilaments.php b/app/Concerns/WithFilaments.php index 176892f..39e1ab8 100644 --- a/app/Concerns/WithFilaments.php +++ b/app/Concerns/WithFilaments.php @@ -7,6 +7,8 @@ use App\Models\Filament; use App\Models\FilamentType; use App\Models\Vendor; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; trait WithFilaments { @@ -14,9 +16,13 @@ trait WithFilaments protected array $filaments = []; - protected function filamentType(string $value): FilamentType + protected ?Collection $loadedFilamentTypes = null; + + protected function filamentType(string $name): FilamentType { - return $this->filamentTypes[$value] ??= FilamentType::firstOrCreate(['title' => $value]); + return $this->filamentTypes[$name] ??= FilamentType::query() + ->whereRaw('lower(title) = ?', [Str::lower($name)]) + ->firstOrCreate(values: ['title' => $name]); } protected function filament(Vendor $vendor, FilamentType $filamentType): Filament @@ -27,4 +33,16 @@ protected function filament(Vendor $vendor, FilamentType $filamentType): Filamen 'filament_type_id' => $filamentType->id, ]); } + + /** + * @return Collection + */ + protected function getFilamentTypes(): Collection + { + return $this->loadedFilamentTypes ??= FilamentType::query() + ->orderByRaw('LENGTH("title") DESC') + ->orderByDesc('title') + ->get() + ->keyBy('title'); + } } diff --git a/app/Concerns/WithNozzles.php b/app/Concerns/WithNozzles.php index 6bcdea2..0e4e66e 100644 --- a/app/Concerns/WithNozzles.php +++ b/app/Concerns/WithNozzles.php @@ -5,6 +5,7 @@ namespace App\Concerns; use App\Models\Nozzle; +use Illuminate\Support\Str; trait WithNozzles { diff --git a/app/Concerns/WithVendor.php b/app/Concerns/WithVendor.php index c6af24e..dbfded4 100644 --- a/app/Concerns/WithVendor.php +++ b/app/Concerns/WithVendor.php @@ -5,6 +5,7 @@ namespace App\Concerns; use App\Models\Vendor; +use Illuminate\Support\Str; trait WithVendor { @@ -12,6 +13,8 @@ trait WithVendor protected function vendor(string $name): Vendor { - return $this->vendors[$name] ??= Vendor::firstOrCreate(['title' => $name]); + return $this->vendors[$name] ??= Vendor::query() + ->whereRaw('lower(title) = ?', [Str::lower($name)]) + ->firstOrCreate(values: ['title' => $name]); } } diff --git a/app/Console/Commands/OrcaSlicer/ImportCommand.php b/app/Console/Commands/OrcaSlicer/ImportCommand.php index c36d602..413b006 100644 --- a/app/Console/Commands/OrcaSlicer/ImportCommand.php +++ b/app/Console/Commands/OrcaSlicer/ImportCommand.php @@ -5,6 +5,7 @@ namespace App\Console\Commands\OrcaSlicer; use App\Services\OrcaSlicer\DownloadService; +use App\Services\OrcaSlicer\FilamentService; use App\Services\OrcaSlicer\FilamentTypeService; use App\Services\OrcaSlicer\MachineService; use App\Services\OrcaSlicer\MapService; @@ -22,16 +23,19 @@ public function handle( MapService $map, MachineService $machine, NozzleService $nozzle, - FilamentTypeService $filament, + FilamentTypeService $type, + FilamentService $filament, ): 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 profile maps', fn () => $map->import()); + $this->components->task('Import sub filament maps', fn () => $map->importSubFilaments()); $this->components->task('Import machines', fn () => $machine->import()); $this->components->task('Import nozzles', fn () => $nozzle->import()); + $this->components->task('Import filament types', fn () => $type->import()); $this->components->task('Import filaments', fn () => $filament->import()); } } diff --git a/app/Data/Casts/ArrayToIntegerCast.php b/app/Data/Casts/ArrayToIntegerCast.php index b866e41..c899e31 100644 --- a/app/Data/Casts/ArrayToIntegerCast.php +++ b/app/Data/Casts/ArrayToIntegerCast.php @@ -11,7 +11,7 @@ class ArrayToIntegerCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): int { if (is_array($value)) { return (int) array_first($value); diff --git a/app/Data/Casts/ArrayToStringCast.php b/app/Data/Casts/ArrayToStringCast.php new file mode 100644 index 0000000..586172e --- /dev/null +++ b/app/Data/Casts/ArrayToStringCast.php @@ -0,0 +1,19 @@ +reject(fn (array $filament): bool => str_contains((string) $filament['name'], 'fdm_')) - ->map(fn (array $filament): ?FilamentData => $this->filament()->get( - $properties['meta']['profile'], - $filament['name'], - $properties['meta'] - )) - ->filter(); - } - - protected function filament(): FilamentService - { - return app(FilamentService::class); - } -} diff --git a/app/Data/OrcaSlicer/FilamentData.php b/app/Data/OrcaSlicer/FilamentData.php index 2454b13..ef9dd8d 100644 --- a/app/Data/OrcaSlicer/FilamentData.php +++ b/app/Data/OrcaSlicer/FilamentData.php @@ -4,12 +4,9 @@ use App\Data\Casts\ArrayToFloatCast; use App\Data\Casts\ArrayToIntegerCast; -use App\Data\Casts\OrcaSlicer\FilamentMachineCast; -use App\Data\Casts\OrcaSlicer\FilamentTitleCast; -use Illuminate\Support\Str; +use App\Data\Casts\ArrayToStringCast; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapName; -use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Data; use Spatie\LaravelData\Mappers\SnakeCaseMapper; @@ -18,16 +15,17 @@ class FilamentData extends Data { public function __construct( - #[MapOutputName('external_id')] - public string $settingId, + #[MapInputName('filament_settings_id')] + #[WithCast(ArrayToStringCast::class)] + public string $externalId, - #[MapInputName('name')] - #[WithCast(FilamentTitleCast::class)] - public string $title, + public string $name, - #[MapInputName('name')] - #[WithCast(FilamentMachineCast::class)] - public string $machine, + public string $inherits, + + #[MapInputName('default_filament_colour')] + #[WithCast(ArrayToStringCast::class)] + public string $color = '#000000', #[WithCast(ArrayToFloatCast::class)] public float $pressureAdvance = 0, @@ -42,12 +40,4 @@ public function __construct( #[WithCast(ArrayToIntegerCast::class)] public int $nozzleTemperatureInitialLayer = 0, ) {} - - public static function prepareForPipeline(array $properties): array - { - $properties['filament_id'] ??= Str::slug($properties['name']); - $properties['setting_id'] ??= $properties['filament_id']; - - return $properties; - } } diff --git a/app/Data/OrcaSlicer/ProfileData.php b/app/Data/OrcaSlicer/ProfileData.php deleted file mode 100644 index f021bb7..0000000 --- a/app/Data/OrcaSlicer/ProfileData.php +++ /dev/null @@ -1,28 +0,0 @@ -belongsTo(static::class, 'parent_id', 'id'); + } + + public function vendor(): Relation { return $this->belongsTo(Vendor::class); } diff --git a/app/Models/User.php b/app/Models/User.php index ac89ed4..0d7681c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -34,17 +34,22 @@ protected function casts(): array ]; } - public function machines(): BelongsToMany + public function machines(): Relation { return $this->belongsToMany(Machine::class, 'user_machine') ->using(UserMachine::class) ->withTimestamps(); } - public function filaments(): BelongsToMany + public function filaments(): Relation { return $this->belongsToMany(Filament::class, 'user_filament') ->using(UserFilament::class) ->withTimestamps(); } + + public function userFilaments(): Relation + { + return $this->hasMany(UserFilament::class, 'user_id', 'id'); + } } diff --git a/app/Services/OrcaSlicer/FilamentProfileService.php b/app/Services/OrcaSlicer/FilamentProfileService.php new file mode 100644 index 0000000..0b07667 --- /dev/null +++ b/app/Services/OrcaSlicer/FilamentProfileService.php @@ -0,0 +1,86 @@ +inherits) { + return $filament; + } + + $profile = $this->profile($filament->inherits); + $content = $this->perform($profile, $filament->inherits); + + $source = $filament->toArray(); + + $source['filament_settings_id'] = $filament->externalId; + + return FilamentData::from( + array_merge($this->clean($content), $this->clean($source)) + ); + } + + protected function perform(string $profile, string $key, array $values = []): array + { + if (! $path = $this->findPath($profile, $key)) { + return $values; + } + + $content = $this->clean( + $this->read($path) + ); + + if (! $parent = $content['inherits'] ?? null) { + return array_merge($content, $values); + } + + return $this->perform($profile, $parent, array_merge($content, $values)); + } + + protected function findPath(string $profile, string $key): ?string + { + return Map::query() + ->where('type', SourceType::Filament) + ->where('key', $key) + ->where(fn (Builder $builder) => $builder + ->whereRelation('parent', 'profile', $profile) + ->orwhere('profile', $profile) + ) + ->orderByDesc('parent_id') + ->first()?->path; + } + + protected function profile(string $key): string + { + return Str::before($key, ' '); + } + + protected function read(string $filename): array + { + return $this->storage->json( + 'profiles/' . $filename + ); + } + + protected function clean(array $values): array + { + return array_filter($values); + } +} diff --git a/app/Services/OrcaSlicer/FilamentService.php b/app/Services/OrcaSlicer/FilamentService.php index 3318116..be6f4c2 100644 --- a/app/Services/OrcaSlicer/FilamentService.php +++ b/app/Services/OrcaSlicer/FilamentService.php @@ -4,74 +4,52 @@ namespace App\Services\OrcaSlicer; -use App\Data\OrcaSlicer\FilamentData; -use App\Exceptions\FilamentProfileNotFoundException; -use Illuminate\Container\Attributes\Config; -use Illuminate\Container\Attributes\Storage; -use Illuminate\Filesystem\FilesystemAdapter; - -use function array_merge; -use function file_exists; -use function file_get_contents; -use function implode; -use function json_decode; -use function report; +use App\Concerns\WithFilaments; +use App\Enums\SourceType; +use App\Models\Filament; +use App\Models\Map; +use Illuminate\Support\Str; class FilamentService { - public function __construct( - #[Storage('orca_slicer')] - protected FilesystemAdapter $storage, - #[Config('orca_slicer.directory')] - protected string $directory - ) {} + use WithFilaments; - public function get(string $vendor, string $profile, array $meta): ?FilamentData + public function import(): void { - if (! $parameters = $this->parameters($vendor, $profile)) { - return null; - } - - $parameters['machine'] = $profile; - $parameters['meta'] = $meta; - - return FilamentData::from($parameters); + Map::query() + ->where('type', SourceType::Filament) + ->each(fn (Map $map) => $this->store($map)); } - protected function parameters(string $vendor, string $profile): array + protected function store(Map $map): void { - $parameters = $this->load($vendor, $profile); + $typeId = $this->detect($map->key, $map->path); - if (! $parent = $parameters['inherits'] ?? false) { - return $parameters; - } - - $previous = $this->parameters($vendor, $parent); - - return array_merge($previous, $parameters); + Filament::updateOrCreate([ + 'vendor_id' => $map->vendor_id, + 'filament_type_id' => $typeId, + ]); } - protected function load(string $vendor, string $profile): array + public function detect(string $key, string $path): int { - $path = $this->path($vendor, $profile); - - if (! file_exists($path)) { - report(new FilamentProfileNotFoundException($vendor, $profile, $path)); + $value = $this->prepare($key, $path); - return []; + foreach ($this->getFilamentTypes() as $type) { + if (Str::contains($value, $type->title)) { + return $type->id; + } } - return json_decode(file_get_contents($this->path($vendor, $profile)), true); + return $this->getFilamentTypes()->get('Unknown')->id; } - protected function path(string $vendor, string $profile): string + protected function prepare(string $key, string $path): string { - return $this->storage->path(implode(DIRECTORY_SEPARATOR, [ - $this->directory, - 'resources/profiles', - $vendor, - 'filament', - $profile . '.json', - ])); + return Str::of($key) + ->append(' ', $path) + ->replace(['@HS', '@HF'], ' HS') + ->replace('@', ' ') + ->toString(); } } diff --git a/app/Services/OrcaSlicer/FilamentTypeService.php b/app/Services/OrcaSlicer/FilamentTypeService.php index ceb133a..b7dbfff 100644 --- a/app/Services/OrcaSlicer/FilamentTypeService.php +++ b/app/Services/OrcaSlicer/FilamentTypeService.php @@ -5,203 +5,35 @@ namespace App\Services\OrcaSlicer; use App\Concerns\WithFilaments; -use App\Enums\SourceType; -use App\Models\Filament; -use App\Models\Map; -use App\Models\Vendor; +use Illuminate\Container\Attributes\Storage; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Str; -use Illuminate\Support\Stringable; class FilamentTypeService { use WithFilaments; - 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 __construct( + #[Storage('orca_resources')] + protected FilesystemAdapter $storage, + ) {} 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, - ]); + collect($this->load()) + ->forget(['version']) + ->flatten() + ->unique() + ->map(fn (string $type) => Str::contains($type, '-') ? $type : [$type, $type . '+']) + ->flatten() + ->crossJoin(['', 'HS']) + ->map(fn (array $pair) => trim(implode(' ', $pair))) + ->prepend('Unknown') + ->each(fn (string $type) => $this->filamentType($type)); } - protected function detect(string $vendor, string $filament, string $path): string + protected function load(): array { - 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(); + return $this->storage->json('info/filament_info.json'); } } diff --git a/app/Services/OrcaSlicer/MapService.php b/app/Services/OrcaSlicer/MapService.php index f24e90e..2879470 100644 --- a/app/Services/OrcaSlicer/MapService.php +++ b/app/Services/OrcaSlicer/MapService.php @@ -6,66 +6,68 @@ 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; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Str; -use SplFileInfo; class MapService { use WithVendor; public function __construct( - #[Storage('orca_slicer')] + #[Storage('orca_resources')] protected FilesystemAdapter $storage, - #[Config('orca_slicer.source')] - protected string $source, - #[Config('orca_slicer.archive')] - protected string $archive, - #[Config('orca_slicer.directory')] - protected string $directory, #[Config('orca_slicer.except_files')] protected array $exceptFiles, ) {} public function import(): void { - foreach ($this->profiles() as $file) { - if ($this->skip($file)) { + foreach ($this->profiles() as $filename) { + if ($this->skip($filename)) { continue; } - $profile = $this->load( - $file->getRealPath() - ); + $profile = $this->load($filename); $vendor = $this->vendor( $profile['name'] ); - $profileName = $this->profileName($file); + $name = $this->profileName($filename); - $this->machines( - $vendor, - $profileName, - $profile['machine_model_list'] - ); + $this->machines($vendor, $name, $profile['machine_model_list']); + $this->filaments($vendor, $name, $profile['filament_list']); + $this->processes($vendor, $name, $profile['process_list']); + } + } - $this->filaments( - $vendor, - $profileName, - $profile['filament_list'] - ); + public function importSubFilaments(): void + { + Map::query() + ->where('type', SourceType::Filament) + ->whereRaw('(LENGTH("path") - LENGTH(REPLACE("path", \'/\', \'\'))) = 3') + ->get() + ->each(fn (Map $map) => $this->subFilament($map)); + } - $this->processes( - $vendor, - $profileName, - $profile['process_list'] - ); - } + protected function subFilament(Map $map): void + { + $profile = explode('/', $map->path)[2]; + + $vendor = $this->vendor($profile); + + $map->replicate()->fill([ + 'parent_id' => $map->id, + 'vendor_id' => $vendor->id, + 'profile' => $profile, + ]); + + Map::insertOrIgnore( + $map->toArray(), + ); } protected function machines(Vendor $vendor, string $profile, array $machines): void @@ -101,46 +103,29 @@ protected function store(SourceType $type, Vendor $vendor, string $profile, stri } /** - * @return SplFileInfo[] + * @return string[] */ protected function profiles(): array { - return File::files( - $this->profilesPath() - ); + return $this->storage->files('profiles'); } - protected function profileName(SplFileInfo $file): string + protected function profileName(string $filename): string { - $name = $file->getFilename(); - $extension = $file->getExtension(); - - return Str::before($name, '.' . $extension); + return pathinfo($filename, PATHINFO_FILENAME); } - protected function skip(SplFileInfo $file): bool + protected function skip(string $filename): bool { - if ($file->getExtension() !== 'json') { + if (pathinfo($filename, PATHINFO_EXTENSION) !== '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'); + return in_array(pathinfo($filename, PATHINFO_BASENAME), $this->exceptFiles, true); } - protected function path(string $filename): string + protected function load(string $filename): array { - return $this->storage->path( - $this->directory . DIRECTORY_SEPARATOR . $filename - ); + return $this->storage->json($filename); } } diff --git a/app/Services/OrcaSlicer/UserProfileService.php b/app/Services/OrcaSlicer/UserProfileService.php new file mode 100644 index 0000000..af84e43 --- /dev/null +++ b/app/Services/OrcaSlicer/UserProfileService.php @@ -0,0 +1,65 @@ +vendor($profile); + + $value = $profile->inherits ?: $profile->externalId; + + $filament = $this->filament($vendor, $value); + + if (! $filament) { + report(new UserFilamentNotFoundException($vendor, $value)); + + return; + } + + $color = $this->color($profile->color); + + $content = $this->filament->get($profile)->toArray(); + + $user->userFilaments()->updateOrCreate([ + 'machine_id' => $machine->id, + 'filament_id' => $filament->id, + 'color_id' => $color->id, + ], $content); + } + + protected function filament(string $vendor, string $filament): ?Filament + { + $path = $vendor . '/filament/' . $filament . '.json'; + + $typeId = $this->filamentType->detect($filament, $path); + + return Filament::query() + ->whereRelation('vendor', 'title', 'ilike', '%' . $vendor . '%') + ->where('filament_type_id', $typeId) + ->first(); + } + + protected function vendor(FilamentData $filament): string + { + return Str::of($filament->externalId)->before(' ')->trim()->toString(); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c6234fc..daecee3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -10,9 +10,9 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__ . '/../routes/web.php', - commands: __DIR__ . '/../routes/console.php', - health: '/up', + web : __DIR__ . '/../routes/web.php', + commands: __DIR__ . '/../routes/playground.php', + health : '/up', ) ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [ diff --git a/config/filesystems.php b/config/filesystems.php index 3268819..625bc91 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -57,6 +57,14 @@ 'report' => false, ], + 'orca_resources' => [ + 'driver' => 'local', + 'root' => storage_path('app/orca_slicer/OrcaSlicer-main/resources'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + ], /* 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 526b315..468f2f9 100644 --- a/database/migrations/2026_01_03_111601_create_maps_table.php +++ b/database/migrations/2026_01_03_111601_create_maps_table.php @@ -13,10 +13,12 @@ public function up(): void $table->id(); $table->foreignId('vendor_id')->constrained('vendors')->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('maps')->cascadeOnDelete(); $table->string('type'); $table->string('profile'); + $table->string('key'); $table->string('path'); diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 24ef3a8..c661e43 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -77,13 +77,13 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors return ( <> - +

- 3D Printing Settings + 3D Filament Settings

@@ -172,7 +172,7 @@ export default function Welcome({ userFilaments, machines, filamentTypes, colors Average Nozzle Temperature diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 737fe7d..c1a1647 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -3,15 +3,6 @@ export interface Auth user: User; } -export interface SharedData -{ - name: string; - quote: { message: string; author: string }; - auth: Auth; - - [key: string]: unknown; -} - export interface User { id: number; diff --git a/tests/Unit/FilamentTypeServiceTest.php b/tests/Unit/FilamentTypeAAAAAServiceTest.php similarity index 97% rename from tests/Unit/FilamentTypeServiceTest.php rename to tests/Unit/FilamentTypeAAAAAServiceTest.php index cbf66da..46bbd73 100644 --- a/tests/Unit/FilamentTypeServiceTest.php +++ b/tests/Unit/FilamentTypeAAAAAServiceTest.php @@ -4,7 +4,7 @@ namespace Tests\Unit; -use App\Services\OrcaSlicer\FilamentTypeService; +use App\Services\OrcaSlicer\FilamentService; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -13,7 +13,7 @@ class FilamentTypeServiceTest extends TestCase #[DataProvider('detectCases')] public function testDetect(?string $vendor, string $filament, ?string $expected): void { - $service = new class extends FilamentTypeService { + $service = new class extends FilamentService { public function exposedDetect(?string $vendor, string $filament): ?string { $path = $vendor . '/filament/' . $filament . '.json';
- User Profiles + By User Profiles