Full Code of MOVIBALE/Lumina-Layers for AI

main 38c1dc2eb890 cached
410 files
2.4 MB
647.9k tokens
1617 symbols
1 requests
Download .txt
Showing preview only (2,680K chars total). Download the full file or copy to clipboard to get everything.
Repository: MOVIBALE/Lumina-Layers
Branch: main
Commit: 38c1dc2eb890
Files: 410
Total size: 2.4 MB

Directory structure:
gitextract_1imuoy6m/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── CHANGELOG.md
├── CHANGELOG_CN.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_CN.md
├── api/
│   ├── __init__.py
│   ├── app.py
│   ├── dependencies.py
│   ├── file_bridge.py
│   ├── file_registry.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── calibration.py
│   │   ├── converter.py
│   │   ├── extractor.py
│   │   ├── five_color.py
│   │   ├── health.py
│   │   ├── lut.py
│   │   ├── slicer.py
│   │   └── system.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── calibration.py
│   │   ├── converter.py
│   │   ├── extractor.py
│   │   ├── five_color.py
│   │   ├── lut.py
│   │   ├── responses.py
│   │   ├── slicer.py
│   │   └── system.py
│   ├── session_store.py
│   ├── worker_pool.py
│   └── workers/
│       ├── __init__.py
│       └── converter_workers.py
├── api_server.py
├── assets/
│   └── smart_8color_stacks.npy
├── bambu_config_template.json
├── benchmark.py
├── config.py
├── core/
│   ├── __init__.py
│   ├── calibration.py
│   ├── color_analyzer.py
│   ├── color_matching_hue_aware.py
│   ├── color_merger.py
│   ├── color_replacement.py
│   ├── converter.py
│   ├── extractor.py
│   ├── five_color_combination.py
│   ├── geometry_utils.py
│   ├── heightmap_loader.py
│   ├── i18n.py
│   ├── image_preprocessor.py
│   ├── image_processing.py
│   ├── isolated_pixel_cleanup.py
│   ├── lut_merger.py
│   ├── mesh_generators.py
│   ├── naming.py
│   ├── slicer.py
│   ├── tray.py
│   └── vector_engine.py
├── docs/
│   └── api_mapping_blueprint.md
├── frontend/
│   ├── README.md
│   ├── eslint.config.js
│   ├── index.html
│   ├── package.json
│   ├── postcss.config.js
│   ├── public/
│   │   ├── hdr/
│   │   │   └── studio_small_09_1k.hdr
│   │   └── test.glb
│   ├── scripts/
│   │   └── generate-test-glb.mjs
│   ├── src/
│   │   ├── App.css
│   │   ├── App.tsx
│   │   ├── __tests__/
│   │   │   ├── App.test.tsx
│   │   │   ├── InteractiveModelViewer.test.ts
│   │   │   ├── KeychainRing3D.test.tsx
│   │   │   ├── LutColorGrid.test.ts
│   │   │   ├── Scene3D.test.tsx
│   │   │   ├── action-bar-always-visible.property.test.ts
│   │   │   ├── action-bar-always-visible.test.ts
│   │   │   ├── api-client.property.test.ts
│   │   │   ├── app-tabs.test.tsx
│   │   │   ├── autoPreview.test.ts
│   │   │   ├── bed-size-selector.property.test.ts
│   │   │   ├── calibration-hooks.property.test.ts
│   │   │   ├── calibration-panel.test.tsx
│   │   │   ├── calibration-store.property.test.ts
│   │   │   ├── colorRemap.property.test.ts
│   │   │   ├── colorSelect.property.test.ts
│   │   │   ├── converter-api.test.ts
│   │   │   ├── converter-store.property.test.ts
│   │   │   ├── extractor-5color.property.test.ts
│   │   │   ├── extractor-api.property.test.ts
│   │   │   ├── extractor-canvas.test.tsx
│   │   │   ├── extractor-panel.test.tsx
│   │   │   ├── extractor-store.property.test.ts
│   │   │   ├── five-color-store.property.test.ts
│   │   │   ├── health-status.property.test.tsx
│   │   │   ├── keychainRing.property.test.ts
│   │   │   ├── layout.test.tsx
│   │   │   ├── loading-spinner.test.tsx
│   │   │   ├── lut-manager-panel.test.tsx
│   │   │   ├── lut-manager-refresh.property.test.ts
│   │   │   ├── lut-manager-store.property.test.ts
│   │   │   ├── lutColorFilter.property.test.ts
│   │   │   ├── model-centering.property.test.ts
│   │   │   ├── paletteLutMerge.property.test.ts
│   │   │   ├── paletteLutMerge.test.tsx
│   │   │   ├── paletteOptimization.property.test.ts
│   │   │   ├── paletteOptimization.test.tsx
│   │   │   ├── realtimePreview.property.test.ts
│   │   │   ├── realtimePreview.test.ts
│   │   │   ├── reliefHeight.property.test.ts
│   │   │   ├── replace-preview.property.test.ts
│   │   │   ├── scaleUtils.property.test.ts
│   │   │   ├── settings-store.property.test.ts
│   │   │   ├── slicer.property.test.ts
│   │   │   ├── stack-positions-nonoverlap.property.test.ts
│   │   │   ├── tab-filter.property.test.ts
│   │   │   ├── tab-switch-layout.property.test.ts
│   │   │   ├── theme.property.test.ts
│   │   │   ├── theme.test.tsx
│   │   │   ├── ui-components.test.tsx
│   │   │   ├── widget-drag-perf.property.test.ts
│   │   │   ├── widget-registry-i18n.property.test.ts
│   │   │   ├── widget-workspace.property.test.ts
│   │   │   ├── widget-workspace.test.tsx
│   │   │   └── zoomable-image.property.test.ts
│   │   ├── api/
│   │   │   ├── __tests__/
│   │   │   │   └── batchApi.property.test.ts
│   │   │   ├── calibration.ts
│   │   │   ├── client.ts
│   │   │   ├── converter.ts
│   │   │   ├── extractor.ts
│   │   │   ├── fiveColor.ts
│   │   │   ├── lut.ts
│   │   │   ├── slicer.ts
│   │   │   ├── system.ts
│   │   │   └── types.ts
│   │   ├── components/
│   │   │   ├── AboutView.tsx
│   │   │   ├── BedPlatform.tsx
│   │   │   ├── CalibrationPanel.tsx
│   │   │   ├── ExtractorCanvas.tsx
│   │   │   ├── ExtractorPanel.tsx
│   │   │   ├── FiveColorCanvas.tsx
│   │   │   ├── FiveColorQueryPanel.tsx
│   │   │   ├── InteractiveModelViewer.tsx
│   │   │   ├── KeychainRing3D.tsx
│   │   │   ├── LanguageToggle.tsx
│   │   │   ├── LoadingSpinner.tsx
│   │   │   ├── LutManagerPanel.tsx
│   │   │   ├── ModelViewer.tsx
│   │   │   ├── Scene3D.tsx
│   │   │   ├── ThemeToggle.tsx
│   │   │   ├── __tests__/
│   │   │   │   ├── ActionBar.batch.test.tsx
│   │   │   │   ├── BasicSettings.batch.test.tsx
│   │   │   │   ├── BatchFileUploader.test.tsx
│   │   │   │   ├── BatchResultSummary.property.test.tsx
│   │   │   │   └── BatchResultSummary.test.tsx
│   │   │   ├── lightingConfig.ts
│   │   │   ├── sections/
│   │   │   │   ├── ActionBar.tsx
│   │   │   │   ├── AdvancedSettings.tsx
│   │   │   │   ├── BasicSettings.tsx
│   │   │   │   ├── BedSizeSelector.tsx
│   │   │   │   ├── CloisonneSettings.tsx
│   │   │   │   ├── CoatingSettings.tsx
│   │   │   │   ├── KeychainLoopSettings.tsx
│   │   │   │   ├── LutColorGrid.tsx
│   │   │   │   ├── OutlineSettings.tsx
│   │   │   │   ├── PalettePanel.tsx
│   │   │   │   ├── ReliefSettings.tsx
│   │   │   │   └── SlicerSelector.tsx
│   │   │   ├── themeConfig.ts
│   │   │   ├── ui/
│   │   │   │   ├── Accordion.tsx
│   │   │   │   ├── BatchFileUploader.tsx
│   │   │   │   ├── BatchResultSummary.tsx
│   │   │   │   ├── Button.tsx
│   │   │   │   ├── Checkbox.tsx
│   │   │   │   ├── ColorModeBadge.tsx
│   │   │   │   ├── CropModal.tsx
│   │   │   │   ├── Dropdown.tsx
│   │   │   │   ├── FullScreenModal.tsx
│   │   │   │   ├── ImageUpload.tsx
│   │   │   │   ├── RadioGroup.tsx
│   │   │   │   ├── Slider.tsx
│   │   │   │   └── ZoomableImage.tsx
│   │   │   └── widget/
│   │   │       ├── ActionBarWidgetContent.tsx
│   │   │       ├── AdvancedSettingsWidgetContent.tsx
│   │   │       ├── BasicSettingsWidgetContent.tsx
│   │   │       ├── CalibrationWidgetContent.tsx
│   │   │       ├── CloisonneSettingsWidgetContent.tsx
│   │   │       ├── CoatingSettingsWidgetContent.tsx
│   │   │       ├── ColorWorkstation.tsx
│   │   │       ├── ExtractorWidgetContent.tsx
│   │   │       ├── FiveColorWidgetContent.tsx
│   │   │       ├── KeychainLoopWidgetContent.tsx
│   │   │       ├── LutManagerWidgetContent.tsx
│   │   │       ├── OutlineSettingsWidgetContent.tsx
│   │   │       ├── ReliefSettingsWidgetContent.tsx
│   │   │       ├── SnapGuides.tsx
│   │   │       ├── TabNavBar.tsx
│   │   │       ├── WidgetHeader.tsx
│   │   │       ├── WidgetPanel.tsx
│   │   │       └── WidgetWorkspace.tsx
│   │   ├── hooks/
│   │   │   ├── useActiveModelUrl.ts
│   │   │   ├── useAutoPreview.ts
│   │   │   ├── useConverterDataInit.ts
│   │   │   └── useThemeConfig.ts
│   │   ├── i18n/
│   │   │   ├── context.tsx
│   │   │   └── translations.ts
│   │   ├── index.css
│   │   ├── main.tsx
│   │   ├── setupTests.ts
│   │   ├── stores/
│   │   │   ├── __tests__/
│   │   │   │   ├── batchStore.property.test.ts
│   │   │   │   ├── batchStore.test.ts
│   │   │   │   ├── converterStore.property.test.ts
│   │   │   │   ├── converterStore.test.ts
│   │   │   │   ├── cropStore.property.test.ts
│   │   │   │   ├── cropStore.test.ts
│   │   │   │   ├── slicerStore.property.test.ts
│   │   │   │   └── slicerStore.test.ts
│   │   │   ├── aboutStore.ts
│   │   │   ├── calibrationStore.ts
│   │   │   ├── converterStore.ts
│   │   │   ├── extractorStore.ts
│   │   │   ├── fiveColorStore.ts
│   │   │   ├── lutManagerStore.ts
│   │   │   ├── settingsStore.ts
│   │   │   ├── slicerStore.ts
│   │   │   └── widgetStore.ts
│   │   ├── types/
│   │   │   └── widget.ts
│   │   └── utils/
│   │       ├── __tests__/
│   │       │   ├── colorUtils.property.test.ts
│   │       │   └── colorUtils.test.ts
│   │       ├── colorUtils.ts
│   │       ├── scaleUtils.ts
│   │       └── widgetUtils.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── icon.icns
├── lumina_studio.spec
├── lut-npy预设/
│   ├── Aliz/
│   │   ├── PETG/
│   │   │   ├── Aliz&4色PETG&CMYW.npy
│   │   │   ├── Aliz&4色PETG&RYBW.npy
│   │   │   ├── Aliz&6色PETG&CMYWGK.npy
│   │   │   ├── Aliz&6色PETG&RYBWGK.npy
│   │   │   ├── Aliz&8色PETG.npy
│   │   │   └── Aliz&PETG&5color&红-黄-蓝-白-黑-20260304.npy
│   │   ├── PLA/
│   │   │   ├── Aliz&4色PLA&CMYW.npy
│   │   │   ├── Aliz&4色PLA&RYBW.npy
│   │   │   ├── Aliz&6色PLA&CMYWGK.npy
│   │   │   ├── Aliz&6色PLA&RYBWGK.npy
│   │   │   └── Aliz&8色PLA.npy
│   │   └── 使用须知&开发组精校版20260402.txt
│   ├── BIQU/
│   │   ├── 必趣&PLAGO&2色BW.npy
│   │   ├── 必趣&PLAGO&4色RYBW.npy
│   │   ├── 必趣&PLAGO&6色RYBWGK.npy
│   │   └── 必趣颜色说明.txt
│   ├── CooBeen/
│   │   └── RYBW-CooBeen PETG.npy
│   ├── Creality/
│   │   └── RYBW-Creality Hyper PLA.npy
│   ├── Custom/
│   │   ├── Bambulab&PLA&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   ├── Bambulab&PLA&BW&白-黑.npy
│   │   ├── Bambulab&PLA&CMYW&青-品红-黄-绿-白-黑.npy
│   │   ├── Bambulab&PLA&RYBW&红-黄-蓝-白.npy
│   │   ├── Merged_8-Color+4-Color+6-Color+4-Color+6-Color+BW_20260307_091510.npz
│   │   ├── Merged_8-Color+6-Color+BW+4-Color_20260227_222117.npz
│   │   └── Merged_8-Color+BW+6-Color+4-Color_20260305_213304.npz
│   ├── Elegoo/
│   │   └── Elegoo_pla_RYBW.npy
│   ├── Jayo/
│   │   ├── Jayo&OW_PLA+_RYBW.npy
│   │   └── Readme.md
│   ├── Karvax/
│   │   ├── Karvax&PLA&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   ├── Karvax&PLA&RYBW&红-蓝-黄-白.npy
│   │   └── Karvax&PLA&RYBW&红-蓝-黄-绿-白-黑.npy
│   ├── LUT命名规范 LUT Naming Convention.md
│   ├── R3D/
│   │   └── R3DPLA青春版RYBW-PLA哑光中国红-PLA哑光黄-PLA哑光宝石蓝-PLA哑光白.npy
│   ├── Sanci/
│   │   └── PETG/
│   │       ├── 4色/
│   │       │   ├── Sanci&PETG&CMYW&粉红-黄-蓝-白.npy
│   │       │   └── Sanci&PETG&RYBW&红-黄-蓝-白.npy
│   │       ├── 6色/
│   │       │   └── Sanci&PETG&RYBW&红-黄-蓝-白-绿-黑.npy
│   │       └── 7色/
│   │           └── Sanci&PETG&Merged&红-黄-蓝-粉-绿-黑-白&20260402_215742.npz
│   ├── Snapmaker/
│   │   ├── 使用说明&开发组精校版&20260405.txt
│   │   ├── 快造&2色PLA&BW.npy
│   │   ├── 快造&4色PLA&B(蓝)MYW.npy
│   │   └── 快造&4色PLA&RYBW.npy
│   ├── XYD小明/
│   │   ├── PETG/
│   │   │   ├── XYD小明&PETG&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   │   ├── XYD小明&PETG&CMYW&青-品红-黄-白.npy
│   │   │   ├── XYD小明&PETG&CMYW&青-品红-黄-绿-白-黑.npy
│   │   │   ├── XYD小明&PETG&RYBW&红-蓝-黄-白.npy
│   │   │   └── XYD小明&PETG&RYBW&红-蓝-黄-绿-白-黑.npy
│   │   └── PLA/
│   │       ├── XYD小明&PLA&8色&红-品红-青-克莱因蓝-黄-白-绿-黑-20260327.npy
│   │       ├── XYD小明&PLA&CMYW&品红-青-黄-绿-白-黑-20260327.npy
│   │       ├── XYD小明&PLA&CMYW&青-品红-黄-白-20260327.npy
│   │       ├── XYD小明&PLA&RYBW&红-蓝-黄-绿-白-黑-20260331.npz
│   │       └── XYD小明&PLA&RYBW&红-黄-蓝-白-20260327.npy
│   ├── bambulab/
│   │   ├── Bambulab&PLA&4色&RYBW&红-蓝-黄-白.npy
│   │   ├── Bambulab&PLA&8色+4色.npz
│   │   ├── Bambulab&PLA&8色-New.npy
│   │   ├── README.txt
│   │   ├── bambulab_pla_basic_cmyw.npy
│   │   ├── bambulab_pla_basic_cmyw_new.npy
│   │   └── bambulab_pla_basic_rybw.npy
│   ├── npy预设说明 npy explanation.txt
│   ├── 天瑞/
│   │   └── 天瑞-PETG-ECO-NGYX-RYBW.npy
│   ├── 必应/
│   │   ├── 必应PETGHF.npy
│   │   ├── 必应plaf.npy
│   │   └── 必应使用说明.txt
│   ├── 瑞贝思/
│   │   ├── 2色/
│   │   │   └── 瑞贝思&PLA&BW2色&20260328.npy
│   │   ├── 4色/
│   │   │   └── 瑞贝思&PLA&RYBW4色&20260328.npy
│   │   └── 6色/
│   │       └── 瑞贝思&PLA&RYBW&红-蓝-黄-绿-白-黑&20260331.npz
│   ├── 精亮/
│   │   ├── 精亮&PLA4色&RYBW.npy
│   │   ├── 精亮&PLA6色&黑 白 品红 青 黄 绿.npy
│   │   ├── 精亮&PLA6色&黑 白 黄 绿 红 蓝.npy
│   │   └── 精亮&PLA8色.npy
│   ├── 纵维立方/
│   │   ├── 纵维立方&4色CMYW.npy
│   │   ├── 纵维立方&4色RYBW.npy
│   │   ├── 纵维立方&6色CMYWGK.npy
│   │   ├── 纵维立方&6色RYBWGK.npy
│   │   ├── 纵维立方&7色(青色蓝色共用蓝色).npy
│   │   ├── 纵维立方&8色.npy
│   │   ├── 纵维立方&PLA2色&BW.npy
│   │   └── 纵维立方颜色说明开发组预设.txt
│   ├── 赛纳/
│   │   ├── 使用说明.txt
│   │   ├── 赛纳&2色PLA&BW-白-黑.npy
│   │   ├── 赛纳&4色PETG&CMYW-品红-青-黄-白.npy
│   │   ├── 赛纳&4色PLA&CMYW-品红-青-黄-白.npy
│   │   ├── 赛纳&4色PLA&RYBW-红-黄-蓝-白.npy
│   │   ├── 赛纳&6色PLA&CMYWGK-品红-青-黄-白-绿-黑.npy
│   │   ├── 赛纳&6色PLA&RYBWGK-红-黄-蓝-白-绿-黑.npy
│   │   └── 赛纳&8色PLA-红-黄-蓝-品红-青-白-绿-黑.npy
│   ├── 通用LUT[有色差]RYBW General for personal use.npy
│   └── 魔创/
│       ├── 魔创&PETG2色.npy
│       ├── 魔创&PETG4色&CMYW.npy
│       ├── 魔创&PETG4色&RYBW.npy
│       ├── 魔创&PETG6色&CMYWGK.npy
│       ├── 魔创&PETG6色&RYBWGK.npy
│       ├── 魔创&PETG8色.npy
│       └── 魔创使用说明.txt
├── main.py
├── requirements.txt
├── tests/
│   ├── test_5color_merge_properties.py
│   ├── test_5color_merge_unit.py
│   ├── test_api_app_unit.py
│   ├── test_api_routers_unit.py
│   ├── test_api_schemas_properties.py
│   ├── test_bed_size_properties.py
│   ├── test_calibration_integration_unit.py
│   ├── test_calibration_routing_properties.py
│   ├── test_cleanup_output_dir_properties.py
│   ├── test_clear_cache_response_properties.py
│   ├── test_color_merge_map_properties.py
│   ├── test_color_merge_unit.py
│   ├── test_color_replace_unit.py
│   ├── test_converter_batch_unit.py
│   ├── test_converter_generate_unit.py
│   ├── test_converter_preview_unit.py
│   ├── test_converter_vector_export_unit.py
│   ├── test_converter_workers_unit.py
│   ├── test_crop_properties.py
│   ├── test_crop_unit.py
│   ├── test_extractor_integration_unit.py
│   ├── test_file_bridge_properties.py
│   ├── test_file_registry_clear_properties.py
│   ├── test_file_registry_properties.py
│   ├── test_five_color_api_unit.py
│   ├── test_five_color_query_properties.py
│   ├── test_health_lut_unit.py
│   ├── test_heic_support_properties.py
│   ├── test_heightmap_color_height_properties.py
│   ├── test_heightmap_properties.py
│   ├── test_heightmap_unit.py
│   ├── test_heightmap_upload_unit.py
│   ├── test_layout_new_tabs_unit.py
│   ├── test_lut_api_properties.py
│   ├── test_lut_api_unit.py
│   ├── test_lut_list_properties.py
│   ├── test_lut_merger_properties.py
│   ├── test_mime_type_properties.py
│   ├── test_naming_properties.py
│   ├── test_naming_unit.py
│   ├── test_palette_connected_selection_unit.py
│   ├── test_preview_click_selection_unit.py
│   ├── test_relief_mode_fix_properties.py
│   ├── test_relief_mode_fix_unit.py
│   ├── test_segmented_glb_properties.py
│   ├── test_segmented_glb_unit.py
│   ├── test_session_store_clear_properties.py
│   ├── test_session_store_properties.py
│   ├── test_session_thread_safety_properties.py
│   ├── test_session_ttl_properties.py
│   ├── test_settings_api_properties.py
│   ├── test_settings_api_unit.py
│   ├── test_slicer_properties.py
│   ├── test_slicer_unit.py
│   ├── test_stack_order_properties.py
│   ├── test_user_replacement_list_ui_unit.py
│   ├── test_vector_engine_unit.py
│   ├── test_worker_pool_properties.py
│   ├── test_worker_pool_unit.py
│   └── verify_merge_remap.py
├── ui/
│   ├── __init__.py
│   ├── callbacks.py
│   ├── crop_extension.py
│   ├── fivecolor_tab_v2.py
│   ├── layout_new.py
│   ├── palette_extension.py
│   └── styles.py
└── utils/
    ├── __init__.py
    ├── bambu_3mf_writer.py
    ├── color_recipe_logger.py
    ├── helpers.py
    ├── lut_manager.py
    └── stats.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.mypy_cache
.pytest_cache
.hypotheses


================================================
FILE: .github/workflows/build.yml
================================================
name: Build and Package Lumina Layers

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  build:
    name: Build (${{ matrix.platform_suffix }})
    runs-on: ${{ matrix.runs_on }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - runs_on: windows-latest
            platform_suffix: win
          - runs_on: macos-latest
            platform_suffix: mac

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Get Version and Hash
        shell: bash
        run: |
          VERSION=$(date +%Y%m%d)
          SHORT_SHA=$(git rev-parse --short=7 HEAD | cut -c1-7 || echo "0000000")
          BASE_NAME="Lumina-Layers-preview-$VERSION-$SHORT_SHA"
          ARTIFACT_NAME="${BASE_NAME}-${{ matrix.platform_suffix }}"

          echo "VERSION=$VERSION" >> "$GITHUB_ENV"
          echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
          echo "BASE_NAME=$BASE_NAME" >> "$GITHUB_ENV"
          echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> "$GITHUB_ENV"

      - name: Create virtual environment (Windows)
        if: matrix.platform_suffix == 'win'
        shell: pwsh
        run: |
          python -m venv .venv
          .\.venv\Scripts\python.exe -m pip install --upgrade pip
          .\.venv\Scripts\python.exe -m pip install pyinstaller
          .\.venv\Scripts\python.exe -m pip install -r requirements.txt

      - name: Create virtual environment (macOS)
        if: matrix.platform_suffix == 'mac'
        shell: bash
        run: |
          python -m venv .venv
          ./.venv/bin/python -m pip install --upgrade pip
          ./.venv/bin/python -m pip install pyinstaller
          ./.venv/bin/python -m pip install -r requirements.txt

      - name: Validate macOS icon asset
        if: matrix.platform_suffix == 'mac'
        shell: bash
        run: |
          test -f icon.icns

      - name: Package Windows build
        if: matrix.platform_suffix == 'win'
        shell: pwsh
        run: |
          .\.venv\Scripts\python.exe -m PyInstaller --noconfirm --onefile `
            --icon="icon.ico" `
            --add-data "icon.ico;." `
            --add-data "bambu_config_template.json;." `
            --add-data "assets;assets" `
            --collect-all "gradio" `
            --collect-all "safehttpx" `
            --collect-all "groovy" `
            --collect-all "gradio_client" `
            --collect-all "uvicorn" `
            --name "${{ env.BASE_NAME }}" `
            main.py

      - name: Package macOS build
        if: matrix.platform_suffix == 'mac'
        shell: bash
        run: |
          ./.venv/bin/python -m PyInstaller --noconfirm --windowed \
            --icon "icon.icns" \
            --add-data "icon.ico:." \
            --add-data "bambu_config_template.json:." \
            --add-data "assets:assets" \
            --add-data "lut-npy预设:lut-npy预设" \
            --collect-all "gradio" \
            --collect-all "safehttpx" \
            --collect-all "groovy" \
            --collect-all "gradio_client" \
            --collect-all "uvicorn" \
            --name "${BASE_NAME}" \
            main.py

      - name: Prepare Windows artifact directory
        if: matrix.platform_suffix == 'win'
        shell: pwsh
        run: |
          $targetDir = "${{ env.ARTIFACT_NAME }}"
          New-Item -ItemType Directory -Path "$targetDir" -Force | Out-Null

          $exePath = "dist/${{ env.BASE_NAME }}.exe"
          if (Test-Path $exePath) {
              Move-Item $exePath -Destination "$targetDir/"
          } else {
              Write-Error "Executable not found at $exePath"
              exit 1
          }

          if (Test-Path "icon.ico") {
              Copy-Item -Path "icon.ico" -Destination "$targetDir/"
          }
          if (Test-Path "bambu_config_template.json") {
              Copy-Item -Path "bambu_config_template.json" -Destination "$targetDir/"
          }
          if (Test-Path "assets") {
              Copy-Item -Path "assets" -Destination "$targetDir/assets" -Recurse
          }
          if (Test-Path "lut-npy预设") {
              Copy-Item -Path "lut-npy预设" -Destination "$targetDir/lut-npy预设" -Recurse
          }

      - name: Prepare macOS artifact directory
        if: matrix.platform_suffix == 'mac'
        shell: bash
        run: |
          target_dir="${ARTIFACT_NAME}"
          app_path="dist/${BASE_NAME}.app"

          mkdir -p "$target_dir"
          if [ ! -d "$app_path" ]; then
            echo "App bundle not found at $app_path" >&2
            exit 1
          fi

          mv "$app_path" "$target_dir/"

      - name: Verify Windows artifact contents
        if: matrix.platform_suffix == 'win'
        shell: pwsh
        run: |
          $targetDir = "${{ env.ARTIFACT_NAME }}"

          if (-not (Test-Path "$targetDir/${{ env.BASE_NAME }}.exe")) {
              Write-Error "Missing executable in artifact directory"
              exit 1
          }
          if (-not (Test-Path "$targetDir/icon.ico")) {
              Write-Error "Missing icon.ico in artifact directory"
              exit 1
          }
          if (-not (Test-Path "$targetDir/bambu_config_template.json")) {
              Write-Error "Missing bambu_config_template.json in artifact directory"
              exit 1
          }
          if (-not (Test-Path "$targetDir/assets")) {
              Write-Error "Missing assets directory in artifact directory"
              exit 1
          }
          if (-not (Test-Path "$targetDir/lut-npy预设")) {
              Write-Error "Missing LUT preset directory in artifact directory"
              exit 1
          }

      - name: Verify macOS artifact contents
        if: matrix.platform_suffix == 'mac'
        shell: bash
        run: |
          app_path="${ARTIFACT_NAME}/${BASE_NAME}.app"

          test -d "$app_path"
          find "$app_path" -type d -name assets | grep -q .
          find "$app_path" -type f -name bambu_config_template.json | grep -q .
          find "$app_path" -type d -name 'lut-npy预设' | grep -q .

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.ARTIFACT_NAME }}
          path: ${{ env.ARTIFACT_NAME }}/


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# Keep our custom spec file
# *.spec

# PyInstaller build outputs
build/
dist/
LuminaStudio_v*/

# PyInstaller test script
test_paths.py

# Debug and test files
run_debug.bat
test.png

# Release packages
*.rar
*.zip
*.7z

# Old version folders
Lumina-Layers-*/

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
.playwright-cli/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/

# pixi
#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
#   in the .venv directory. It is recommended not to include this directory in version control.
.pixi

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/

# Visual Studio Code
#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
#  you could uncomment the following to ignore the entire vscode folder
.vscode/

# Ruff stuff:
.ruff_cache/

# PyPI configuration file
.pypirc

# Cursor
#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
#  refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore

# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/

# Lumina Studio output files
output/*.3mf
output/*.glb
output/*.npy
output/lumina_stats.txt
output/*

# Gradio cache directory
output/.gradio_cache/
# ==========================================
# Lumina Studio Specific
# ==========================================

# 本地个人设置 (包含上次选择�?LUT 路径�?
user_settings.json

# 批量生成产生的压缩包
outputs/*.zip

# 核心输出目录 (保留目录结构,忽略具体文�?
outputs/*.3mf
outputs/*.glb
outputs/*.npy
outputs/batch_*/

# 指标统计文件
lumina_stats.txt
+lut-npy163623644576/Custom/*.npy
.venv312/
.venv*/


# Kiro hooks
.kiro/hooks/
lut-npyԤ��/Custom/*.npy

# Kiro documentation and internal files (personal use only)
.kiro/

# Reference projects (for development reference only)
LD_ColorLayering-main/
ChromaStack-main/

# Frontend
node_modules/
frontend/node_modules/
frontend/dist/

# Temporary assets
assets/temp_*.npy

# Local temporary files
.trae/
OPTIMIZATION_LOG.md


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to Lumina Studio are documented in this file.

[📖 中文更新日志 / Chinese Changelog](CHANGELOG_CN.md)

---

## v1.6.8 (2026-04-30)

### Bug Fixes
- **fix(color)**: Distinguished CMYW and RYBW as separate color subtypes in `infer_color_mode()` and `ColorSystem.get()` — CMYW LUTs were previously misidentified as RYBW, causing wrong slot names, wrong filament colors, and wrong corner labels in 3MF export
- **fix(color)**: Preserved CMYW/RYBW subtype through the full pipeline: LUT detection → config lookup → API schemas → Gradio UI → 3MF export, ensuring Cyan/Magenta/Yellow slots are used instead of Red/Blue/Yellow
- **fix(3mf)**: Fixed `bambu_3mf_writer.py` overwriting CMYW/RYBW to generic "4-Color" mode, which always fell back to RYBW color mapping
- **fix(ui)**: Added CMYW and RYBW as distinct color mode options in all Gradio Radio components (converter, calibration, extractor tabs)
- **fix(ui)**: Fixed `generate_board_wrapper` in Gradio UI always passing "RYBW" to calibration generator regardless of selected mode
- **fix(ui)**: Fixed `<label htmlFor>` not matching any element `id` in Dropdown and Slider React components
- **fix(color)**: Updated Cyan/Magenta/Yellow filament colors to pure RGB values (`#00FFFF`/`#FF00FF`/`#FFFF00`) across CMYW, 6-Color, and 8-Color modes for accurate 3MF rendering

### Changes
- Added `CMYW` and `RYBW` enum values to `CalibrationColorMode`, `ColorMode` (backend schemas and frontend TypeScript types)
- Added CMYW/RYBW routing entries in calibration API router and test routing maps

---

## v1.6.7 (2026-03-29)

### Bug Fixes
- **fix(lut)**: Fixed critical bug where 6-Color RYBWGK LUT users (e.g. 瑞贝思) received wrong filament color assignments in BambuStudio AMS — the 3MF was hard-coded with CMYWGK slot preview colors (Cyan/Magenta/Green/Yellow) instead of the actual calibrated filament colors (Red/Yellow/Blue/Green), causing users to load the wrong filament in each slot and producing incorrect print results
- **fix(lut)**: Preview colors in generated 3MF files now derive from the LUT's own pure-color calibration entries (`(i,i,i,i,i)` stacks) rather than the static `ColorSystem` defaults, making them accurate for any 4-Color, 6-Color, or 8-Color calibration regardless of filament brand or color variant

---

## v1.6.6 (2026-03-29)

### Bug Fixes
- **fix(lut)**: Fixed 6-Color LUTs with generic filenames (e.g. `lumina_lut (8).npy`) being misidentified as 4-Color mode, causing incorrect layer stacking and inverted print output
- **fix(lut)**: Fixed 6-Color RYBWGK LUTs with "RYBW" in filename being misidentified as 4-Color mode due to keyword matching taking precedence over file size detection
- **fix(recipe)**: Fixed color recipe report displaying bottom-to-top and top-to-bottom layer indices in reversed order

### Improvements
- **feat(ui)**: Added filament color dot badges next to LUT dropdown in both Gradio and React frontends — shows actual filament colors for the detected mode (4/6/8-Color, Merged, BW)
- **fix(ui)**: Updated color dot values to match actual filament hex values from config (Magenta `#EC008C`, Red `#DC143C`, etc.)
- **refactor(lut)**: Reordered `infer_color_mode` detection priority: explicit numeric keywords → file size → color-system keywords, preventing RYBWGK from matching RYBW (4-Color)

---

## v1.6.5 (2026-03-25)

### Improvements
- Added a visible "✕ Back" button at the top-left corner of the fullscreen 3D preview, allowing users to easily exit fullscreen mode (previously, the only exit was hidden in the small 2D thumbnail at the bottom-right corner)

---

## v1.6.4 (2026-03-12)

### Bug Fixes
- Fixed SVG mode "separate backing plate" checkbox being ignored; backing plate was always exported as a separate object regardless of setting
- Fixed SVG mode backing plate color appearing gray instead of white (Board slot now correctly falls back to white when not matched in color system)
- Fixed SVG mode backing plate and adjacent color layer gaps/through-cracks: root cause was v1.6.3 geometry clipping independently calling `simplify()` on each shape, causing shared boundary coordinate misalignment; replaced with `set_precision(grid_size=1e-6)` to snap all shape vertices to the same precision grid, eliminating gaps at the source
- Fixed converter page width/height linkage not triggering on the first manual Enter after flow "select image A -> generate -> remove -> select image B" (root cause: `lastValue` baseline was not synchronized after input remount)
- Fixed manual width/height integer input being overwritten to decimal values (e.g. `240 -> 240.1`): linkage output is now normalized to integers and aligned with `step=1`

---

## v1.6.3 (2026-03-08)

### Features
- **Cloisonné Mode** - Auto-extract color boundaries to generate metal wire frames; wire exported as independent object for metallic material assignment; adjustable wire width (0.2-1.2mm) and height (0.04-1.0mm); enforced single-sided mode
- **Free Color Mode** - Use any RGB color beyond LUT constraints; custom color sets with independent 3MF object export
- **Transparent Coating Layer** - Add transparent protective layer at model bottom; adjustable height (0.04-0.12mm); independent object export for transparent material
- **Outline Border** - Add customizable border around model; adjustable width (0.5-5.0mm); smart integration with coating layers
- **Card Palette Layout** - Display LUT colors in physical calibration board spatial arrangement; 8-color auto A/B group split; toggle between block/card layout
- **Color Search & Filter** - Color picker search, Hex/RGB text search, hue family filtering (Red/Orange/Yellow/Green/Cyan/Blue/Purple/Neutral), auto-scroll with breathing light animation
- **2.5D Relief Mode** - Height-based modeling with independent Z-axis heights per color; optical layering preserved (top 5 layers); auto height generator (Min-Max normalization); heightmap support (PNG/JPG/BMP); smart validation for aspect ratio and contrast
- **Isolated Pixel Cleanup** - Automatic noise reduction for isolated color pixels; auto-enabled in High-Fidelity mode
- **Connected Region Color Replacement** - Local color replacement by 4-connected regions; dual-list palette (user replacement / auto-matched); click-to-replace on 2D preview
- **CIELAB Perceptual Color Matching** - Color matching switched from RGB Euclidean distance to CIELAB perceptual uniform space; applied to all color matching operations
- **Automatic Color Merging** - Low-usage color consolidation with CIELAB Delta-E distance; adjustable threshold with preview/apply/revert; test case: 390 → 62 colors (84% reduction)
- **Slicer Integration** - One-click launch for Bambu Studio / OrcaSlicer / ElegooSlicer; direct workflow without manual drag-and-drop; persistent slicer selection
- **Complete BambuStudio 3MF Export** - Full multi-material support with proper object naming and metadata integration
- **5-Color Extended Mode** - Full pipeline support for 5-color extended mode (extractor/converter/naming/UI)
- **Color Recipe Logging** - Color mapping documentation and logging system with download support
- **Clear Output** - Support clearing output directory with real-time size display
- **Large Canvas Option** - Advanced option to remove size limits
- **Progress Display** - Real-time progress feedback across convert_image_to_3d/svg_to_mesh stages

### Bug Fixes
- Fixed 8-color stacking order causing incorrect color mixing
- Fixed 8-color ref_stacks format consistency with 4-color/6-color [top...bottom]
- Fixed viewing surface (Z=0) and back surface inversion
- Fixed RYBW mode incorrectly detected as BW mode
- Fixed color replacement now correctly updates material_matrix stacking data
- Fixed outline mesh missing on image boundary edges
- Fixed missing import for safe_fix_3mf_names in BW calibration generation
- Fixed coating/outline compatibility when both features enabled simultaneously
- Fixed relief/cloisonné mutual exclusion with auto-disable and info toast
- Fixed preview click coordinate transformation for Gradio 6.0
- Fixed 5-Color high-fidelity top/bottom and left/right orientation
- Fixed 5-Color Extended model orientation and SVG layer count
- Disabled 2.5D relief for 5-Color Extended mode
- Fixed 2.5D relief mode state leaks, hex key mismatch and parameter clamping
- Fixed SVG subpath handling
- Fixed color_recipe_path return value inconsistency after main branch merge
- Removed svg_to_mesh internal progress calls to avoid Gradio event loop GIL interference
- Removed redundant preview pre-generation in generate_with_auto_preview
- Fixed HEIC format support: frontend display, upload file_types, passback conversion
- Fixed cached 3MF invalidation when parameters change
- Fixed ModelingMode None crash on image upload
- Unified 8-color stacks asset path resolution for PyInstaller
- Restored ZIP_DEFLATED compression and indent alignment
- Removed leftover conflict marker in bambu_3mf_writer.py
- Removed file_types from gr.Image calls (incompatible with Gradio 6.5.1)
- Fixed SVG upload crash due to Gradio base64 preprocessing bug
- Fixed SVG geometry crop: replaced pixel-based Dual-Pass Crop with geometry-based bounds crop
- Fixed SVG even-odd fill rule when combining subpaths
- Fixed icon.ico regenerated as square sizes and bundled in PyInstaller
- Fixed slicer launch return count mismatch (5 outputs)
- Fixed bambu_config_template.json not bundled in PyInstaller and artifact; fixed frozen path resolution
- Removed unused create_5color_combination_tab and duplicate helper functions from layout_new.py

### Features (Post-Release)
- **Multi-Color LUT Support** - Added multi-color LUT support and color recipe query functionality

### Performance
- Full pipeline speed optimization: SVG mode UI ~140s → ~51s (2.7x speedup)
- Optimized 3MF generation pipeline: vectorized mesh, parallel generation, streaming export, SVG caching

### Other
- Relicensed project to GPLv3
- Relief max height default adjusted to 2.4; coating slider range to 0.08-0.16
- Standardized status messages by removing emoji characters

---

## v1.5.9 (2026-02-26)

### Code Quality
- Replaced all bare exception catches (`except:`) with `except Exception:`

---

## v1.5.8 (2026-02-25)

### Features
- **Isolated Pixel Cleanup** - Auto-enabled in High-Fidelity mode; smart detection and merging of isolated color blocks
- **Backing Separation** - Backing exported as independent object; fixed backing layer hardcoding and parameter passing

---

## v1.5.7 (2026-02-10)

### Features
- **6-Color Extended Mode** - 1296 colors (6 base filaments × 3 layers) for wider color gamut
- **8-Color Professional Mode** - 2738 colors (8 base filaments × 2 pages) for maximum color range
- **Two-Page Workflow** - 8-color mode uses two calibration boards merged into single LUT
- **Manual Color Correction** - Click any color cell to manually adjust RGB values before merging
- **Smart Corner Detection** - Automatic corner marker colors based on selected mode
- **BW Grayscale Mode** - 32-level grayscale calibration for monochrome prints
- **LUT Merging with Stacking Preservation** - Combine multiple LUTs (8+6+4+BW); NPZ format with colors and stacking arrays; intelligent reconstruction; full color replacement support
- **Docker Support** - Dockerfile for containerized deployment
- **Unified 4-Color Architecture** - Unified 4-color mode architecture with full automated test suite

### Bug Fixes
- Fixed 8-color stacking order causing incorrect color mixing
- Fixed 8-color ref_stacks format consistency [top...bottom]
- Fixed viewing surface (Z=0) and back surface inversion
- Fixed RYBW mode incorrectly detected as BW mode
- Fixed RYBW calibration board color recognition
- Fixed 8-color manual correction persistence after merge
- Fixed Mac UI styling issues
- Width/height slider input blur triggers linked calculation to avoid frequent jumps during manual input

---

## v1.5.6 (2026-02-08)

### Features
- **Complete 8-Color Image Conversion** - Full 8-color mode support in UI; auto-detection for 2600-2800 color range LUTs
- **ModelingMode Enum Migration** - Migrated modeling mode from string comparison to ModelingMode enum

### Bug Fixes
- Fixed 8-color mode stacking effect
- Fixed about page missing v1.5.4 version number

---

## v1.5.5 (2026-02-07)

### Features
- 8-color calibration board algorithm optimization and quality improvement

---

## v1.5.4 (2026-02-06)

### Features
- **Vector Mode Improvements** - Boolean operation optimization for color overlap handling; SVG order preservation for correct layering; micro Z-offset (0.001mm) for detail independence; enhanced small feature protection

### Bug Fixes
- Fixed black background in vector mode 2D preview
- Fixed preview click coordinate transformation for Gradio 6.0
- Added missing colormath library to requirements

### Other
- Removed deprecated layout.py

---

## v1.5.3 (2026-02-05)

### Features
- **Image Cropping** - Non-invasive image cropping with aspect ratio presets
- **Color Analyzer** - Extracted color recommendation algorithm to independent ColorAnalyzer module
- **Auto Color Detail Button** - Added width factor support; fixed duplicate click toast issue
- **Color Replacement Undo** - Added undo functionality; fixed quantized color count parameter not passed

### Performance
- Vectorized color mapping with RGB encoding + searchsorted
- Vectorized _greedy_rect_merge with NumPy operations

---

## v1.5.1 (2026-02-03)

### Features
- **Complete UI Overhaul** - Full UI redesign with batch mode implementation and i18n support
- **Preview Follows Modeling Mode** - Preview updates based on modeling mode selection
- **Greedy Rectangle Merge** - Optimized 3MF face count with greedy rectangle merging algorithm

### Performance
- K-Means pre-scaling optimization: 20-50x speedup for large images

### Bug Fixes
- Fixed single-sided 3MF output X-axis mirroring
- Reverted smart mesh simplification to fix missing mesh bugs
- Fixed merge conflicts in i18n.py

---

## v1.5.0 (2026-02-01)

### Features
- **Code Standardization** - All code comments translated to English; unified Google-style docstrings; removed redundant comments

---

## v1.4.2 (2026-01-31)

### Features
- **Tray Icon i18n** - Multi-language support for system tray icon menu options

### Bug Fixes
- Version number update and bug fixes
- Reverted "3MF color injection" feature

---

## v1.4.1 (2026-01-29)

### Features
- **Modeling Mode Consolidation** - High-Fidelity mode replaces Vector & Woodblock modes; two unified modes (High-Fidelity / Pixel Art)
- **Dynamic Language Toggle** - Click language button to switch Chinese/English; full UI translation without page reload
- **Output Directory** - Save output files to local project output directory instead of C: temp
- **Gradio Temp Directory Redirect** - Redirected Gradio temp directory to project directory

### Bug Fixes
- Fixed 3MF naming issue when colors are missing; use local output dir
- Fixed transparent background recognition issue

---

## v1.4 (2026-01-20)

### Features
- **Three Modeling Modes** - Vector Mode (smooth curves, OpenCV contour extraction), Woodblock Mode (SLIC superpixels + detail preservation), Voxel Mode (blocky geometry)
- **Color Quantization Engine** - "Cluster First, Match Second" with K-Means clustering (8-256 colors); 1000x speed improvement; spatial denoising
- **Resolution Decoupling** - Vector/Woodblock: 10 px/mm, Voxel: 2.4 px/mm
- **Smart 3D Preview Downsampling** - Large models auto-simplify preview
- **Browser Crash Protection** - Detects model complexity, disables preview for 2M+ pixels
- **System Tray Integration** - System tray icon with macOS title bar support
- **Modular Code Structure** - Refactored into Core/UI/Utils modules
- **Auto Port Selection** - Automatically selects available port to avoid conflicts

### Bug Fixes
- Fixed Gradio 6.0+ compatibility
- Fixed macOS 26812 trace trap memory issue
- Fixed cumulative generation statistics font color
- Fixed language switch button styling
- Fixed Windows image causing errors on deletion

---

## v1.3 (2026-01-18)

### Features
- **Bilingual UI** - Chinese/English labels throughout the interface
- **Live 3D Preview** - Interactive preview with actual LUT-matched colors
- **Dual Color Modes** - Full support for both CMYW and RYBW color systems

### Bug Fixes
- Fixed 3MF naming (slicer shows correct color names)
- Optimized default gap to 0.82mm for standard line widths

---

## v1.2 (2026-01-17)

### Features
- **Unified Application** - All three tools (Calibration Generator, Color Extractor, Image Converter) merged into single application

---

## v1.0 (2026-01-15)

### Initial Release
- Calibration board generator
- Color extractor with computer vision
- Image-to-3D converter with LUT-based color matching
- CMYW/RYBW color system support
- 3MF export for BambuStudio compatibility


================================================
FILE: CHANGELOG_CN.md
================================================
# 更新日志

Lumina Studio 所有重要变更记录。

[📖 English Changelog / 英文更新日志](CHANGELOG.md)

---

## v1.6.8 (2026-04-30)

### Bug 修复
- **fix(color)**: 区分 CMYW 和 RYBW 为独立颜色子类型——`infer_color_mode()` 和 `ColorSystem.get()` 现在正确识别 CMYW LUT,不再统一回退到 RYBW,修复了 3MF 导出中槽名、耗材颜色、角标全部错误的问题
- **fix(color)**: CMYW/RYBW 子类型现在贯穿完整管线:LUT 检测 → 配置查找 → API Schema → Gradio UI → 3MF 导出,确保青/品红/黄槽位正确映射
- **fix(3mf)**: 修复 `bambu_3mf_writer.py` 将 CMYW/RYBW 覆盖为通用 "4-Color" 模式导致始终回退到 RYBW 颜色映射的问题
- **fix(ui)**: Gradio 所有 Radio 组件(转换器/校准板/提取器 tab)新增 CMYW 和 RYBW 独立选项
- **fix(ui)**: 修复 Gradio 校准板生成器始终传 "RYBW" 给生成函数的问题
- **fix(ui)**: 修复 React Dropdown 和 Slider 组件 `<label htmlFor>` 与元素 `id` 不匹配的无障碍报错
- **fix(color)**: Cyan/Magenta/Yellow 耗材色值统一更新为纯 RGB(`#00FFFF`/`#FF00FF`/`#FFFF00`),适用于 CMYW、6色、8色模式

### 变更
- `CalibrationColorMode`、`ColorMode`(后端 Schema 和前端 TypeScript)新增 `CMYW` 和 `RYBW` 枚举值
- 校准板 API 路由和测试路由表新增 CMYW/RYBW 条目

---

## v1.6.7 (2026-03-29)

### Bug 修复
- **fix(lut)**: 修复关键 Bug:6色 RYBWGK 用户(如瑞贝思)生成的 3MF 文件中 AMS 耗材颜色分配错误——原来固定使用 CMYWGK 色槽预览色(青/品红/绿/黄),而非实际标定的耗材颜色(红/黄/蓝/绿),导致用户在错误的 AMS 槽位装入错误耗材,打印结果颜色错乱
- **fix(lut)**: 生成的 3MF 文件中材料预览颜色现在从 LUT 自身的纯色标定条目(`(i,i,i,i,i)` 叠层)推导,而非使用 `ColorSystem` 中的静态默认值,适用于任意品牌、任意颜色变体的 4色/6色/8色标定

---

## v1.6.6 (2026-03-29)

### Bug 修复
- **fix(lut)**: 修复通用文件名 LUT(如 `lumina_lut (8).npy`)被错误识别为 4-Color 模式,导致层序错误、打印结果颠倒的问题
- **fix(lut)**: 修复含 "RYBW" 关键词的 6-Color RYBWGK 文件名被误判为 4-Color 模式的问题(关键词检测优先于文件大小检测)
- **fix(recipe)**: 修复颜色配方报告中底→顶与顶→底层序索引方向颠倒显示的问题

### 改进
- **feat(ui)**: Gradio 和 React 前端 LUT 下拉框旁新增耗材颜色小球标识,实时显示当前色卡对应的耗材颜色(支持 4/6/8色、合并、黑白模式)
- **fix(ui)**: 颜色小球色值改用 config.py 实际耗材色值(品红 `#EC008C`、红 `#DC143C` 等),视觉区分更明确
- **refactor(lut)**: 重排 `infer_color_mode` 检测优先级:明确数字关键词 → 文件大小 → 颜色系关键词,防止 RYBWGK 被 RYBW(4色)误匹配

---

## v1.6.5 (2026-03-25)

### 改进
- 在 3D 全屏预览左上角新增醒目的「✕ 返回」按钮,方便用户快速退出全屏模式(此前退出入口仅在右下角 2D 缩略图中,不易发现)

---

## v1.6.4 (2026-03-12)

### Bug 修复
- 修复 SVG 模式"底板单独一个对象"勾选状态被忽略,底板始终作为独立对象导出的问题
- 修复 SVG 模式底板颜色为灰色而非白色的问题(Board 槽位无法映射到颜色系统时现回退为白色)
- 修复 SVG 模式底板及相邻颜色层之间出现细缝/贯穿缝的问题:根因为 v1.6.3 几何裁剪对每个形状独立调用 `simplify()`,导致相邻形状共享边界坐标错位;改用 `set_precision(grid_size=1e-6)` 将所有形状顶点对齐到同一精度网格,从根源消除间隙
- 修复转换页在“选图 A -> 生成 -> 叉掉 -> 重选图 B”后,宽高参数首次手动输入回车不触发联动的问题(根因是输入框重建后 `lastValue` 基线未同步)
- 修复宽高参数手动输入整数后被回写为小数(如 `240 -> 240.1`)的问题:联动结果统一为整数并与 `step=1` 保持一致

---

## v1.6.3 (2026-03-08)

### 新功能
- **掐丝珐琅模式** - 自动提取颜色边界生成金属丝线框;丝线作为独立对象导出,可在切片软件中单独指定金属材料;丝线宽度(0.2-1.2mm)和高度(0.04-1.0mm)可调;强制单面模式
- **自由配色模式** - 突破 LUT 色彩限制,使用任意 RGB 颜色;自定义色彩集合,每个颜色独立导出为 3MF 对象
- **透明镀层** - 在模型底部添加透明保护层;镀层高度可调(0.04-0.12mm);作为独立对象导出,可指定透明材料
- **外轮廓** - 为模型添加可定制边框;轮廓宽度可调(0.5-5.0mm);同时开启镀层时自动延伸覆盖
- **色卡布局模式** - 按物理校准板的空间排列显示 LUT 颜色;8色 LUT 自动分为 A/B 两组并排显示;可在高级设置中切换色块/色卡模式
- **颜色搜索与过滤** - 以色找色(ColorPicker 选色自动匹配)、文本搜索(Hex/RGB)、色系过滤(红/橙/黄/绿/青/蓝/紫/中性色)、匹配色块自动滚动带呼吸灯动效
- **2.5D 浮雕模式** - 为不同颜色分配独立 Z 轴高度;保留顶部 5 层光学叠色;自动高度生成器(Min-Max 归一化);高度图支持(PNG/JPG/BMP);宽高比偏差和低对比度智能验证
- **孤立像素清理** - 智能检测并合并孤立色块,减少打印瑕疵;高保真模式下自动启用
- **连通域颜色替换** - 按量化色 4 邻接连通域替换颜色;双列表调色板(用户替换/自动配准);点击 2D 预览选择连通区域替换
- **CIELAB 感知色彩匹配** - 颜色匹配从 RGB 欧氏距离切换到 CIELAB 感知均匀空间;应用于所有颜色匹配操作
- **自动颜色合并** - 低使用率颜色整合,使用 CIELAB Delta-E 距离;可调阈值,带预览/应用/撤销;测试案例:390色 → 62色(减少84%)
- **切片器集成** - 一键启动 Bambu Studio / OrcaSlicer / ElegooSlicer;生成后直接打开,无需手动拖拽;记忆上次选择
- **完整 BambuStudio 3MF 导出** - 完整多材料支持,正确对象命名和元数据集成
- **5色扩展模式** - 5色通道全链路支持(extractor/converter/naming/UI)
- **颜色配方日志** - 颜色映射文档和日志系统,支持下载
- **清空输出** - 支持清空输出目录并实时显示输出大小
- **大画幅选项** - 高级选项解除尺寸限制
- **进度显示** - 完善进度显示,各阶段实时反馈

### Bug 修复
- 修复 8 色堆叠顺序错误导致的叠色效果不正确
- 修复 8 色 ref_stacks 格式与 4 色/6 色一致性 [顶...底]
- 修复观赏面(Z=0)和背面颠倒的问题
- 修复 RYBW 模式被错误检测为 BW 模式
- 修复颜色替换现在正确更新 material_matrix 堆叠数据
- 修复图像边界边缘缺少外轮廓网格
- 修复黑白校准板生成时缺少 safe_fix_3mf_names 导入
- 修复同时开启镀层和外轮廓时的兼容性问题
- 修复浮雕/掐丝珐琅互斥,带中英文提示信息
- 修复预览图点击坐标变换(适配 Gradio 6.0)
- 修复 5 色高保真顶底与左右朝向
- 修复 5 色扩展模型方向与 SVG 层数
- 禁用 5 色扩展模式的 2.5D 浮雕
- 修复 2.5D 浮雕模式状态泄漏、hex key 不匹配和参数钳制
- 修复 SVG subpath 处理
- 修复 main 分支合并后 color_recipe_path 返回值不一致
- 移除 svg_to_mesh 内部 progress 调用,避免 Gradio 事件循环唤醒干扰 GIL
- 移除 generate_with_auto_preview 中的冗余预览预生成
- 修复 HEIC 格式支持:前端显示、上传文件类型、回传转换
- 修复参数变更时缓存 3MF 失效
- 修复图片上传时 ModelingMode None 崩溃
- 统一 8 色 stacks 资源路径解析(PyInstaller 兼容)
- 恢复 ZIP_DEFLATED 压缩和缩进对齐
- 移除 bambu_3mf_writer.py 中残留的冲突标记
- 移除 gr.Image 调用中的 file_types(与 Gradio 6.5.1 不兼容)
- 修复 SVG 上传因 Gradio base64 预处理 bug 导致崩溃
- 修复 SVG 几何裁剪:将基于像素的双通道裁剪替换为基于几何边界的裁剪
- 修复 SVG 合并子路径时的奇偶填充规则
- 修复 icon.ico 重新生成为正方形尺寸并打包到 PyInstaller
- 修复切片器启动返回值数量不匹配(5 个输出)
- 修复 bambu_config_template.json 未打包到 PyInstaller 和构建产物;修复冻结路径解析
- 移除 layout_new.py 中未使用的 create_5color_combination_tab 和重复辅助函数

### 新功能(发布后补充)
- **多颜色 LUT 支持** - 添加多颜色 LUT 支持和配色查询功能

### 性能优化
- 全流程速度优化:SVG 模式 UI 耗时 ~140s → ~51s(2.7x 加速)
- 优化 3MF 生成管线:向量化网格、并行生成、流式导出、SVG 缓存

### 其他
- 项目协议变更为 GPLv3
- 浮雕最大高度默认调整为 2.4;镀层滑块范围调整为 0.08-0.16
- 标准化状态消息,移除 emoji 字符

---

## v1.5.9 (2026-02-26)

### 代码质量
- 将所有裸异常捕获(`except:`)替换为 `except Exception:`

---

## v1.5.8 (2026-02-25)

### 新功能
- **孤立像素清理** - 高保真模式自动启用;智能检测并合并孤立色块
- **底板分离** - 底板作为独立对象导出;修复 backing 层硬编码和参数传递问题

---

## v1.5.7 (2026-02-10)

### 新功能
- **6 色扩展模式** - 1296 色(6 种基础耗材 × 3 层),更广色域
- **8 色专业模式** - 2738 色(8 种基础耗材 × 2 页),最大色彩范围
- **双页工作流** - 8 色模式使用两块校准板合并为单个 LUT
- **手动颜色修正** - 点击任意色块手动调整 RGB 值
- **智能角点检测** - 根据所选模式自动识别角点标记颜色
- **黑白灰度模式** - 32 级灰度校准,用于单色打印
- **LUT 合并与堆叠信息保留** - 组合多个 LUT(8+6+4+黑白);NPZ 格式包含颜色和堆叠数组;智能重建;完全兼容颜色替换
- **Docker 支持** - 添加 Dockerfile 支持容器化部署
- **统一 4 色模式架构** - 统一 4 色模式架构 + 全自动测试套件

### Bug 修复
- 修复 8 色堆叠顺序错误导致的叠色效果不正确
- 修复 8 色 ref_stacks 格式一致性 [顶...底]
- 修复观赏面(Z=0)和背面颠倒
- 修复 RYBW 模式被错误检测为 BW 模式
- 修复 RYBW 校准板颜色识别问题
- 修复 8 色手动校色合并后不持久
- 修复 Mac 上的 UI 样式问题
- 宽高 Slider 输入框失焦触发联动计算,避免手动输入时频繁跳动

---

## v1.5.6 (2026-02-08)

### 新功能
- **完整 8 色图像转换** - UI 新增 8 色模式支持;8 色 LUT 自动检测(2600-2800 色范围)
- **ModelingMode 枚举迁移** - 将建模模式从字符串比较迁移到 ModelingMode 枚举

### Bug 修复
- 修复 8 色模式叠色效果
- 修复关于页面中遗漏的 v1.5.4 版本号

---

## v1.5.5 (2026-02-07)

### 新功能
- 8 色校准板算法优化与质量提升

---

## v1.5.4 (2026-02-06)

### 新功能
- **矢量模式改进** - 布尔运算优化颜色重叠处理;SVG 顺序保持确保正确层叠;微 Z 偏移(0.001mm)保持细节独立;增强小特征保护

### Bug 修复
- 修复矢量模式 2D 预览黑色背景
- 修复预览点击坐标变换(Gradio 6.0)
- 向 requirements 文件中添加缺少的 colormath 库

### 其他
- 移除废弃的 layout.py

---

## v1.5.3 (2026-02-05)

### 新功能
- **图片裁剪** - 非侵入式图片裁剪功能,支持宽高比预设
- **色彩分析器** - 将色彩推荐算法抽取到独立 ColorAnalyzer 模块
- **自动计算色彩细节按钮** - 添加宽度因子支持;修复重复点击无 toast 问题
- **颜色替换撤销** - 添加撤销功能;修复量化颜色数参数未传递的 bug

### 性能优化
- 向量化颜色映射(RGB 编码 + searchsorted)
- 向量化 _greedy_rect_merge(NumPy 操作)

---

## v1.5.1 (2026-02-03)

### 新功能
- **全面 UI 重构** - 全新 UI 设计,批量模式实现,国际化支持
- **预览跟随建模模式** - 预览根据建模模式选择更新
- **贪婪矩形合并** - 优化 3MF 面数的贪婪矩形合并算法

### 性能优化
- K-Means 预缩放优化:大图加速 20-50 倍

### Bug 修复
- 修复单面模式 3MF 输出 X 轴镜像
- 撤销智能网格简化以修复缺失网格 bug
- 修复 i18n.py 合并冲突

---

## v1.5.0 (2026-02-01)

### 新功能
- **代码标准化** - 所有代码注释翻译为英文;统一 Google-style docstrings;移除冗余注释

---

## v1.4.2 (2026-01-31)

### 新功能
- **托盘图标国际化** - 为托盘图标菜单选项添加多语言支持

### Bug 修复
- 版本号更新和 bug 修复
- 撤销"3MF 颜色注入功能"

---

## v1.4.1 (2026-01-29)

### 新功能
- **建模模式整合** - 高保真模式取代矢量模式和版画模式;两种统一模式(高保真/像素艺术)
- **动态语言切换** - 点击语言按钮即可在中英文之间切换;全界面翻译无需刷新页面
- **输出目录** - 将输出文件保存到项目本地 output 目录,不再写入 C 盘临时目录
- **Gradio 临时目录重定向** - 将 Gradio 临时目录重定向到项目目录

### Bug 修复
- 修复颜色缺失时的 3MF 命名问题;使用本地输出目录
- 修复透明背景无法识别的问题

---

## v1.4 (2026-01-20)

### 新功能
- **三种建模模式** - 矢量模式(平滑曲线、OpenCV 轮廓提取)、版画模式(SLIC 超像素 + 细节保护)、像素模式(方块几何)
- **色彩量化引擎** - "先聚类,后匹配",K-Means 聚类(8-256 色);速度提升 1000 倍;空间去噪
- **分辨率解耦** - 矢量/版画模式 10 px/mm,像素模式 2.4 px/mm
- **3D 预览智能降采样** - 大模型自动简化预览
- **浏览器崩溃保护** - 检测模型复杂度,超 200 万像素禁用预览
- **系统托盘集成** - 系统托盘图标,支持 macOS 标题栏
- **模块化代码结构** - 重构为 Core/UI/Utils 模块
- **自动端口选择** - 自动选择可用端口避免冲突

### Bug 修复
- 修复 Gradio 6.0+ 兼容性
- 修复 macOS 26812 trace trap 内存问题
- 修复累计生成统计数字字体颜色
- 修复语言切换按钮样式
- 修复 Windows 下导致错误的图片删除

---

## v1.3 (2026-01-18)

### 新功能
- **双语界面** - 界面全程中英文标签
- **实时 3D 预览** - 交互式预览,显示实际 LUT 匹配的颜色
- **双色彩模式** - 完整支持 CMYW 和 RYBW 色彩系统

### Bug 修复
- 修复 3MF 命名(切片器显示正确颜色名称)
- 优化默认间隙为 0.82mm,适配标准线宽

---

## v1.2 (2026-01-17)

### 新功能
- **统一应用** - 三大工具(校准板生成器、颜色提取器、图像转换器)合并为单个应用

---

## v1.0 (2026-01-15)

### 首次发布
- 校准板生成器
- 基于计算机视觉的颜色提取器
- 基于 LUT 色彩匹配的图像转 3D 转换器
- CMYW/RYBW 色彩系统支持
- 3MF 导出(BambuStudio 兼容)


================================================
FILE: Dockerfile
================================================
# Use an official Python runtime as a parent image
FROM python:3.13-slim

# Set the working directory in the container
WORKDIR /app

# Install system dependencies required for pycairo and opencv
# libcairo2-dev, pkg-config -> for pycairo
# libgl1, libglib2.0-0 -> for opencv-python
RUN apt-get update && apt-get install -y \
    gcc \
    pkg-config \
    libcairo2-dev \
    libgl1 \
    libglib2.0-0 \
    && rm -rf /var/lib/apt/lists/*

# Copy the requirements file into the container at /app
COPY requirements.txt /app/

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . /app/

# Expose the port Gradio runs on
EXPOSE 7860

# Define environment variable to ensure output is flushed
ENV PYTHONUNBUFFERED=1
ENV LUMINA_HOST=0.0.0.0

# Run the application
CMD ["python", "main.py"]


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.



================================================
FILE: README.md
================================================
<p align="center">
  <img src="logo.png" width="128" alt="Lumina Studio Logo">
</p>

<h1 align="center">Lumina Studio</h1>

<p align="center">
  A Multi-Material FDM Color System Based on Physical Calibration
</p>

<p align="center">
  <a href="https://github.com/MOVIBALE/Lumina-Layers/stargazers">
    <img src="https://img.shields.io/github/stars/MOVIBALE/Lumina-Layers?style=social" alt="Stars">
  </a>
  &nbsp;
  <a href="https://github.com/MOVIBALE/Lumina-Layers/releases/latest">
    <img src="https://img.shields.io/github/v/release/MOVIBALE/Lumina-Layers?label=Latest%20Version&amp;include_prereleases" alt="Release">
  </a>
  &nbsp;
  <a href="LICENSE">
    <img src="https://img.shields.io/badge/License-GPL%20v3.0-blue.svg" alt="License">
  </a>
</p>

<p align="center">
  <a href="README_CN.md">📖 Chinese Version / 中文文档</a>
</p>

---

<h2 align="center">Official Links & Community</h2>

<p align="center">
  <b>GitHub :</b>
  <a href="https://github.com/MOVIBALE/Lumina-Layers">
    <img src="https://img.shields.io/badge/GitHub-Lumina--Layers-181717?style=for-the-badge&logo=github" alt="GitHub">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>Join Discord :</b>
  <a href="https://discord.gg/57whRe3C8G">
    <img src="https://img.shields.io/badge/Discord-Lumina%20Studio-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord">
  </a>
</p>

<p align="center">
  <b> YouTube:</b>
  <a href="https://www.youtube.com/channel/UCyP2Euw9whk1j-MT8d652Kw">
    <img src="https://img.shields.io/badge/YouTube-Lumina%20Studio-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>Patreon:</b>
  <a href="https://www.patreon.com/Lumina_studio">
    <img src="https://img.shields.io/badge/Patreon-Lumina%20Studio-FF424D?style=for-the-badge&logo=patreon&logoColor=white" alt="Patreon">
  </a>
</p>

<p align="center">
  <b>Bilibili:</b>
  <a href="https://b23.tv/CCxxiKC">
    <img src="https://img.shields.io/badge/Bilibili-Lumina%20Studio-00A1D6?style=for-the-badge&logo=bilibili&logoColor=white" alt="Bilibili">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>QQ Group:</b>
  <a href="https://qm.qq.com/q/vocxOMTnj2">
    <img src="https://img.shields.io/badge/QQ%20Group-1065401448-EB1923?style=for-the-badge&logo=tencentqq&logoColor=white" alt="QQ Group">
  </a>
</p>

## Project Status

**Current Version**: v1.6.8  
**License**: GNU GPL v3.0  
**Nature**: Non-profit Open Source Community Project

---
## Project Background
To simplify the steep learning curve of software such as HueForge/FlatForge and the requirement for specific filaments, Lumina uses brute-force and simplified brute-force methods based on physical calibration to obtain actual printed colors. The current mode does not involve any color theory calculations (color calculation based on color/TD values may be introduced in advanced features of version 2.0 in the future). The current approach is: Print - Capture - Extract Color - Map Stacking Formula Based on Extracted Color - Print (this is a color matching function, inspired by the default color matching in Autoforge and CMYK Lithophane).

## Features
**Color Modes**

2/4/5/6/8 Colors

**Generation Modes**

High-fidelity mode / Pixel mode / SVG mode

**Other Features**

Custom color card and color calibration functions

Adjust the number of generated colors

Image cutout / background removal

Independent backplate

Outline

Add transparent layer

Cloisonné enamel mode

Replace colors in the image after generating preview

**Advanced Features**

Color Formula Search

Merge color card function

## Open Ecosystem

### About .npy Calibration Files

All calibration presets (.npy files) are **completely free and open**, following these principles:

- **Vendor Lock-in Rejection**: We **will never** force users to use specific filament brands, or require manufacturers to produce specific "compatible filaments" — past, present, or future. This violates the spirit of open source.
  
- **Community Collaboration**: All users, organizations, and filament manufacturers are welcome to submit PRs to synchronize calibration presets. Your printer data can help others.
- No additional testing tools required — only a 3D printer and a phone/camera.

**Open Data = Community Co-creation**

---

## Installation

### Clone Repository

```bash
git clone https://github.com/MOVIBALE/Lumina-Layers.git
cd Lumina-Layers
```

### Option 1:Docker 

Using Docker is the easiest way to run Lumina Studio without worrying about system-level dependencies (such as cairo or pkg-config).
1. **Build the lumina image**:
   ```bash
   docker build -t lumina-layers .
   ```

2. **Run Container**:
   ```bash
   docker run -d -p 7860:7860 lumina-layers
   ```

3. Open in your browser `http://localhost:7860`。

### Option 2:Local Installation

**Basic Dependencies**:
```bash
pip install -r requirements.txt
```

---

## User Guide

### Quick Start

```bash
python main.py
```
This will launch the web interface containing all three modules in a browser tab.

---

## Tech Stack

| Component | Technology |
|------|------|
| Core Logic | Python (NumPy for voxel operations) |
| Geometry Engine | Trimesh (mesh generation and export) |
| UI Framework | Gradio 4.0+ |
| Vision Stack | OpenCV (perspective and color extraction) |
| Color Matching | SciPy KDTree |
| 3D Preview | Gradio Model3D (GLB format) |

---


## License

This project is licensed under the **GNU GPL v3.0** open-source license.

- ✅ **Open Source & Freedom**: You are free to run, study, modify and distribute this software.
- 🔄 **Strong Copyleft**: If you modify and distribute this software, you must publish your source code under the GPL v3.0 license.
- ❌ **No Closed-Source**: It is strictly prohibited to package and sell this software or its derivatives as closed-source products.

**Commercial Use & "Small Creator" Support Statement**: This project supports and encourages individual creators, small vendors and micro-enterprises to earn income through labor. You may freely use this software to generate models and sell physical printed products without additional authorization.

---

## Technical Origin & Statement

### Technical Inspiration

This project is inspired by the following works:

- **HueForge** – The first project to commercialize FDM multi-layer stacking color mixing technology.
- **AutoForge** – Automated color matching built on Hueforge.
- **CMYK Backlit Lithophane** – Multi-layer stacked backlit lithophane effects in 3D printing based on transmission and subtractive color principles.

### Technical Differences & Positioning

Traditional tools rely on theoretical calculations (such as TD1/TD0 transmission distance values), but these parameters often fail due to various objective variations.

**Lumina Studio 1.X uses a brute-force approach**:
1. Print physical calibration charts with 1024+ colors (full permutation for 2 colors × 5 layers, 4 colors × 5 layers; simplified brute-force for 6 colors × 5 layers, 8 colors × 5 layers)
2. Scan via photography and extract real RGB data
3. Build a "Lookup Table (LUT) of actual results"
4. Match using nearest-neighbor algorithm (similar to Bambu Lab's keychain generator matching)

### Prior Art Statement

The core principle of FDM multi-layer color mixing was publicly disclosed by software such as HueForge between 2022 and 2023, and constitutes **prior art**.

The author of Hueforge has also clarified that such technical principles have entered the public domain. In most countries and regions, patents on these principles would almost certainly be rejected if rigorously examined by patent offices.

These authors chose openness to support community development, so this technology is generally **not patentable**.

Lumina Studio will remain open-source, collaborative, and non-profit. Public oversight is welcome.

- This is an open-source non-profit project with no bundled sales, and no features will be locked behind paywalls.
- If you or your company wish to support the project’s continued development, please contact us. Sponsored products will only be used for software development, testing and optimization.
- Sponsorship represents support for the project and does not constitute any commercial binding.
- Sponsorship arrangements that would influence technical decisions or open-source licenses are rejected.

Lumina Studio has not referenced any pending patent content, as most such patents only include specifications and do not disclose code in the short term; blind reference would hinder independent development.

**Special thanks to HueForge for their support and understanding of open source!**

---
## Acknowledgments
Special Thanks to:

- **[Hueforge](https://shop.thehueforge.com/)**
- **[AutoForge](https://github.com/AutoForgeAI/autoforge)**
- **[ChromaStack](https://github.com/borealis-zhe/ChromaStack)** 
- **[LD_ColorLayering](https://github.com/Luban-Daddy/LD_ColorLayering)** 
- **[ChromaPrint3D](https://github.com/Neroued/ChromaPrint3D)** 

---

## Contributors
<a href="https://github.com/MOVIBALE/Lumina-Layers/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=MOVIBALE/Lumina-Layers" />
</a>

Made with care by all contributors!
---
⭐ Star this repo if you find it useful!


================================================
FILE: README_CN.md
================================================
<p align="center">
  <img src="logo.png" width="128" alt="Lumina Studio Logo">
</p>

<h1 align="center">Lumina Studio</h1>

<p align="center">
  基于物理校准的多材料FDM色彩系统
</p>

<p align="center">
  <a href="https://github.com/MOVIBALE/Lumina-Layers/stargazers">
    <img src="https://img.shields.io/github/stars/MOVIBALE/Lumina-Layers?style=social" alt="Stars">
  </a>
  &nbsp;
  <a href="https://github.com/MOVIBALE/Lumina-Layers/releases/latest">
    <img src="https://img.shields.io/github/v/release/MOVIBALE/Lumina-Layers?label=最新版本&amp;include_prereleases" alt="Release">
  </a>
  &nbsp;
  <a href="LICENSE">
    <img src="https://img.shields.io/badge/协议-GPL%20v3.0-blue.svg" alt="License">
  </a>
</p>

<p align="center">
  <a href="README.md">📖 English Version / 英文文档</a>
</p>

---

<h2 align="center">官方链接与社区</h2>

<p align="center">
  <b>GitHub 仓库:</b>
  <a href="https://github.com/MOVIBALE/Lumina-Layers">
    <img src="https://img.shields.io/badge/GitHub-Lumina--Layers-181717?style=for-the-badge&logo=github" alt="GitHub">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>加入 Discord 社区:</b>
  <a href="https://discord.gg/57whRe3C8G">
    <img src="https://img.shields.io/badge/Discord-Lumina%20Studio-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord">
  </a>
</p>

<p align="center">
  <b>订阅 YouTube 频道:</b>
  <a href="https://www.youtube.com/channel/UCyP2Euw9whk1j-MT8d652Kw">
    <img src="https://img.shields.io/badge/YouTube-Lumina%20Studio-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>在 Patreon 支持我们:</b>
  <a href="https://www.patreon.com/Lumina_studio">
    <img src="https://img.shields.io/badge/Patreon-Lumina%20Studio-FF424D?style=for-the-badge&logo=patreon&logoColor=white" alt="Patreon">
  </a>
</p>

<p align="center">
  <b>关注我们的 Bilibili:</b>
  <a href="https://b23.tv/CCxxiKC">
    <img src="https://img.shields.io/badge/Bilibili-Lumina%20Studio-00A1D6?style=for-the-badge&logo=bilibili&logoColor=white" alt="Bilibili">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <b>加入 QQ 交流群:</b>
  <a href="https://qm.qq.com/q/vocxOMTnj2">
    <img src="https://img.shields.io/badge/QQ%20群-1065401448-EB1923?style=for-the-badge&logo=tencentqq&logoColor=white" alt="QQ Group">
  </a>
</p>

---

## 项目状态

**当前版本**: v1.6.8  
**协议**: GNU GPL v3.0  
**性质**: 非营利性开源社区项目

---
## 项目背景
Lumina为了简化用户使用hueforge/flatforge等其他软件学习门槛过高或需要使用指定要求的耗材的问题,基于物理校准使用了穷举法和简化穷举法来获得实际打印颜色,目前的模式并未带来任何颜色理论计算(未来可能会在2.0的高级功能中推出基于颜色/td值等的颜色计算玩法),目前采取的方法是打印-拍摄-提取颜色-根据提取的颜色映射堆叠配方-打印(这像是一种颜色匹配功能,就像是你在autoforge和CMYK Lithophane那样会默认给你匹配一些颜色的功能一样,所以受此启发)

## 功能
**颜色模式 Color Modes**

2/4/5/6/8色

**生成模式 Generation Modes**

高保真模式/像素模式/svg模式

High-fidelity mode / Pixel mode / SVG mode

**其他功能 Other Features**

自定义色卡和校准颜色功能 Custom color card and color calibration functions

调节生成颜色的数量 Adjust the number of generated colors.

抠图功能 image cutout / background removal

背板独立 Independent backplate

描边 Outline

添加透明层 Add transparent layer

掐丝珐琅模式 wiry enamel(cloisonné enamel)

生成预览后替换图中颜色 Replace colors in the image

**高级功能 Advanced Features**

颜色配方查询功能 Color Formula Search

合并色卡功能 Merge color card function


## 生态开放

### 关于 .npy 校准文件

所有校准预设(`.npy`文件)**完全免费开放**,遵循以下原则:

- **拒绝供应商锁定**:过去、现在、未来,我们**永远不会**强迫用户使用特定耗材品牌,也不会要求制造商生产符合要求的特定的"兼容耗材"。这违背开源精神。
  
- **社区共建**:欢迎所有用户、组织、耗材厂商提交PR,同步校准预设。你的打印机数据可以帮助他人。
- 无需任何其他测试工具,只需要你有3D打印机和手机/相机。

**数据开放 = 社区共创**

---




## 安装

### 克隆仓库

```bash
git clone https://github.com/MOVIBALE/Lumina-Layers.git
cd Lumina-Layers
```

### 选项 1:Docker (推荐)

使用 Docker 是运行 Lumina Studio 最简单的方法,无需担心系统级依赖项(如 `cairo` 或 `pkg-config`)。

1. **构建镜像**:
   ```bash
   docker build -t lumina-layers .
   ```

2. **运行容器**:
   ```bash
   docker run -d -p 7860:7860 lumina-layers
   ```

3. 在浏览器中打开 `http://localhost:7860`。

### 选项 2:本地安装

**基础依赖**(必需):
```bash
pip install -r requirements.txt
```

---

## 使用指南

### 快速启动

```bash
python main.py
```

这将在标签页中启动包含所有三个模块的Web界面。

---

## 技术栈

| 组件 | 技术 |
|------|------|
| 核心逻辑 | Python(NumPy用于体素操作) |
| 几何引擎 | Trimesh(网格生成与导出) |
| UI框架 | Gradio 4.0+ |
| 视觉栈 | OpenCV(透视与颜色提取) |
| 色彩匹配 | SciPy KDTree |
| 3D预览 | Gradio Model3D(GLB格式) |

---


## 许可协议

本项目采用 **GNU GPL v3.0** 开源协议。

- ✅ **开源与自由**:你可以自由地运行、研究、修改和分发本软件。
- 🔄 **强传染性 (Copyleft)**:如果你修改了本软件并分发,你必须在 GPL v3.0 协议下公开你的源代码。
- ❌ **禁止闭源**:严禁将本软件或其衍生作品闭源打包销售。

**商业使用与"小摊主"支持声明**:本项目支持并鼓励个人创作者、小摊主及小微企业通过劳动获取收益。你可以自由地使用本软件生成模型并销售物理打印成品,无需额外授权。

---
## 技术来源与技术声明

### 技术来源

本项目受以下项目启发:

- **HueForge** - 首个将FDM多层堆叠混色技术做成商业软件的项目。
- **AutoForge** - 基于Hueforge制作的自动化色彩匹配。
- **CMYK背光浮雕画** - 基于透射原理和减色原理在3D打印中得到多层堆叠背光浮雕的效果。

### 技术区别与定位

传统工具依赖理论计算(如TD1/TD0透射距离值),但这些参数极易因各种客观原因差异而失效。

**Lumina Studio 1.X 采用"穷举法"路线**:
1. 打印1024及更多色物理校准板(2色x5层的全排列),(4色×5层的全排列),(6色x5层的简化穷举),(8色x5层的简化穷举)
2. 拍照扫描,提取真实RGB数据
3. 建立"实际结果查找表"(LUT)
4. 用最近邻算法匹配(类似于Bambulab的钥匙扣生成器的匹配)


### 现有技术(Prior Art)声明

FDM多层叠色的核心原理已于2022-2023年间由HueForge等软件公开披露,属于**现有技术**(Prior Art)。
Hueforge作者也明确,此类技术原理已经进入公共领域,在绝大部分国家和地区,如果专利局认真审核,原理性专利一定会被驳回。
这些作者选择保持开放以帮助社区发展,因此该技术通常**不具备专利性**。

Lumina Studio一直将以开源,互助,非盈利性的定位保持下去,欢迎各位监督。
- 本项目为开源非盈利项目,不会进行任何捆绑销售,并且不会将任何功能做成付费功能。
- 如果你或你的企业希望支持项目持续发展,欢迎联系。赞助的产品等将仅用于软件的开发和测试优化。
- 赞助仅代表对项目的支持,赞助行为不构成任何商业绑定。
- 拒绝任何影响技术决策或开源协议的赞助合作。
Lumina Stuido并未参考任何申请的专利内容,因为该类专利大部分情况下只有说明书,并且短期内不会公开技术代码,盲目参考这些专利,会影响自身开发的思路。
**特别感谢HueForge对开源的支持和理解!**

---
## 致谢

特别感谢:

- **[Hueforge](https://shop.thehueforge.com/)**
- **[AutoForge](https://github.com/AutoForgeAI/autoforge)**
- **[ChromaStack](https://github.com/borealis-zhe/ChromaStack)** 
- **[LD_ColorLayering](https://github.com/Luban-Daddy/LD_ColorLayering)** 
- **[ChromaPrint3D](https://github.com/Neroued/ChromaPrint3D)** 

---

## 贡献者

<a href="https://github.com/MOVIBALE/Lumina-Layers/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=MOVIBALE/Lumina-Layers" />
</a>

由所有贡献者精心制作!

---
⭐ 如果觉得有用,请给个Star!


================================================
FILE: api/__init__.py
================================================


================================================
FILE: api/app.py
================================================
"""Lumina Studio API — Application Factory.
Lumina Studio API — 应用工厂模块。

Provides a ``create_app()`` factory function that builds a fully-configured
FastAPI instance with CORS middleware and all domain routers registered.
Uses an async ``lifespan`` context manager to manage WorkerPool and
background tasks lifecycle.
提供 ``create_app()`` 工厂函数,构建配置完整的 FastAPI 实例,
包含 CORS 中间件和所有领域路由的注册。
使用异步 ``lifespan`` 上下文管理器管理 WorkerPool 和后台任务的生命周期。
"""

import asyncio
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from api.dependencies import (
    file_registry,
    get_file_registry,
    get_session_store,
    session_store,
    worker_pool,
)
from api.file_bridge import file_to_response
from api.routers import (
    calibration_router,
    converter_router,
    extractor_router,
    five_color_router,
    health_router,
    lut_router,
    slicer_router,
    system_router,
)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Manage application startup and shutdown lifecycle.
    管理应用启动和关闭的生命周期。

    Startup:
        - Initialize the WorkerPool process pool.
          初始化 WorkerPool 进程池。
        - Start the periodic session cleanup background task.
          启动定期会话清理后台任务。

    Shutdown:
        - Gracefully shut down the WorkerPool, waiting for in-flight tasks.
          优雅关闭 WorkerPool,等待正在执行的任务完成。
    """
    # --- Startup ---
    worker_pool.start()
    print(f"[POOL] Started with {worker_pool.max_workers} workers")

    async def _cleanup_loop() -> None:
        """Periodically clean up expired sessions.
        定期清理过期会话。
        """
        while True:
            await asyncio.sleep(60)
            count = session_store.cleanup_expired()
            if count > 0:
                print(f"[SESSION] Cleaned up {count} expired sessions")

    cleanup_task = asyncio.create_task(_cleanup_loop())

    yield

    # --- Shutdown ---
    cleanup_task.cancel()
    worker_pool.shutdown(wait=True)
    print("[POOL] Shutdown complete")


def create_app() -> FastAPI:
    """Create and configure the FastAPI application.
    创建并配置 FastAPI 应用实例。

    Returns:
        FastAPI: A fully-configured application instance with CORS middleware,
            lifespan manager, and all domain routers registered.
            配置完整的应用实例,已注册 CORS 中间件、生命周期管理器和所有领域路由。
    """
    app = FastAPI(title="Lumina Studio API", version="2.0", lifespan=lifespan)

    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    app.include_router(converter_router)
    app.include_router(extractor_router)
    app.include_router(calibration_router)
    app.include_router(five_color_router)
    app.include_router(health_router)
    app.include_router(lut_router)
    app.include_router(slicer_router)
    app.include_router(system_router)

    @app.get("/api/files/{file_id}")
    def serve_file(file_id: str):
        """Serve a registered file by file_id."""
        result = file_registry.resolve(file_id)
        if result is None:
            raise HTTPException(status_code=404, detail="File not found or expired")
        path, filename = result
        return file_to_response(path, filename)

    return app


app: FastAPI = create_app()


================================================
FILE: api/dependencies.py
================================================
"""Lumina Studio API — Dependency Injection.
Lumina Studio API — 依赖注入模块。

Global singletons and FastAPI dependency functions.
Separated from app.py to avoid circular imports between
app and router modules.
全局单例和 FastAPI 依赖注入函数。
从 app.py 分离以避免 app 与 router 模块之间的循环导入。
"""

from api.file_registry import FileRegistry
from api.session_store import SessionStore
from api.worker_pool import WorkerPoolManager
from config import WorkerPoolConfig

# Global singletons
session_store: SessionStore = SessionStore(ttl=1800)
file_registry: FileRegistry = FileRegistry()

_worker_pool_config = WorkerPoolConfig.from_env()
worker_pool: WorkerPoolManager = WorkerPoolManager(max_workers=_worker_pool_config.MAX_WORKERS)


def get_session_store() -> SessionStore:
    """FastAPI dependency: return global SessionStore."""
    return session_store


def get_file_registry() -> FileRegistry:
    """FastAPI dependency: return global FileRegistry."""
    return file_registry


def get_worker_pool() -> WorkerPoolManager:
    """FastAPI dependency: return global WorkerPoolManager.
    FastAPI 依赖注入:返回全局 WorkerPoolManager 实例。

    Returns:
        WorkerPoolManager: The global worker pool singleton. (全局工作进程池单例)
    """
    return worker_pool


================================================
FILE: api/file_bridge.py
================================================
import io
import os
import tempfile
from typing import Optional

import numpy as np
from fastapi import UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from PIL import Image


async def upload_to_ndarray(file: UploadFile) -> np.ndarray:
    """将 UploadFile 图像转换为 RGB NumPy ndarray。

    Args:
        file: FastAPI UploadFile 对象

    Returns:
        np.ndarray: shape (H, W, 3), dtype uint8, RGB 格式

    Raises:
        ValueError: 文件格式无效或无法解码
    """
    contents = await file.read()
    try:
        img = Image.open(io.BytesIO(contents))
        if img.mode != "RGB":
            img = img.convert("RGB")
        return np.array(img, dtype=np.uint8)
    except Exception as e:
        raise ValueError(f"无法解码图像文件: {e}")


async def upload_to_tempfile(
    file: UploadFile, suffix: Optional[str] = None
) -> str:
    """将 UploadFile 保存为临时文件,返回路径。

    Args:
        file: FastAPI UploadFile 对象
        suffix: 文件后缀(如 ".png"、".svg")

    Returns:
        str: 临时文件绝对路径
    """
    if suffix is None:
        suffix = os.path.splitext(file.filename or "")[1] or ".tmp"
    contents = await file.read()
    fd, path = tempfile.mkstemp(suffix=suffix)
    try:
        os.write(fd, contents)
    finally:
        os.close(fd)
    return path


def pil_to_png_bytes(img: Image.Image) -> bytes:
    """将 PIL Image 编码为 PNG 字节流。"""
    buf = io.BytesIO()
    img.save(buf, format="PNG")
    return buf.getvalue()


def ndarray_to_png_bytes(arr: np.ndarray) -> bytes:
    """将 NumPy ndarray 编码为 PNG 字节流。"""
    img = Image.fromarray(arr)
    return pil_to_png_bytes(img)


def pil_to_streaming_response(img: Image.Image, fmt: str = "PNG") -> StreamingResponse:
    """将 PIL Image 编码为 StreamingResponse。"""
    buf = io.BytesIO()
    img.save(buf, format=fmt)
    buf.seek(0)
    media_type = "image/png" if fmt.upper() == "PNG" else f"image/{fmt.lower()}"
    return StreamingResponse(buf, media_type=media_type)


def file_to_response(path: str, filename: Optional[str] = None) -> FileResponse:
    """将文件路径包装为 FileResponse。"""
    if filename is None:
        filename = os.path.basename(path)
    media_type = _guess_media_type(path)
    return FileResponse(path=path, filename=filename, media_type=media_type)


def _guess_media_type(path: str) -> str:
    """根据文件扩展名推断 MIME 类型。"""
    ext = os.path.splitext(path)[1].lower()
    return {
        ".3mf": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
        ".glb": "model/gltf-binary",
        ".zip": "application/zip",
        ".npy": "application/octet-stream",
        ".npz": "application/octet-stream",
        ".png": "image/png",
        ".jpg": "image/jpeg",
    }.get(ext, "application/octet-stream")


================================================
FILE: api/file_registry.py
================================================
import os
import threading
import uuid
from typing import Optional, Tuple


class FileRegistry:
    """文件注册表,管理生成文件的 UUID 映射。

    支持两种注册方式:
    1. register_path(session_id, path, filename) — 注册磁盘文件路径
    2. register_bytes(session_id, data, filename) — 注册内存字节流(保存为临时文件)
    """

    def __init__(self) -> None:
        self._registry: dict[str, dict] = {}  # {file_id: {path, filename, session_id}}
        self._lock = threading.Lock()

    def register_path(self, session_id: str, path: str,
                      filename: Optional[str] = None) -> str:
        """注册磁盘文件,返回 file_id。"""
        file_id = str(uuid.uuid4())
        if filename is None:
            filename = os.path.basename(path)
        with self._lock:
            self._registry[file_id] = {
                "path": path,
                "filename": filename,
                "session_id": session_id,
            }
        return file_id

    def register_bytes(self, session_id: str, data: bytes,
                       filename: str) -> str:
        """注册字节流(写入临时文件),返回 file_id。"""
        import tempfile
        suffix = os.path.splitext(filename)[1] or ".bin"
        fd, path = tempfile.mkstemp(suffix=suffix)
        try:
            os.write(fd, data)
        finally:
            os.close(fd)
        return self.register_path(session_id, path, filename)

    def resolve(self, file_id: str) -> Optional[Tuple[str, str]]:
        """解析 file_id,返回 (path, filename) 或 None。"""
        with self._lock:
            entry = self._registry.get(file_id)
        if entry is None:
            return None
        path = entry["path"]
        if not os.path.exists(path):
            return None
        return path, entry["filename"]

    def cleanup_session(self, session_id: str) -> int:
        """清理指定 session 的所有注册文件,返回清理数量。"""
        to_remove = []
        with self._lock:
            for fid, entry in self._registry.items():
                if entry["session_id"] == session_id:
                    to_remove.append(fid)
            for fid in to_remove:
                self._registry.pop(fid, None)
        return len(to_remove)

    def clear_all(self) -> int:
        """清除所有注册文件并删除磁盘文件,返回清理数量。"""
        with self._lock:
            count = 0
            for fid, entry in list(self._registry.items()):
                path = entry.get("path")
                if path and os.path.exists(path):
                    try:
                        os.remove(path)
                        count += 1
                    except OSError:
                        pass
                self._registry.pop(fid, None)
            return count


================================================
FILE: api/routers/__init__.py
================================================
"""Lumina Studio API — Router re-exports.
Lumina Studio API — 路由模块的统一导出。

This package re-exports all domain routers so that consumers
can import directly from ``api.routers`` instead of reaching into
individual domain modules.
本包统一导出所有领域 Router,使用方可直接从 ``api.routers``
导入,无需深入各领域子模块。
"""

from api.routers.calibration import router as calibration_router
from api.routers.converter import router as converter_router
from api.routers.extractor import router as extractor_router
from api.routers.five_color import router as five_color_router
from api.routers.health import router as health_router
from api.routers.lut import router as lut_router
from api.routers.slicer import router as slicer_router
from api.routers.system import router as system_router

__all__ = [
    "converter_router",
    "extractor_router",
    "calibration_router",
    "five_color_router",
    "health_router",
    "lut_router",
    "slicer_router",
    "system_router",
]


================================================
FILE: api/routers/calibration.py
================================================
"""Calibration domain API router.
Calibration 领域 API 路由模块。
"""

from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException

from api.dependencies import get_file_registry
from api.file_bridge import pil_to_png_bytes
from api.file_registry import FileRegistry
from api.schemas.calibration import CalibrationGenerateRequest
from api.schemas.responses import CalibrationResponse
from core.calibration import (
    generate_5color_extended_batch_zip,
    generate_8color_batch_zip,
    generate_bw_calibration_board,
    generate_calibration_board,
    generate_smart_board,
)

router = APIRouter(prefix="/api/calibration", tags=["Calibration"])


def _handle_core_error(e: Exception, context: str) -> None:
    """将 core 模块异常转换为 HTTP 500 错误。"""
    print(f"[API] {context} error: {e}")
    raise HTTPException(status_code=500, detail=f"{context} failed: {str(e)}")


@router.post("/generate")
def calibration_generate(
    request: CalibrationGenerateRequest,
    registry: FileRegistry = Depends(get_file_registry),
) -> CalibrationResponse:
    """Generate a printable calibration board.
    生成可打印的校准板。
    """
    mode = request.color_mode.value
    try:
        if mode == "BW (Black & White)":
            path, preview_img, status = generate_bw_calibration_board(
                block_size_mm=float(request.block_size),
                gap_mm=request.gap,
                backing_color=request.backing.value,
            )
        elif mode in ("4-Color", "CMYW", "RYBW"):
            path, preview_img, status = generate_calibration_board(
                color_mode=mode if mode in ("CMYW", "RYBW") else "RYBW",
                block_size_mm=float(request.block_size),
                gap_mm=request.gap,
                backing_color=request.backing.value,
            )
        elif mode == "6-Color (Smart 1296)":
            path, preview_img, status = generate_smart_board(
                block_size_mm=float(request.block_size),
                gap_mm=request.gap,
            )
        elif mode == "8-Color Max":
            path, preview_img, status = generate_8color_batch_zip()
        elif mode == "5-Color Extended (1444)":
            path, preview_img, status = generate_5color_extended_batch_zip(
                float(request.block_size), float(request.gap)
            )
        else:
            raise HTTPException(
                status_code=422, detail=f"Unsupported color mode: {mode}"
            )
    except HTTPException:
        raise
    except Exception as e:
        _handle_core_error(e, "Calibration generation")

    # Register files via FileRegistry
    sid = "calibration"  # Stateless endpoint uses fixed session identifier
    download_id = registry.register_path(sid, path)
    preview_bytes = pil_to_png_bytes(preview_img)
    preview_id = registry.register_bytes(sid, preview_bytes, "preview.png")

    return CalibrationResponse(
        status="ok",
        message=status,
        download_url=f"/api/files/{download_id}",
        preview_url=f"/api/files/{preview_id}",
    )


================================================
FILE: api/routers/converter.py
================================================
"""Converter domain API router.
Converter 领域 API 路由模块。
"""

from __future__ import annotations

import asyncio
import os
import pickle
import tempfile
import zipfile

import cv2
import numpy as np
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from PIL import Image
from pydantic import BaseModel

from api.dependencies import get_file_registry, get_session_store, get_worker_pool
from api.file_bridge import ndarray_to_png_bytes, pil_to_png_bytes, upload_to_tempfile
from api.file_registry import FileRegistry
from api.schemas.converter import (
    BedSizeItem,
    BedSizeListResponse,
    ColorMergePreviewRequest,
    ColorReplaceRequest,
    ConvertGenerateRequest,
)
from api.schemas.responses import (
    BatchItemResult,
    BatchResponse,
    ColorReplaceResponse,
    CropResponse,
    GenerateResponse,
    HeightmapUploadResponse,
    MergePreviewResponse,
    PreviewResponse,
)
from api.session_store import SessionStore
from api.worker_pool import WorkerPoolManager
from api.workers.converter_workers import (
    worker_batch_convert_item,
    worker_generate_model,
    worker_generate_preview,
)
from core.color_merger import ColorMerger
from core.image_preprocessor import ImagePreprocessor
from core.color_replacement import ColorReplacementManager
from core.converter import convert_image_to_3d, extract_color_palette, generate_empty_bed_glb, generate_segmented_glb
from config import BedManager, ModelingMode as CoreModelingMode, PrinterConfig
from core.heightmap_loader import HeightmapLoader
from utils.lut_manager import LUTManager

router = APIRouter(prefix="/api/convert", tags=["Converter"])

_STUB_RESPONSE: dict[str, str] = {
    "status": "not_implemented",
    "message": "Phase 2 will integrate core logic",
}


def _handle_core_error(e: Exception, context: str) -> None:
    """将 core 模块异常转换为 HTTP 500 错误。"""
    print(f"[API] {context} error: {e}")
    raise HTTPException(status_code=500, detail=f"{context} failed: {str(e)}")


def _require_session(store: SessionStore, session_id: str) -> dict:
    """获取 session 数据,不存在时抛出 404。"""
    data = store.get(session_id)
    if data is None:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
    return data


def _require_preview_cache(session_data: dict) -> dict:
    """获取 preview_cache,不存在时抛出 409。"""
    cache = session_data.get("preview_cache")
    if cache is None:
        raise HTTPException(
            status_code=409, detail="No preview cache. Call POST /api/convert/preview first."
        )
    return cache


def _image_to_png_bytes(img: object) -> bytes:
    """将 ndarray 或 PIL Image 转换为 PNG 字节流。"""
    if isinstance(img, np.ndarray):
        return ndarray_to_png_bytes(img)
    if isinstance(img, Image.Image):
        return pil_to_png_bytes(img)
    raise TypeError(f"Unsupported image type: {type(img)}")


@router.get("/bed-sizes", response_model=BedSizeListResponse)
def get_bed_sizes() -> BedSizeListResponse:
    """Return all available printer bed sizes.
    返回所有可用的打印热床尺寸列表。
    """
    beds = [
        BedSizeItem(
            label=label,
            width_mm=w,
            height_mm=h,
            is_default=(label == BedManager.DEFAULT_BED),
        )
        for label, w, h in BedManager.BEDS
    ]
    return BedSizeListResponse(beds=beds)


@router.get("/bed-preview")
def get_bed_preview(
    bed_label: str = BedManager.DEFAULT_BED,
    registry: FileRegistry = Depends(get_file_registry),
) -> dict:
    """Generate a GLB preview of the empty print bed.
    生成空热床的 GLB 3D 预览。

    Args:
        bed_label: Bed size label (e.g. "256×256 mm"). (热床尺寸标签)

    Returns:
        dict: Contains preview_3d_url pointing to the GLB file. (包含 GLB 文件 URL)
    """
    bed_w, bed_h = BedManager.get_bed_size(bed_label)
    try:
        glb_path = generate_empty_bed_glb(bed_w, bed_h)
    except Exception as e:
        _handle_core_error(e, "Bed preview generation")

    if glb_path is None:
        raise HTTPException(status_code=500, detail="Failed to generate bed preview")

    glb_id = registry.register_path("bed-preview", glb_path)
    return {"preview_3d_url": f"/api/files/{glb_id}"}


@router.post("/crop", response_model=CropResponse)
async def crop_image(
    image: UploadFile = File(..., description="输入图像"),
    x: int = Form(0, description="裁剪起点 X"),
    y: int = Form(0, description="裁剪起点 Y"),
    width: int = Form(100, ge=1, description="裁剪宽度"),
    height: int = Form(100, ge=1, description="裁剪高度"),
    registry: FileRegistry = Depends(get_file_registry),
) -> CropResponse:
    """Crop an uploaded image and return the cropped result URL.
    裁剪上传的图片并返回裁剪后的文件 URL。

    Args:
        image: 上传的图片文件
        x: 裁剪起点 X 坐标
        y: 裁剪起点 Y 坐标
        width: 裁剪宽度(像素)
        height: 裁剪高度(像素)
        registry: FileRegistry 依赖

    Returns:
        CropResponse: 包含裁剪后图片 URL 和尺寸
    """
    # 1. Save uploaded file to temp path
    temp_path = await upload_to_tempfile(image)

    # 2. Validate that the file is a readable image
    try:
        ImagePreprocessor.get_image_dimensions(temp_path)
    except ValueError:
        raise HTTPException(status_code=422, detail="Invalid image file")

    # 3. Crop image (CropRegion.clamp is called internally)
    try:
        cropped_path = ImagePreprocessor.crop_image(temp_path, x, y, width, height)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=str(e))

    # 4. Get cropped image dimensions
    w, h = ImagePreprocessor.get_image_dimensions(cropped_path)

    # 5. Register cropped file and return response
    file_id = registry.register_path("crop", cropped_path)
    return CropResponse(
        status="ok",
        message="Image cropped successfully",
        cropped_url=f"/api/files/{file_id}",
        width=w,
        height=h,
    )


@router.post("/preview")
async def convert_preview(
    image: UploadFile = File(..., description="输入图像"),
    lut_name: str = Form(..., description="LUT 名称"),
    target_width_mm: float = Form(60.0, description="目标宽度 (mm)"),
    auto_bg: bool = Form(False, description="自动去背景"),
    bg_tol: int = Form(40, description="背景容差"),
    color_mode: str = Form("4-Color", description="颜色模式"),
    modeling_mode: str = Form("high-fidelity", description="建模模式"),
    quantize_colors: int = Form(48, description="K-Means 色彩细节"),
    enable_cleanup: bool = Form(True, description="孤立像素清理"),
    hue_weight: float = Form(0.0, description="色相保护权重"),
    is_dark: bool = Form(True, description="深色主题"),
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
    pool: WorkerPoolManager = Depends(get_worker_pool),
) -> PreviewResponse:
    """Generate a 2D color-matched preview via process pool.
    通过进程池生成 2D 颜色匹配预览图。

    File upload and session/registry operations run on the main thread.
    CPU-intensive preview generation is offloaded to the worker pool.
    文件上传和 session/registry 操作在主线程完成。
    CPU 密集型预览生成卸载到工作进程池。
    """
    # Resolve LUT path
    lut_path = LUTManager.get_lut_path(lut_name)
    if lut_path is None:
        raise HTTPException(status_code=404, detail=f"LUT not found: {lut_name}")

    # 1. File upload (I/O, main thread)
    temp_path = await upload_to_tempfile(image)

    # 2. CPU computation offloaded to process pool (only paths and scalars)
    try:
        print(f"[API convert_preview] hue_weight={hue_weight}, lut_name={lut_name}, color_mode={color_mode}")
        result = await pool.submit(
            worker_generate_preview,
            temp_path,
            lut_path,
            target_width_mm,
            auto_bg,
            bg_tol,
            color_mode,
            modeling_mode,
            quantize_colors,
            enable_cleanup,
            is_dark,
            hue_weight,
        )
    except asyncio.TimeoutError:
        raise HTTPException(status_code=504, detail="Preview generation timed out")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Preview generation failed: {str(e)}")

    # 3. Result processing (I/O + Session, main thread)
    if result["preview_png_path"] is None:
        raise HTTPException(status_code=500, detail=result["status_msg"] or "Preview generation failed")

    # Load cache_data from disk (worker serialized to .pkl)
    with open(result["cache_data_path"], "rb") as f:
        cache_data = pickle.load(f)

    # Load preview image from disk (worker saved as .png)
    preview_img = Image.open(result["preview_png_path"])

    status_msg: str = result["status_msg"]

    # Create session and store state
    session_id = store.create()
    store.put(session_id, "preview_cache", cache_data)
    store.put(session_id, "image_path", temp_path)
    store.put(session_id, "lut_path", lut_path)
    store.put(session_id, "lut_name", lut_name)
    store.put(session_id, "replacement_regions", [])
    store.put(session_id, "replacement_history", [])
    store.put(session_id, "free_color_set", set())
    store.register_temp_file(session_id, temp_path)
    # Register worker temp files for cleanup
    store.register_temp_file(session_id, result["preview_png_path"])
    store.register_temp_file(session_id, result["cache_data_path"])

    # Register preview image
    preview_bytes = _image_to_png_bytes(preview_img)
    preview_id = registry.register_bytes(session_id, preview_bytes, "preview.png")

    # Generate segmented GLB (one Mesh per color)
    preview_glb_url: str | None = None
    try:
        glb_path = generate_segmented_glb(cache_data)
        if glb_path and os.path.exists(glb_path):
            glb_id = registry.register_path(session_id, glb_path)
            preview_glb_url = f"/api/files/{glb_id}"
    except Exception as e:
        # Non-fatal: log and continue without GLB
        print(f"[API] Segmented GLB generation failed (non-fatal): {e}")

    # Build palette with quantized_hex, matched_hex, pixel_count, percentage
    raw_palette: list[dict] = cache_data.get("color_palette", []) if cache_data else []
    quantized_image = cache_data.get("quantized_image") if cache_data else None
    matched_rgb_arr = cache_data.get("matched_rgb") if cache_data else None
    mask_solid_arr = cache_data.get("mask_solid") if cache_data else None

    palette: list[dict] = []
    if raw_palette and matched_rgb_arr is not None and mask_solid_arr is not None:
        # Build a matched_hex -> quantized_hex lookup from pixel data
        matched_to_quantized: dict[str, str] = {}
        if quantized_image is not None:
            solid_mask = mask_solid_arr
            q_pixels = quantized_image[solid_mask]   # (N, 3)
            m_pixels = matched_rgb_arr[solid_mask]    # (N, 3)
            # For each matched color, find the most common quantized color
            for entry in raw_palette:
                m_hex = entry["hex"]  # '#rrggbb'
                m_rgb = entry["color"]  # (R, G, B)
                color_mask = np.all(m_pixels == np.array(m_rgb, dtype=np.uint8), axis=1)
                if np.any(color_mask):
                    q_subset = q_pixels[color_mask]
                    unique_q, q_counts = np.unique(q_subset, axis=0, return_counts=True)
                    dominant_q = unique_q[np.argmax(q_counts)]
                    r, g, b = int(dominant_q[0]), int(dominant_q[1]), int(dominant_q[2])
                    matched_to_quantized[m_hex] = f"#{r:02x}{g:02x}{b:02x}"

        for entry in raw_palette:
            m_hex = entry["hex"]  # '#rrggbb'
            q_hex = matched_to_quantized.get(m_hex, m_hex)
            palette.append({
                "quantized_hex": q_hex,
                "matched_hex": m_hex,
                "pixel_count": entry["count"],
                "percentage": entry["percentage"],
            })

    dimensions = {}
    if cache_data:
        dimensions = {
            "width": cache_data.get("target_w", 0),
            "height": cache_data.get("target_h", 0),
        }

    # Extract color contours from cache (generated by generate_segmented_glb)
    contours_data: dict[str, list[list[list[float]]]] | None = None
    if cache_data and 'color_contours' in cache_data:
        contours_data = cache_data['color_contours']

    return PreviewResponse(
        session_id=session_id,
        status="ok",
        message=status_msg or "Preview generated",
        preview_url=f"/api/files/{preview_id}",
        preview_glb_url=preview_glb_url,
        palette=palette,
        dimensions=dimensions,
        contours=contours_data,
    )


@router.post("/upload-heightmap")
async def upload_heightmap(
    heightmap: UploadFile = File(..., description="高度图文件"),
    session_id: str = Form(..., description="Session ID"),
    max_relief_height: float = Form(2.0, description="最大浮雕高度 (mm)"),
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> HeightmapUploadResponse:
    """上传高度图并计算基于高度图的 color_height_map。

    根据高度图灰度值和 preview_cache 中的颜色匹配数据,
    计算每个调色板颜色对应区域的平均高度。

    Args:
        heightmap: 高度图图像文件
        session_id: 会话 ID
        max_relief_height: 最大浮雕高度 (mm)
        store: SessionStore 依赖
        registry: FileRegistry 依赖

    Returns:
        HeightmapUploadResponse: 包含 color_height_map 和缩略图 URL
    """
    # 1. Validate session
    session_data = _require_session(store, session_id)

    # 2. Validate preview_cache exists
    cache = _require_preview_cache(session_data)

    # 3. Read uploaded file and save to temp
    temp_path = await upload_to_tempfile(heightmap)
    store.register_temp_file(session_id, temp_path)

    # 4. Load and validate heightmap using HeightmapLoader
    result = HeightmapLoader.load_and_validate(temp_path)
    if not result["success"]:
        raise HTTPException(
            status_code=422,
            detail=result["error"] or "Invalid heightmap file",
        )

    grayscale: np.ndarray = result["grayscale"]
    original_size: tuple[int, int] = result["original_size"]  # (w, h)
    thumbnail: np.ndarray | None = result["thumbnail"]
    warnings: list[str] = list(result["warnings"])

    # 5. Check aspect ratio vs original image
    matched_rgb: np.ndarray = cache["matched_rgb"]
    target_h, target_w = matched_rgb.shape[:2]
    hm_w, hm_h = original_size

    ar_warning = HeightmapLoader._check_aspect_ratio(hm_w, hm_h, target_w, target_h)
    if ar_warning:
        warnings.append(ar_warning)

    # 6. Resize heightmap to match target dimensions
    grayscale_resized = HeightmapLoader._resize_to_target(grayscale, target_w, target_h)

    # 7. Compute per-color average height from heightmap
    base_thickness: float = PrinterConfig.LAYER_HEIGHT  # 0.08mm
    mask_solid: np.ndarray | None = cache.get("mask_solid")

    color_height_map: dict[str, float] = {}
    palette_data: list[dict] = cache.get("color_palette", [])

    for entry in palette_data:
        color_rgb = np.array(entry["color"], dtype=np.uint8)  # (3,)
        hex_val: str = entry["hex"]  # '#rrggbb'
        hex_key = hex_val.lstrip("#").lower()

        # Find pixels matching this color in matched_rgb
        color_mask = np.all(matched_rgb == color_rgb, axis=2)  # (H, W) bool
        if mask_solid is not None:
            color_mask = color_mask & mask_solid

        if not np.any(color_mask):
            # No pixels for this color, assign base thickness
            color_height_map[hex_key] = base_thickness
            continue

        # Average grayscale value at matching pixel positions
        avg_gray = float(np.mean(grayscale_resized[color_mask]))

        # Map to height range [base_thickness, max_relief_height]
        height = base_thickness + (avg_gray / 255.0) * (max_relief_height - base_thickness)
        color_height_map[hex_key] = round(height, 4)

    # 8. Store heightmap data in session
    store.put(session_id, "heightmap_grayscale", grayscale_resized)
    store.put(session_id, "heightmap_original_size", original_size)
    store.put(session_id, "heightmap_max_relief_height", max_relief_height)
    store.put(session_id, "heightmap_color_height_map", color_height_map)

    # 9. Register thumbnail in FileRegistry
    thumbnail_url = ""
    if thumbnail is not None:
        thumb_bytes = ndarray_to_png_bytes(thumbnail)
        thumb_id = registry.register_bytes(session_id, thumb_bytes, "heightmap_thumb.png")
        thumbnail_url = f"/api/files/{thumb_id}"

    return HeightmapUploadResponse(
        status="ok",
        message="Heightmap uploaded and processed",
        thumbnail_url=thumbnail_url,
        original_size=original_size,
        color_height_map=color_height_map,
        warnings=warnings,
    )


class _GenerateBody(BaseModel):
    """Wrapper combining session_id with generate parameters."""

    session_id: str
    params: ConvertGenerateRequest


@router.post("/generate")
async def convert_generate(
    body: _GenerateBody,
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
    pool: WorkerPoolManager = Depends(get_worker_pool),
) -> GenerateResponse:
    """Generate a printable 3MF model via process pool.
    通过进程池生成可打印的 3MF 模型。

    Session and FileRegistry operations run on the main thread.
    CPU-intensive model generation is offloaded to the worker pool.
    Session 和 FileRegistry 操作在主线程完成。
    CPU 密集型模型生成卸载到工作进程池。
    """
    # 1. Session validation (main thread)
    session_data = _require_session(store, body.session_id)
    cache = _require_preview_cache(session_data)

    request = body.params

    # Retrieve paths stored during preview
    image_path: str | None = session_data.get("image_path")
    lut_path: str | None = session_data.get("lut_path")
    if not image_path or not os.path.exists(image_path):
        raise HTTPException(status_code=409, detail="Image file missing. Call POST /api/convert/preview first.")
    if not lut_path or not os.path.exists(lut_path):
        raise HTTPException(status_code=409, detail="LUT file missing. Call POST /api/convert/preview first.")

    # Merge replacement_regions: prefer session state, fall back to request body
    replacement_regions = session_data.get("replacement_regions") or None
    if request.replacement_regions is not None:
        replacement_regions = [
            {
                "quantized_hex": r.quantized_hex,
                "matched_hex": r.matched_hex,
                "replacement_hex": r.replacement_hex,
            }
            for r in request.replacement_regions
        ]

    free_color_set = session_data.get("free_color_set") or None
    if request.free_color_set is not None:
        free_color_set = request.free_color_set

    # Convert API ModelingMode enum to core ModelingMode enum
    core_modeling_mode = CoreModelingMode(request.modeling_mode.value)

    # Resolve height_mode for relief branching
    # 解析 height_mode 用于浮雕分支选择
    height_mode = request.height_mode or "color"

    # If heightmap mode, save session heightmap to temp file for worker process
    # 高度图模式时,将 session 中的高度图保存为临时文件供工作进程使用
    heightmap_path: str | None = None
    if request.enable_relief and height_mode == "heightmap":
        heightmap_grayscale = session_data.get("heightmap_grayscale")
        if heightmap_grayscale is not None:
            import tempfile
            import numpy as np
            from PIL import Image
            fd, hm_temp_path = tempfile.mkstemp(suffix=".png")
            os.close(fd)
            Image.fromarray(heightmap_grayscale).save(hm_temp_path)
            heightmap_path = hm_temp_path
            store.register_temp_file(body.session_id, hm_temp_path)

    # 2. Collect scalar parameters into a dict for the worker
    params: dict = {
        "target_width_mm": request.target_width_mm,
        "spacer_thick": request.spacer_thick,
        "structure_mode": request.structure_mode.value,
        "auto_bg": request.auto_bg,
        "bg_tol": request.bg_tol,
        "color_mode": request.color_mode.value,
        "add_loop": request.add_loop,
        "loop_width": request.loop_width,
        "loop_length": request.loop_length,
        "loop_hole": request.loop_hole,
        "loop_pos": request.loop_pos,
        "modeling_mode": core_modeling_mode,
        "quantize_colors": request.quantize_colors,
        "replacement_regions": replacement_regions,
        "separate_backing": request.separate_backing,
        "enable_relief": request.enable_relief,
        "height_mode": height_mode,
        "heightmap_path": heightmap_path,
        "color_height_map": request.color_height_map,
        "heightmap_max_height": request.heightmap_max_height,
        "enable_cleanup": request.enable_cleanup,
        "enable_outline": request.enable_outline,
        "outline_width": request.outline_width,
        "enable_cloisonne": request.enable_cloisonne,
        "wire_width_mm": request.wire_width_mm,
        "wire_height_mm": request.wire_height_mm,
        "free_color_set": free_color_set,
        "enable_coating": request.enable_coating,
        "coating_height_mm": request.coating_height_mm,
        "hue_weight": request.hue_weight,
    }

    # 3. CPU computation offloaded to process pool (only paths and scalars)
    try:
        result = await pool.submit(
            worker_generate_model,
            image_path,
            lut_path,
            params,
        )
    except asyncio.TimeoutError:
        raise HTTPException(status_code=504, detail="3MF generation timed out")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"3MF generation failed: {str(e)}")

    # 4. Result processing (I/O + FileRegistry, main thread)
    threemf_path: str | None = result.get("threemf_path")
    glb_path: str | None = result.get("glb_path")
    status_msg: str = result.get("status_msg", "")

    if not threemf_path or not os.path.exists(threemf_path):
        raise HTTPException(status_code=500, detail=status_msg or "3MF generation failed")

    # Register output files via FileRegistry
    sid = body.session_id
    download_id = registry.register_path(sid, threemf_path)

    preview_3d_url: str | None = None
    if glb_path and os.path.exists(glb_path):
        glb_id = registry.register_path(sid, glb_path)
        preview_3d_url = f"/api/files/{glb_id}"

    return GenerateResponse(
        status="ok",
        message=status_msg or "Model generated",
        download_url=f"/api/files/{download_id}",
        preview_3d_url=preview_3d_url,
        threemf_disk_path=threemf_path,
    )


@router.post("/batch")
async def convert_batch(
    images: list[UploadFile] = File(..., description="批量图像"),
    lut_name: str = Form(..., description="LUT 名称"),
    target_width_mm: float = Form(60.0, description="目标宽度 (mm)"),
    spacer_thick: float = Form(1.2, description="底板厚度 (mm)"),
    structure_mode: str = Form("Double-sided", description="打印结构模式"),
    auto_bg: bool = Form(False, description="自动去背景"),
    bg_tol: int = Form(40, description="背景容差"),
    color_mode: str = Form("4-Color", description="颜色模式"),
    modeling_mode: str = Form("high-fidelity", description="建模模式"),
    quantize_colors: int = Form(48, description="K-Means 色彩细节"),
    enable_cleanup: bool = Form(True, description="孤立像素清理"),
    hue_weight: float = Form(0.0, description="色相保护权重"),
    registry: FileRegistry = Depends(get_file_registry),
    pool: WorkerPoolManager = Depends(get_worker_pool),
) -> BatchResponse:
    """Batch-convert multiple images via process pool.
    通过进程池批量转换多张图像。

    File uploads and FileRegistry operations run on the main thread.
    Each batch item's CPU-intensive conversion is submitted sequentially
    to the worker pool, one at a time.
    文件上传和 FileRegistry 操作在主线程完成。
    每个批量项的 CPU 密集型转换逐个提交到工作进程池。
    """
    # Resolve LUT path (main thread)
    lut_path = LUTManager.get_lut_path(lut_name)
    if lut_path is None:
        raise HTTPException(status_code=404, detail=f"LUT not found: {lut_name}")

    # Validate modeling_mode string (main thread)
    try:
        CoreModelingMode(modeling_mode)
    except ValueError:
        raise HTTPException(
            status_code=422,
            detail=f"Invalid modeling_mode: {modeling_mode}",
        )

    results: list[BatchItemResult] = []
    successful_paths: list[str] = []

    # Submit each batch item sequentially to the process pool
    for upload_file in images:
        filename = upload_file.filename or "unknown"
        try:
            # 1. File upload (I/O, main thread)
            temp_path = await upload_to_tempfile(upload_file)

            # 2. CPU computation offloaded to process pool (only paths and scalars)
            result = await pool.submit(
                worker_batch_convert_item,
                temp_path,
                lut_path,
                target_width_mm,
                spacer_thick,
                structure_mode,
                auto_bg,
                bg_tol,
                color_mode,
                modeling_mode,
                quantize_colors,
                enable_cleanup,
                hue_weight,
            )

            # 3. Result processing (main thread)
            threemf_path: str | None = result.get("threemf_path")
            status_msg: str = result.get("status_msg", "")

            if threemf_path and os.path.exists(threemf_path):
                successful_paths.append(threemf_path)
                results.append(BatchItemResult(
                    filename=filename,
                    status="success",
                ))
            else:
                results.append(BatchItemResult(
                    filename=filename,
                    status="failed",
                    error=status_msg or "3MF generation returned no output",
                ))
        except asyncio.TimeoutError:
            results.append(BatchItemResult(
                filename=filename,
                status="failed",
                error="Batch item conversion timed out",
            ))
        except Exception as e:
            results.append(BatchItemResult(
                filename=filename,
                status="failed",
                error=str(e),
            ))

    # Package successful 3MF files into a ZIP (main thread)
    fd, zip_path = tempfile.mkstemp(suffix=".zip")
    os.close(fd)
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for path_3mf in successful_paths:
            zf.write(path_3mf, os.path.basename(path_3mf))

    # Register ZIP via FileRegistry (main thread)
    session_id = "batch"
    download_id = registry.register_path(session_id, zip_path)

    success_count = sum(1 for r in results if r.status == "success")
    total_count = len(results)

    return BatchResponse(
        status="ok" if success_count > 0 else "failed",
        message=f"Batch complete: {success_count}/{total_count} succeeded",
        download_url=f"/api/files/{download_id}",
        results=results,
    )


@router.post("/replace-color")
def replace_color(
    request: ColorReplaceRequest,
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> ColorReplaceResponse:
    """Replace a single color in the current session preview (synchronous).
    替换当前 session 预览中的单个颜色(同步执行)。

    This endpoint is intentionally kept synchronous (no process pool offload)
    because the computation is lightweight: it operates on the cached
    matched_rgb array (small preview image) with simple NumPy mask operations
    over a small number of user-driven color replacements (typically 1-10).
    The heavy image processing was already completed in the /preview step.
    此端点有意保持同步执行(不卸载到进程池),因为计算量很小:
    仅对缓存的 matched_rgb 数组(小尺寸预览图)执行简单的 NumPy 掩码操作,
    颜色替换数量由用户驱动(通常 1-10 个)。
    繁重的图像处理已在 /preview 步骤中完成。

    Args:
        request (ColorReplaceRequest): Color replacement parameters. (颜色替换参数)
        store (SessionStore): Session store dependency. (会话存储依赖)
        registry (FileRegistry): File registry dependency. (文件注册表依赖)

    Returns:
        ColorReplaceResponse: Replacement result with preview URL. (替换结果及预览 URL)

    Raises:
        HTTPException(404): Session not found. (会话不存在)
        HTTPException(409): No preview cache available. (无预览缓存)
        HTTPException(500): Internal processing error. (内部处理错误)
    """
    session_data = _require_session(store, request.session_id)
    cache = _require_preview_cache(session_data)

    try:
        # Parse hex colors to RGB tuples
        selected_rgb = ColorReplacementManager._hex_to_color(request.selected_color)
        replacement_rgb = ColorReplacementManager._hex_to_color(request.replacement_color)

        # Build manager from all existing replacement_regions
        manager = ColorReplacementManager()
        for record in session_data.get("replacement_regions", []):
            orig = ColorReplacementManager._hex_to_color(record["selected_color"])
            repl = ColorReplacementManager._hex_to_color(record["replacement_color"])
            manager.add_replacement(orig, repl)

        # Add the new replacement
        manager.add_replacement(selected_rgb, replacement_rgb)

        # Apply all replacements to the original matched_rgb
        matched_rgb: np.ndarray = cache["matched_rgb"]
        replaced_rgb = manager.apply_to_image(matched_rgb)

        # Generate preview PNG from replaced image
        preview_bytes = _image_to_png_bytes(replaced_rgb)
        preview_id = registry.register_bytes(
            request.session_id, preview_bytes, "preview_replaced.png"
        )

        # Save history snapshot (deep copy of regions before this change) for undo
        current_regions = session_data.get("replacement_regions", [])
        snapshot = [dict(r) for r in current_regions]
        history = list(session_data.get("replacement_history", []))
        history.append(snapshot)
        store.put(request.session_id, "replacement_history", history)

        # Append new replacement record
        current_regions.append({
            "selected_color": request.selected_color,
            "replacement_color": request.replacement_color,
        })
        store.put(request.session_id, "replacement_regions", current_regions)

    except HTTPException:
        raise
    except Exception as e:
        _handle_core_error(e, "Color replacement")

    return ColorReplaceResponse(
        status="ok",
        message="Color replaced successfully",
        preview_url=f"/api/files/{preview_id}",
        replacement_count=len(current_regions),
    )


def _rgb_to_lab(rgb_array: np.ndarray) -> np.ndarray:
    """Convert RGB array to CIELAB color space via OpenCV.
    通过 OpenCV 将 RGB 数组转换为 CIELAB 色彩空间。

    Args:
        rgb_array (np.ndarray): RGB values of shape (N, 3), dtype uint8. (RGB 值,形状 (N, 3))

    Returns:
        np.ndarray: LAB values of shape (N, 3), dtype float64. (LAB 值,形状 (N, 3))
    """
    rgb_2d = rgb_array.reshape(1, -1, 3).astype(np.uint8)
    lab_2d = cv2.cvtColor(rgb_2d, cv2.COLOR_RGB2LAB)
    return lab_2d.reshape(-1, 3).astype(np.float64)


@router.post("/merge-colors")
def merge_colors(
    request: ColorMergePreviewRequest,
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> MergePreviewResponse:
    """Preview the effect of merging similar colors (synchronous).
    预览合并相似颜色的效果(同步执行)。

    This endpoint is intentionally kept synchronous (no process pool offload)
    because the computation is lightweight: it operates on the cached palette
    (typically 3-64 colors) and the cached matched_rgb array from the /preview
    step. Delta-E calculations involve small NumPy arrays (palette-sized, not
    image-sized), and pixel replacement uses simple mask operations.
    The heavy image processing was already completed in the /preview step.
    此端点有意保持同步执行(不卸载到进程池),因为计算量很小:
    仅对缓存的调色板(通常 3-64 色)和 /preview 步骤缓存的 matched_rgb 数组操作。
    Delta-E 计算涉及小型 NumPy 数组(调色板级别,非图像级别),
    像素替换使用简单的掩码操作。
    繁重的图像处理已在 /preview 步骤中完成。

    Args:
        request (ColorMergePreviewRequest): Merge parameters. (合并参数)
        store (SessionStore): Session store dependency. (会话存储依赖)
        registry (FileRegistry): File registry dependency. (文件注册表依赖)

    Returns:
        MergePreviewResponse: Merge result with preview URL and quality metric.
                              (合并结果及预览 URL 和质量指标)

    Raises:
        HTTPException(404): Session not found. (会话不存在)
        HTTPException(409): No preview cache available. (无预览缓存)
        HTTPException(500): Internal processing error. (内部处理错误)
    """
    session_data = _require_session(store, request.session_id)
    cache = _require_preview_cache(session_data)

    try:
        # Extract palette from preview cache
        palette = extract_color_palette(cache)
        colors_before = len(palette)

        # If merge disabled, return empty merge with perfect quality
        if not request.merge_enable:
            preview_bytes = _image_to_png_bytes(cache["matched_rgb"])
            preview_id = registry.register_bytes(
                request.session_id, preview_bytes, "preview_merged.png"
            )
            store.put(request.session_id, "merge_map", {})
            return MergePreviewResponse(
                status="ok",
                message="Color merging disabled",
                preview_url=f"/api/files/{preview_id}",
                merge_map={},
                quality_metric=100.0,
                colors_before=colors_before,
                colors_after=colors_before,
            )

        # Build merge map using ColorMerger
        merger = ColorMerger(rgb_to_lab_func=_rgb_to_lab)
        merge_map = merger.build_merge_map(
            palette,
            threshold_percent=request.merge_threshold,
            max_distance=float(request.merge_max_distance),
        )

        # Apply merging to matched_rgb
        matched_rgb: np.ndarray = cache["matched_rgb"]
        merged_rgb = merger.apply_color_merging(matched_rgb, merge_map)

        # Calculate quality metric
        merged_palette = extract_color_palette({
            "matched_rgb": merged_rgb,
            "mask_solid": cache["mask_solid"],
        })
        quality = merger.calculate_quality_metric(palette, merged_palette, merge_map)
        colors_after = len(merged_palette)

        # Generate preview PNG
        preview_bytes = _image_to_png_bytes(merged_rgb)
        preview_id = registry.register_bytes(
            request.session_id, preview_bytes, "preview_merged.png"
        )

        # Store merge_map in session
        store.put(request.session_id, "merge_map", merge_map)

    except HTTPException:
        raise
    except Exception as e:
        _handle_core_error(e, "Color merging")

    return MergePreviewResponse(
        status="ok",
        message=f"Merged {len(merge_map)} colors",
        preview_url=f"/api/files/{preview_id}",
        merge_map=merge_map,
        quality_metric=round(quality, 2),
        colors_before=colors_before,
        colors_after=colors_after,
    )


================================================
FILE: api/routers/extractor.py
================================================
"""Extractor domain API router.
Extractor 领域 API 路由模块。
"""

from __future__ import annotations

import json
import os
from typing import List, Tuple

import numpy as np
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from PIL import Image

from api.dependencies import get_file_registry, get_session_store
from api.file_bridge import ndarray_to_png_bytes, pil_to_png_bytes, upload_to_ndarray
from api.file_registry import FileRegistry
from api.schemas.extractor import ExtractorManualFixRequest
from api.schemas.responses import ExtractResponse, ManualFixResponse
from api.session_store import SessionStore
from core.extractor import manual_fix_cell, run_extraction

router = APIRouter(prefix="/api/extractor", tags=["Extractor"])


def _handle_core_error(e: Exception, context: str) -> None:
    """将 core 模块异常转换为 HTTP 500 错误。"""
    print(f"[API] {context} error: {e}")
    raise HTTPException(status_code=500, detail=f"{context} failed: {str(e)}")


def _image_to_png_bytes(img: object) -> bytes:
    """将 ndarray 或 PIL Image 转换为 PNG 字节流。"""
    if isinstance(img, np.ndarray):
        return ndarray_to_png_bytes(img)
    if isinstance(img, Image.Image):
        return pil_to_png_bytes(img)
    raise TypeError(f"Unsupported image type: {type(img)}")


@router.post("/extract")
async def extractor_extract(
    image: UploadFile = File(..., description="校准板照片"),
    corner_points: str = Form(..., description="4 个角点坐标 JSON 数组 [[x,y],...]"),
    color_mode: str = Form("4-Color", description="校准颜色模式"),
    page: str = Form("Page 1", description="8-Color 页码"),
    offset_x: int = Form(0, description="水平采样偏移"),
    offset_y: int = Form(0, description="垂直采样偏移"),
    zoom: float = Form(1.0, description="透视校正缩放"),
    distortion: float = Form(0.0, description="畸变校正"),
    white_balance: bool = Form(False, description="白平衡校正"),
    vignette_correction: bool = Form(False, description="暗角校正"),
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> ExtractResponse:
    """Extract colors from a photographed calibration board.
    从拍摄的校准板照片中提取颜色。
    """
    # Parse corner_points from JSON string
    try:
        points: List[List[int]] = json.loads(corner_points)
    except (json.JSONDecodeError, TypeError) as e:
        raise HTTPException(status_code=422, detail=f"Invalid corner_points JSON: {e}")

    if len(points) != 4:
        raise HTTPException(
            status_code=422,
            detail=f"corner_points must contain exactly 4 points, got {len(points)}",
        )

    # Convert UploadFile to ndarray
    try:
        img_arr = await upload_to_ndarray(image)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=str(e))

    # Call core extraction (field name mapping: distortion->barrel, white_balance->wb, vignette_correction->bright)
    try:
        vis_img, preview_img, lut_path, status_msg = run_extraction(
            img=img_arr,
            points=points,
            offset_x=offset_x,
            offset_y=offset_y,
            zoom=zoom,
            barrel=distortion,
            wb=white_balance,
            bright=vignette_correction,
            color_mode=color_mode,
        )
    except Exception as e:
        _handle_core_error(e, "Color extraction")

    if lut_path is None:
        raise HTTPException(status_code=500, detail=status_msg or "Extraction failed")

    # Create session and store state
    session_id = store.create()
    store.put(session_id, "lut_path", lut_path)
    store.put(session_id, "color_mode", color_mode)

    # For 8-Color mode: save page-specific temp file
    if "8-Color" in color_mode and lut_path:
        import sys
        if getattr(sys, "frozen", False):
            assets_dir = os.path.join(os.getcwd(), "assets")
        else:
            assets_dir = "assets"
        os.makedirs(assets_dir, exist_ok=True)
        page_idx = 1 if "1" in str(page) else 2
        temp_path = os.path.join(assets_dir, f"temp_8c_page_{page_idx}.npy")
        try:
            lut = np.load(lut_path)
            np.save(temp_path, lut)
            store.put(session_id, "lut_path", temp_path)
            lut_path = temp_path
        except Exception as e:
            print(f"[8-COLOR] Error saving page {page_idx}: {e}")

    # For 5-Color Extended mode: save page-specific temp file
    if "5-Color" in color_mode and lut_path:
        import sys
        if getattr(sys, "frozen", False):
            assets_dir = os.path.join(os.getcwd(), "assets")
        else:
            assets_dir = "assets"
        os.makedirs(assets_dir, exist_ok=True)
        page_idx: int = 1 if "1" in str(page) else 2
        temp_path = os.path.join(assets_dir, f"temp_5c_ext_page_{page_idx}.npy")
        try:
            lut = np.load(lut_path)
            np.save(temp_path, lut)
            store.put(session_id, "lut_path", temp_path)
            lut_path = temp_path
        except Exception as e:
            print(f"[5-COLOR-EXT] Error saving page {page_idx}: {e}")

    # Register LUT file
    lut_download_id = registry.register_path(session_id, lut_path)

    # Register warp view (visualization)
    warp_view_id = ""
    if vis_img is not None:
        vis_bytes = _image_to_png_bytes(vis_img)
        warp_view_id = registry.register_bytes(session_id, vis_bytes, "warp_view.png")

    # Register LUT preview
    lut_preview_id = ""
    if preview_img is not None:
        preview_bytes = _image_to_png_bytes(preview_img)
        lut_preview_id = registry.register_bytes(
            session_id, preview_bytes, "lut_preview.png"
        )

    return ExtractResponse(
        session_id=session_id,
        status="ok",
        message=status_msg or "Extraction complete",
        lut_download_url=f"/api/files/{lut_download_id}",
        warp_view_url=f"/api/files/{warp_view_id}" if warp_view_id else "",
        lut_preview_url=f"/api/files/{lut_preview_id}" if lut_preview_id else "",
    )


@router.post("/manual-fix")
def extractor_manual_fix(
    request: ExtractorManualFixRequest,
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> ManualFixResponse:
    """Manually override a single LUT cell color value.
    手动覆盖单个 LUT 单元格的颜色值。
    """
    # Resolve lut_path: prefer session lookup, fallback to direct path
    lut_path = request.lut_path
    if request.session_id:
        session_data = store.get(request.session_id)
        if session_data and "lut_path" in session_data:
            lut_path = session_data["lut_path"]

    try:
        preview_result, status_msg = manual_fix_cell(
            coord=request.cell_coord,
            color_input=request.override_color,
            lut_path=lut_path,
        )
    except Exception as e:
        _handle_core_error(e, "Manual fix")

    if preview_result is None:
        raise HTTPException(status_code=500, detail=status_msg or "Manual fix failed")

    # Register updated preview
    preview_bytes = _image_to_png_bytes(preview_result)
    sid = "extractor-fix"
    preview_id = registry.register_bytes(sid, preview_bytes, "lut_preview.png")

    return ManualFixResponse(
        status="ok",
        message=status_msg or "Cell updated",
        lut_preview_url=f"/api/files/{preview_id}",
    )


@router.post("/merge-5color-extended")
def extractor_merge_5color_extended(
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> ExtractResponse:
    """Merge two 5-Color Extended pages into a single LUT.
    合并两页 5 色扩展 LUT 为一个完整 LUT。
    """
    import sys
    from config import LUT_FILE_PATH

    if getattr(sys, "frozen", False):
        assets_dir = os.path.join(os.getcwd(), "assets")
    else:
        assets_dir = "assets"

    path1 = os.path.join(assets_dir, "temp_5c_ext_page_1.npy")
    path2 = os.path.join(assets_dir, "temp_5c_ext_page_2.npy")

    if not os.path.exists(path1) or not os.path.exists(path2):
        raise HTTPException(
            status_code=400,
            detail="Missing temp pages. Please extract Page 1 and Page 2 first.",
        )

    try:
        lut1 = np.load(path1).reshape(-1, 3)
        lut2 = np.load(path2).reshape(-1, 3)
        merged = np.vstack([lut1, lut2])
        np.save(LUT_FILE_PATH, merged)
    except Exception as e:
        _handle_core_error(e, "5-Color Extended merge")

    # Create session for merged result
    session_id = store.create()
    store.put(session_id, "lut_path", LUT_FILE_PATH)
    store.put(session_id, "color_mode", "5-Color Extended")

    lut_download_id = registry.register_path(session_id, LUT_FILE_PATH)

    return ExtractResponse(
        session_id=session_id,
        status="ok",
        message=f"5-Color Extended LUT merged ({merged.shape[0]}x{merged.shape[1]})",
        lut_download_url=f"/api/files/{lut_download_id}",
        warp_view_url="",
        lut_preview_url="",
    )


@router.post("/merge-8color")
def extractor_merge_8color(
    store: SessionStore = Depends(get_session_store),
    registry: FileRegistry = Depends(get_file_registry),
) -> ExtractResponse:
    """Merge two 8-Color pages into a single LUT.
    合并两页 8 色 LUT 为一个完整 LUT。
    """
    import sys
    from config import LUT_FILE_PATH

    if getattr(sys, "frozen", False):
        assets_dir = os.path.join(os.getcwd(), "assets")
    else:
        assets_dir = "assets"

    path1 = os.path.join(assets_dir, "temp_8c_page_1.npy")
    path2 = os.path.join(assets_dir, "temp_8c_page_2.npy")

    if not os.path.exists(path1) or not os.path.exists(path2):
        raise HTTPException(
            status_code=400,
            detail="Missing temp pages. Please extract Page 1 and Page 2 first.",
        )

    try:
        lut1 = np.load(path1)
        lut2 = np.load(path2)
        merged = np.concatenate([lut1, lut2], axis=0)
        np.save(LUT_FILE_PATH, merged)
    except Exception as e:
        _handle_core_error(e, "8-Color merge")

    # Create session for merged result
    session_id = store.create()
    store.put(session_id, "lut_path", LUT_FILE_PATH)
    store.put(session_id, "color_mode", "8-Color Max")

    lut_download_id = registry.register_path(session_id, LUT_FILE_PATH)

    return ExtractResponse(
        session_id=session_id,
        status="ok",
        message=f"8-Color LUT merged ({merged.shape[0]}x{merged.shape[1]})",
        lut_download_url=f"/api/files/{lut_download_id}",
        warp_view_url="",
        lut_preview_url="",
    )


================================================
FILE: api/routers/five_color.py
================================================
"""Lumina Studio API — Five-Color Query Router.
Lumina Studio API — 五色组合查询路由。

Provides endpoints for querying base colors from a LUT and
performing five-color combination lookups.
提供从 LUT 获取基础颜色和执行五色组合查询的端点。
"""

from fastapi import APIRouter, HTTPException, Query

from api.schemas.five_color import (
    BaseColorEntry,
    BaseColorsResponse,
    FiveColorQueryRequest,
    FiveColorQueryResponse,
)
from core.five_color_combination import (
    ColorCountDetector,
    ColorQueryEngine,
    StackFileManager,
    StackLUTLoader,
    rgb_to_hex,
)
from utils.lut_manager import LUTManager

router = APIRouter(prefix="/api/five-color", tags=["Five-Color"])


def _load_engine(lut_name: str) -> tuple[ColorQueryEngine, str]:
    """Load a LUT and create a ColorQueryEngine.
    加载 LUT 并创建 ColorQueryEngine。

    Args:
        lut_name: LUT 显示名称。

    Returns:
        (engine, lut_display_name)

    Raises:
        HTTPException 404: LUT 不存在。
        HTTPException 400: LUT 格式无法识别。
        HTTPException 500: 加载失败。
    """
    path: str | None = LUTManager.get_lut_path(lut_name)
    if path is None:
        raise HTTPException(status_code=404, detail=f"LUT not found: {lut_name}")

    try:
        if path.endswith(".npz"):
            success, msg, stack_data, rgb_data = StackLUTLoader.load_npz_file(path)
            if not success:
                raise HTTPException(status_code=500, detail=f"Failed to load LUT: {msg}")
            engine = ColorQueryEngine(stack_lut=stack_data, lut_rgb=rgb_data)
        else:
            # .npy file
            success, msg, rgb_data = StackLUTLoader.load_lut_rgb(path)
            if not success:
                raise HTTPException(status_code=500, detail=f"Failed to load LUT: {msg}")

            color_count, combination_count = ColorCountDetector.detect_color_count(rgb_data)
            if color_count == 0:
                raise HTTPException(
                    status_code=400,
                    detail=f"Unrecognized LUT format: {combination_count} combinations",
                )

            stack_path = StackFileManager.find_stack_file(color_count)
            stack_data = None
            if stack_path is not None:
                ok, _, loaded_stack = StackLUTLoader.load_stack_lut(stack_path)
                if ok:
                    stack_data = loaded_stack

            engine = ColorQueryEngine(
                stack_lut=stack_data, lut_rgb=rgb_data, color_count=color_count
            )

        return engine, lut_name

    except HTTPException:
        raise
    except Exception as exc:
        raise HTTPException(
            status_code=500, detail=f"Failed to load LUT: {exc}"
        ) from exc


@router.get("/base-colors")
def get_base_colors(lut_name: str = Query(..., description="LUT 显示名称")) -> BaseColorsResponse:
    """Return the base colors of a LUT.
    返回指定 LUT 的基础颜色列表。
    """
    engine, display_name = _load_engine(lut_name)
    base_colors = engine.get_base_colors()
    color_names = engine.get_color_names()

    colors: list[BaseColorEntry] = [
        BaseColorEntry(
            index=i,
            rgb=rgb,
            name=color_names[i] if i < len(color_names) else "",
            hex=rgb_to_hex(rgb),
        )
        for i, rgb in enumerate(base_colors)
    ]

    return BaseColorsResponse(
        lut_name=display_name,
        color_count=len(colors),
        colors=colors,
    )


@router.post("/query")
def query_five_color(request: FiveColorQueryRequest) -> FiveColorQueryResponse:
    """Query a five-color combination result.
    查询五色组合结果。
    """
    engine, _ = _load_engine(request.lut_name)

    # Validate index range
    for idx in request.selected_indices:
        if idx < 0 or idx >= engine.color_count:
            raise HTTPException(
                status_code=400,
                detail=f"Index {idx} out of range [0, {engine.color_count})",
            )

    result = engine.query(request.selected_indices)

    return FiveColorQueryResponse(
        found=result.found,
        selected_indices=result.selected_indices,
        result_rgb=result.result_rgb,
        result_hex=rgb_to_hex(result.result_rgb) if result.result_rgb else None,
        row_index=result.row_index,
        message=result.message,
    )


================================================
FILE: api/routers/health.py
================================================
"""Lumina Studio API — Health Check Router.
Lumina Studio API — 健康检查路由。

Provides a ``GET /api/health`` endpoint that returns service status,
version, uptime, and Worker Pool health information.
提供 ``GET /api/health`` 端点,返回服务状态、版本号、运行时间和 Worker Pool 健康信息。
"""

import time

from fastapi import APIRouter, Depends

from api.dependencies import get_worker_pool
from api.schemas.responses import HealthResponse, WorkerPoolStatus
from api.worker_pool import WorkerPoolManager

router = APIRouter(prefix="/api", tags=["Health"])

_start_time: float = time.time()


@router.get("/health")
def health_check(
    pool: WorkerPoolManager = Depends(get_worker_pool),
) -> HealthResponse:
    """Return service health status including Worker Pool state.
    返回服务健康状态信息,包含 Worker Pool 运行状况。
    """
    return HealthResponse(
        status="ok",
        version="2.0",
        uptime_seconds=round(time.time() - _start_time, 2),
        worker_pool=WorkerPoolStatus(
            healthy=pool.is_alive,
            max_workers=pool.max_workers,
        ),
    )


================================================
FILE: api/routers/lut.py
================================================
"""Lumina Studio API — LUT Management Router.
Lumina Studio API — LUT 管理路由。

Provides endpoints for LUT preset listing, information queries, and merge operations.
提供 LUT 预设列表、信息查询和合并操作端点。
"""

import os
import time

from fastapi import APIRouter, HTTPException

from api.schemas.lut import (
    LutInfoResponse,
    MergeRequest,
    MergeResponse,
    MergeStats,
)
from api.schemas.responses import LUTListResponse, LutInfo, LutColorsResponse, LutColorEntry
from core.lut_merger import LUTMerger
from utils.lut_manager import LUTManager

router = APIRouter(prefix="/api/lut", tags=["LUT"])

_VALID_PRIMARY_MODES = {"6-Color", "8-Color", "8-Color Max"}


@router.get("/list")
def list_luts() -> LUTListResponse:
    """Return all available LUT presets as a list of LutInfo objects.
    返回所有可用 LUT 预设,以 LutInfo 对象列表形式返回。
    """
    lut_dict: dict[str, str] = LUTManager.get_all_lut_files()
    lut_list: list[LutInfo] = [
        LutInfo(
            name=display_name,
            color_mode=LUTManager.infer_color_mode(display_name, file_path),
            path=file_path,
        )
        for display_name, file_path in lut_dict.items()
    ]
    return LUTListResponse(luts=lut_list)


@router.post("/merge")
def merge_luts_endpoint(request: MergeRequest) -> MergeResponse:
    """Execute LUT merge: primary + multiple secondary LUTs.
    执行 LUT 合并:主 LUT + 多个辅助 LUT。

    Replicates the flow of ``ui/callbacks.py::on_merge_execute``:
    resolve paths → detect modes → validate compatibility →
    load data → skip Merged secondaries → merge → save to Custom dir.
    复刻 ``ui/callbacks.py::on_merge_execute`` 的完整流程:
    解析路径 → 检测模式 → 验证兼容性 → 加载数据 → 跳过 Merged Secondary → 合并 → 保存到 Custom 目录。
    """
    # 1. Validate secondary list not empty / 验证 Secondary 列表非空
    if not request.secondary_names:
        raise HTTPException(
            status_code=400,
            detail="At least one secondary LUT is required",
        )

    # 2. Resolve primary path / 解析主 LUT 路径
    primary_path: str | None = LUTManager.get_lut_path(request.primary_name)
    if primary_path is None:
        raise HTTPException(
            status_code=404,
            detail=f"LUT not found: {request.primary_name}",
        )

    try:
        # 3. Detect primary mode / 检测主 LUT 模式
        primary_mode, _ = LUTMerger.detect_color_mode(primary_path)
        if primary_mode not in _VALID_PRIMARY_MODES:
            raise HTTPException(
                status_code=400,
                detail="Primary LUT must be 6-Color or 8-Color",
            )

        # 4. Load primary data / 加载主 LUT 数据
        primary_rgb, primary_stacks = LUTMerger.load_lut_with_stacks(
            primary_path, primary_mode
        )
        entries = [(primary_rgb, primary_stacks, primary_mode)]
        all_modes: list[str] = [primary_mode]

        # 5. Load each secondary, skip Merged / 加载辅助 LUT,跳过 Merged
        for sec_name in request.secondary_names:
            sec_path: str | None = LUTManager.get_lut_path(sec_name)
            if sec_path is None:
                continue
            sec_mode, _ = LUTMerger.detect_color_mode(sec_path)
            if sec_mode == "Merged":
                continue
            sec_rgb, sec_stacks = LUTMerger.load_lut_with_stacks(
                sec_path, sec_mode
            )
            entries.append((sec_rgb, sec_stacks, sec_mode))
            all_modes.append(sec_mode)

        # 6. Need at least 2 entries / 至少需要 2 个有效条目
        if len(entries) < 2:
            raise HTTPException(
                status_code=400,
                detail="At least one secondary LUT is required",
            )

        # 7. Validate compatibility / 验证兼容性
        valid, err_msg = LUTMerger.validate_compatibility(all_modes)
        if not valid:
            raise HTTPException(status_code=400, detail=err_msg)

        # 8. Merge / 执行合并
        merged_rgb, merged_stacks, stats = LUTMerger.merge_luts(
            entries, dedup_threshold=request.dedup_threshold
        )

        # 9. Save to Custom dir / 保存到 Custom 目录
        timestamp: str = time.strftime("%Y%m%d_%H%M%S")
        mode_str: str = "+".join(all_modes)
        output_name: str = f"Merged_{mode_str}_{timestamp}.npz"
        custom_dir: str = os.path.join(LUTManager.LUT_PRESET_DIR, "Custom")
        os.makedirs(custom_dir, exist_ok=True)
        output_path: str = os.path.join(custom_dir, output_name)

        LUTMerger.save_merged_lut(merged_rgb, merged_stacks, output_path)

        # 10. Return response / 返回响应
        return MergeResponse(
            status="success",
            message=f"Merged {stats['total_before']} colors → {stats['total_after']} colors",
            filename=output_name,
            stats=MergeStats(
                total_before=stats["total_before"],
                total_after=stats["total_after"],
                exact_dupes=stats["exact_dupes"],
                similar_removed=stats["similar_removed"],
            ),
        )

    except HTTPException:
        raise
    except Exception as exc:
        raise HTTPException(
            status_code=500,
            detail=f"Merge failed: {exc}",
        ) from exc


@router.get("/{lut_name}/colors")
def get_lut_colors(lut_name: str) -> LutColorsResponse:
    """Return all unique colors available in a LUT file.
    返回 LUT 文件中所有可用的唯一颜色。
    """
    path: str | None = LUTManager.get_lut_path(lut_name)
    if path is None:
        raise HTTPException(status_code=404, detail=f"LUT not found: {lut_name}")

    from core.converter import extract_lut_available_colors

    raw_colors: list[dict] = extract_lut_available_colors(path)
    entries: list[LutColorEntry] = [
        LutColorEntry(hex=c["hex"], rgb=c["color"]) for c in raw_colors
    ]
    return LutColorsResponse(lut_name=lut_name, total=len(entries), colors=entries)


@router.get("/{lut_name}/info")
def get_lut_info(lut_name: str) -> LutInfoResponse:
    """Return color mode and color count for a specific LUT.
    返回指定 LUT 的颜色模式和颜色数量。
    """
    path: str | None = LUTManager.get_lut_path(lut_name)
    if path is None:
        raise HTTPException(status_code=404, detail=f"LUT not found: {lut_name}")

    color_mode, color_count = LUTMerger.detect_color_mode(path)
    return LutInfoResponse(name=lut_name, color_mode=color_mode, color_count=color_count)


================================================
FILE: api/routers/slicer.py
================================================
"""Slicer domain API router.
Slicer 领域 API 路由模块 — 切片软件检测与启动端点。
"""

from __future__ import annotations

import os

from fastapi import APIRouter
from fastapi.responses import JSONResponse

from api.schemas.slicer import (
    SlicerDetectResponse,
    SlicerInfo,
    SlicerLaunchRequest,
    SlicerLaunchResponse,
)
from core.slicer import detect_installed_slicers, launch_slicer

router = APIRouter(prefix="/api/slicer", tags=["Slicer"])


@router.get("/detect")
def detect_slicers() -> SlicerDetectResponse:
    """扫描系统已安装的切片软件。"""
    slicers = detect_installed_slicers()
    return SlicerDetectResponse(
        slicers=[
            SlicerInfo(
                id=s.id,
                display_name=s.display_name,
                exe_path=s.exe_path,
            )
            for s in slicers
        ]
    )


@router.post("/launch")
def launch_slicer_endpoint(request: SlicerLaunchRequest) -> SlicerLaunchResponse:
    """启动切片软件打开指定文件。"""
    # 1) Validate file exists
    if not os.path.isfile(request.file_path):
        return JSONResponse(
            status_code=400,
            content=SlicerLaunchResponse(
                status="error",
                message=f"文件不存在: {request.file_path}",
            ).model_dump(),
        )

    # 2) Get fresh slicer list
    slicers = detect_installed_slicers()

    # 3) Attempt launch
    try:
        success, message = launch_slicer(request.slicer_id, request.file_path, slicers)
    except Exception as exc:
        return JSONResponse(
            status_code=500,
            content=SlicerLaunchResponse(
                status="error",
                message=f"启动失败: {exc}",
            ).model_dump(),
        )

    if not success:
        # Distinguish 404 (slicer not found) from 500 (launch failure)
        if "not found" in message.lower():
            return JSONResponse(
                status_code=404,
                content=SlicerLaunchResponse(
                    status="error",
                    message=f"未找到切片软件: {request.slicer_id}",
                ).model_dump(),
            )
        return JSONResponse(
            status_code=500,
            content=SlicerLaunchResponse(
                status="error",
                message=f"启动失败: {message}",
            ).model_dump(),
        )

    return SlicerLaunchResponse(status="success", message=message)


================================================
FILE: api/routers/system.py
================================================
"""Lumina Studio API — System Management Router.
Lumina Studio API — 系统管理路由。

Provides cache cleanup utilities and the ``POST /api/system/clear-cache``
endpoint (endpoint registered in a later task).
提供缓存清理工具函数,以及 ``POST /api/system/clear-cache`` 端点
(端点在后续任务中注册)。
"""

import json
import os
from pathlib import Path

from fastapi import APIRouter, Depends, HTTPException

import config
from api.dependencies import get_file_registry, get_session_store
from api.file_registry import FileRegistry
from api.schemas.system import (
    CacheCleanupDetails,
    ClearCacheResponse,
    ClearCacheResult,
    SaveSettingsResponse,
    StatsResponse,
    UserSettings,
    UserSettingsResponse,
)
from api.session_store import SessionStore

router = APIRouter(prefix="/api/system", tags=["System"])

CLEANABLE_EXTENSIONS: set[str] = {".3mf", ".glb", ".png", ".jpg"}


def cleanup_output_dir(output_dir: str) -> tuple[int, int]:
    """Scan *output_dir* and delete files whose extension is in
    :data:`CLEANABLE_EXTENSIONS`.

    扫描 *output_dir*,删除扩展名匹配的临时文件。

    Returns:
        ``(deleted_count, freed_bytes)``。
        If *output_dir* does not exist, returns ``(0, 0)`` without raising.
    """
    if not os.path.isdir(output_dir):
        return 0, 0

    deleted_count = 0
    freed_bytes = 0

    for entry in os.scandir(output_dir):
        if not entry.is_file():
            continue
        _, ext = os.path.splitext(entry.name)
        if ext.lower() not in CLEANABLE_EXTENSIONS:
            continue
        try:
            size = entry.stat().st_size
            os.remove(entry.path)
            deleted_count += 1
            freed_bytes += size
        except OSError:
            pass

    return deleted_count, freed_bytes


def perform_cache_cleanup(
    file_registry: FileRegistry,
    session_store: SessionStore,
    output_dir: str,
) -> ClearCacheResult:
    """Coordinate a full cache cleanup across all subsystems.

    执行缓存清理,依次清理 FileRegistry、SessionStore 和 OUTPUT_DIR,
    返回汇总统计结果。

    Steps:
        1. ``FileRegistry.clear_all()`` — clear registry & delete files
        2. ``SessionStore.clear_all()`` — clear sessions & temp files
        3. ``cleanup_output_dir()`` — delete matching files in OUTPUT_DIR
    """
    registry_cleaned: int = file_registry.clear_all()
    sessions_cleaned: int = session_store.clear_all()
    output_files_cleaned, freed_bytes = cleanup_output_dir(output_dir)

    return ClearCacheResult(
        registry_cleaned=registry_cleaned,
        sessions_cleaned=sessions_cleaned,
        output_files_cleaned=output_files_cleaned,
        total_freed_bytes=freed_bytes,
    )


@router.post("/clear-cache")
def clear_cache(
    file_registry: FileRegistry = Depends(get_file_registry),
    session_store: SessionStore = Depends(get_session_store),
) -> ClearCacheResponse:
    """Clear all cached/temporary files across subsystems.

    清除所有子系统中的缓存和临时文件,返回清理统计信息。
    """
    result: ClearCacheResult = perform_cache_cleanup(
        file_registry, session_store, config.OUTPUT_DIR
    )
    total_deleted: int = (
        result.registry_cleaned
        + result.sessions_cleaned
        + result.output_files_cleaned
    )
    return ClearCacheResponse(
        status="success",
        message=f"Cache cleared: {total_deleted} files deleted",
        deleted_files=total_deleted,
        freed_bytes=result.total_freed_bytes,
        details=CacheCleanupDetails(
            registry_cleaned=result.registry_cleaned,
            sessions_cleaned=result.sessions_cleaned,
            output_files_cleaned=result.output_files_cleaned,
        ),
    )


# ---------------------------------------------------------------------------
# Settings & Stats endpoints
# ---------------------------------------------------------------------------

SETTINGS_FILE: Path = Path("user_settings.json")


@router.get("/settings")
def get_settings() -> UserSettingsResponse:
    """读取 user_settings.json 并返回 UserSettings。文件不存在时返回默认值。"""
    if not SETTINGS_FILE.exists():
        return UserSettingsResponse(status="success", settings=UserSettings())
    try:
        data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
        return UserSettingsResponse(status="success", settings=UserSettings(**data))
    except (json.JSONDecodeError, ValueError):
        return UserSettingsResponse(status="success", settings=UserSettings())


@router.post("/settings")
def save_settings(settings: UserSettings) -> SaveSettingsResponse:
    """将 UserSettings 写入 user_settings.json。"""
    try:
        SETTINGS_FILE.write_text(
            json.dumps(settings.model_dump(), indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
        return SaveSettingsResponse(status="success", message="Settings saved")
    except OSError as e:
        raise HTTPException(
            status_code=500, detail=f"Failed to save settings: {e}"
        )


@router.get("/stats")
def get_stats() -> StatsResponse:
    """获取使用统计数据。"""
    from utils.stats import Stats

    data: dict = Stats.get_all()
    return StatsResponse(
        calibrations=data.get("calibrations", 0),
        extractions=data.get("extractions", 0),
        conversions=data.get("conversions", 0),
    )


================================================
FILE: api/schemas/__init__.py
================================================
"""Lumina Studio API — Pydantic schemas and enums re-exports.
Lumina Studio API — Pydantic 数据模型与枚举的统一导出。

This package re-exports all domain schemas and enums so that consumers
can import directly from ``api.schemas`` instead of reaching into
individual domain modules.
本包统一导出所有领域 Schema 和枚举,使用方可直接从 ``api.schemas``
导入,无需深入各领域子模块。
"""

from api.schemas.five_color import (
    BaseColorEntry,
    BaseColorsResponse,
    FiveColorQueryRequest,
    FiveColorQueryResponse,
)
from api.schemas.calibration import BackingColor, CalibrationGenerateRequest
from api.schemas.converter import (
    AutoHeightMode,
    ColorMergePreviewRequest,
    ColorMode,
    ColorReplaceRequest,
    ColorReplacementItem,
    ConvertBatchRequest,
    ConvertGenerateRequest,
    ConvertPreviewRequest,
    ModelingMode,
    StructureMode,
)
from api.schemas.extractor import (
    CalibrationColorMode,
    ExtractorExtractRequest,
    ExtractorManualFixRequest,
    ExtractorPage,
)
from api.schemas.lut import (
    LutInfoResponse,
    MergeRequest,
    MergeResponse,
    MergeStats,
)
from api.schemas.system import (
    CacheCleanupDetails,
    ClearCacheResponse,
    ClearCacheResult,
)
from api.schemas.slicer import (
    SlicerDetectResponse,
    SlicerInfo,
    SlicerLaunchRequest,
    SlicerLaunchResponse,
)
from api.schemas.responses import (
    BatchItemResult,
    BatchResponse,
    CalibrationResponse,
    ColorReplaceResponse,
    ExtractResponse,
    GenerateResponse,
    HealthResponse,
    LUTListResponse,
    ManualFixResponse,
    MergePreviewResponse,
    PreviewResponse,
)

__all__ = [
    # --- Converter enums ---
    "ColorMode",
    "ModelingMode",
    "StructureMode",
    "AutoHeightMode",
    # --- Converter models ---
    "ColorReplacementItem",
    "ConvertPreviewRequest",
    "ConvertGenerateRequest",
    "ConvertBatchRequest",
    "ColorReplaceRequest",
    "ColorMergePreviewRequest",
    # --- Extractor enums ---
    "CalibrationColorMode",
    "ExtractorPage",
    # --- Extractor models ---
    "ExtractorExtractRequest",
    "ExtractorManualFixRequest",
    # --- Calibration enums ---
    "BackingColor",
    # --- Calibration models ---
    "CalibrationGenerateRequest",
    # --- LUT Manager models ---
    "MergeRequest",
    "MergeResponse",
    "MergeStats",
    "LutInfoResponse",
    # --- Slicer models ---
    "SlicerInfo",
    "SlicerDetectResponse",
    "SlicerLaunchRequest",
    "SlicerLaunchResponse",
    # --- System models ---
    "CacheCleanupDetails",
    "ClearCacheResponse",
    "ClearCacheResult",
    # --- Five-Color models ---
    "BaseColorEntry",
    "BaseColorsResponse",
    "FiveColorQueryRequest",
    "FiveColorQueryResponse",
    # --- Response models ---
    "CalibrationResponse",
    "PreviewResponse",
    "ColorReplaceResponse",
    "MergePreviewResponse",
    "GenerateResponse",
    "BatchItemResult",
    "BatchResponse",
    "LUTListResponse",
    "HealthResponse",
    "ExtractResponse",
    "ManualFixResponse",
]


================================================
FILE: api/schemas/calibration.py
================================================
"""Calibration domain Pydantic schemas and enums.
Calibration 领域的 Pydantic 数据模型与枚举定义。

This module defines the request model for calibration board generation,
including backing color options and block sizing parameters.
本模块定义校准板生成 API 的请求模型,
包括底板颜色选项和色块尺寸参数。
"""

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel, Field

from api.schemas.extractor import CalibrationColorMode


# ========== Enums ==========


class BackingColor(str, Enum):
    """Backing plate color for calibration board generation.
    校准板底板颜色。

    Attributes:
        WHITE: White backing plate.
            白色底板。
        CYAN: Cyan backing plate.
            青色底板。
        MAGENTA: Magenta backing plate.
            品红色底板。
        YELLOW: Yellow backing plate.
            黄色底板。
        RED: Red backing plate.
            红色底板。
        BLUE: Blue backing plate.
            蓝色底板。
    """

    WHITE = "White"
    CYAN = "Cyan"
    MAGENTA = "Magenta"
    YELLOW = "Yellow"
    RED = "Red"
    BLUE = "Blue"


# ========== Models ==========


class CalibrationGenerateRequest(BaseModel):
    """Request model for generating a calibration board.
    生成校准板的请求模型。

    Used by ``POST /api/calibration/generate`` to create a printable
    calibration board with specified color mode, block size, and spacing.
    用于 ``POST /api/calibration/generate``,按指定颜色模式、色块尺寸和间距
    生成可打印的校准板。

    Attributes:
        color_mode: Calibration color system mode.
            校准颜色模式。
        block_size: Block size in millimeters (3-10).
            色块尺寸 (mm)。
        gap: Gap between blocks in millimeters (0.4-2.0).
            色块间距 (mm)。
        backing: Backing plate color.
            底板颜色。
    """

    color_mode: CalibrationColorMode = Field(
        CalibrationColorMode.FOUR_COLOR, description="校准颜色模式"
    )
    block_size: int = Field(5, ge=3, le=10, description="色块尺寸 (mm)")
    gap: float = Field(0.82, ge=0.4, le=2.0, description="色块间距 (mm)")
    backing: BackingColor = Field(
        BackingColor.WHITE, description="底板颜色"
    )


================================================
FILE: api/schemas/converter.py
================================================
"""Converter domain Pydantic schemas and enums.
Converter 领域的 Pydantic 数据模型与枚举定义。

This module defines all request models for the image-to-3D conversion API,
including preview, generate, batch, color replacement, and color merging.
本模块定义图像转 3D 转换 API 的所有请求模型,
包括预览、生成、批量、颜色替换和颜色合并。
"""

from __future__ import annotations

from enum import Enum
from typing import Dict, List, Optional, Set, Tuple

from pydantic import BaseModel, Field


# ========== Enums ==========


class ColorMode(str, Enum):
    """Color system mode for the converter.
    转换器的颜色系统模式。

    Attributes:
        BW: Black & White grayscale mode (32 levels).
            黑白灰度模式 (32 级)。
        FOUR_COLOR: 4-Color CMYW/RYBW mode (1024 colors).
            4 色 CMYW/RYBW 模式 (1024 色)。
        SIX_COLOR: 6-Color extended smart mode (1296 colors).
            6 色扩展智能模式 (1296 色)。
        EIGHT_COLOR: 8-Color professional mode (2738 colors).
            8 色专业模式 (2738 色)。
        MERGED: Merged multi-mode LUT.
            合并多模式 LUT。
    """

    BW = "BW (Black & White)"
    FOUR_COLOR = "4-Color"
    CMYW = "CMYW"
    RYBW = "RYBW"
    SIX_COLOR = "6-Color (Smart 1296)"
    EIGHT_COLOR = "8-Color Max"
    MERGED = "Merged"


class ModelingMode(str, Enum):
    """3D modeling strategy mode.
    3D 建模策略模式。

    Attributes:
        HIGH_FIDELITY: RLE-based smooth meshing for high detail.
            基于 RLE 的平滑网格,高细节。
        PIXEL: Voxel-based blocky meshing for pixel art style.
            基于体素的块状网格,像素艺术风格。
        VECTOR: Native SVG-to-3D conversion.
            原生 SVG 转 3D 转换。
    """

    HIGH_FIDELITY = "high-fidelity"
    PIXEL = "pixel"
    VECTOR = "vector"


class StructureMode(str, Enum):
    """Print structure mode (single-sided or double-sided).
    打印结构模式(单面或双面)。

    Attributes:
        DOUBLE_SIDED: Double-sided structure with backing plate.
            双面结构,含底板。
        SINGLE_SIDED: Single-sided structure without backing plate.
            单面结构,无底板。
    """

    DOUBLE_SIDED = "Double-sided"
    SINGLE_SIDED = "Single-sided"


class AutoHeightMode(str, Enum):
    """Automatic height assignment mode for 2.5D relief.
    2.5D 浮雕的自动高度分配模式。

    Attributes:
        DARKER_HIGHER: Darker colors get higher relief.
            深色凸起。
        LIGHTER_HIGHER: Lighter colors get higher relief.
            浅色凸起。
        USE_HEIGHTMAP: Use external heightmap image for relief.
            根据高度图。
    """

    DARKER_HIGHER = "深色凸起"
    LIGHTER_HIGHER = "浅色凸起"
    USE_HEIGHTMAP = "根据高度图"


# ========== Models ==========


class ColorReplacementItem(BaseModel):
    """A single color replacement record.
    单条颜色替换记录。

    Maps a quantized source color through its LUT match to a user-chosen
    replacement color.
    将量化后的原色通过 LUT 匹配映射到用户选择的替换色。

    Attributes:
        quantized_hex: Quantized source color in #rrggbb format.
            量化后的原色 (#rrggbb)。
        matched_hex: LUT-matched color in #rrggbb format.
            LUT 匹配色 (#rrggbb)。
        replacement_hex: User-chosen replacement color in #rrggbb format.
            替换目标色 (#rrggbb)。
    """

    quantized_hex: str = Field(..., description="量化后的原色 (#rrggbb)")
    matched_hex: str = Field(..., description="LUT 匹配色 (#rrggbb)")
    replacement_hex: str = Field(..., description="替换目标色 (#rrggbb)")


class ConvertPreviewRequest(BaseModel):
    """Request model for generating a 2D color preview.
    生成 2D 颜色预览的请求模型。

    Used by ``POST /api/convert/preview`` to produce a quick preview image
    showing how the input image will be color-matched against the LUT.
    用于 ``POST /api/convert/preview``,生成快速预览图,
    展示输入图像与 LUT 的颜色匹配效果。

    Attributes:
        lut_name: Name of the LUT to use (from LUT list).
            LUT 名称(从 LUT 列表获取)。
        target_width_mm: Target output width in millimeters.
            目标宽度 (mm)。
        auto_bg: Whether to automatically remove background.
            是否自动去背景。
        bg_tol: Background removal tolerance.
            背景容差。
        color_mode: Color system mode.
            颜色模式。
        modeling_mode: 3D modeling strategy.
            建模模式。
        quantize_colors: Number of K-Means quantization colors.
            K-Means 色彩细节。
        enable_cleanup: Whether to clean up isolated pixels.
            是否启用孤立像素清理。
    """

    lut_name: str = Field(..., description="LUT 名称")
    target_width_mm: float = Field(
        60.0, ge=10, le=400, description="目标宽度 (mm)"
    )
    auto_bg: bool = Field(False, description="自动去背景")
    bg_tol: int = Field(40, ge=0, le=150, description="背景容差")
    color_mode: ColorMode = Field(
        ColorMode.FOUR_COLOR, description="颜色模式"
    )
    modeling_mode: ModelingMode = Field(
        ModelingMode.HIGH_FIDELITY, description="建模模式"
    )
    quantize_colors: int = Field(48, ge=8, le=256, description="K-Means 色彩细节")
    enable_cleanup: bool = Field(True, description="孤立像素清理")
    hue_weight: float = Field(0.0, ge=0.0, le=1.0, description="色相保护权重 (0=纯色差, 0.5=推荐, 1.0=最强)")


class ConvertGenerateRequest(BaseModel):
    """Request model for generating a final 3MF model.
    生成最终 3MF 模型的请求模型。

    Used by ``POST /api/convert/generate`` to produce a printable 3MF file
    with full parameter control including relief, outline, cloisonne, coating,
    keychain loop, and color replacement options.
    用于 ``POST /api/convert/generate``,生成可打印的 3MF 文件,
    支持浮雕、描边、掐丝珐琅、涂层、挂件环和颜色替换等完整参数控制。

    Attributes:
        lut_name: Name of the LUT to use.
            LUT 名称。
        target_width_mm: Target output width in millimeters.
            目标宽度 (mm)。
        spacer_thick: Backing plate thickness in millimeters.
            底板厚度 (mm)。
        structure_mode: Print structure mode.
            打印结构模式。
        auto_bg: Whether to automatically remove background.
            是否自动去背景。
        bg_tol: Background removal tolerance.
            背景容差。
        color_mode: Color system mode.
            颜色模式。
        modeling_mode: 3D modeling strategy.
            建模模式。
        quantize_colors: Number of K-Means quantization colors.
            K-Means 色彩细节。
        enable_cleanup: Whether to clean up isolated pixels.
            是否启用孤立像素清理。
        separate_backing: Whether to export backing plate as separate object.
            底板是否作为独立对象。
        add_loop: Whether to add a keychain loop.
            是否启用挂件环。
        loop_width: Keychain loop width in millimeters.
            环宽度 (mm)。
        loop_length: Keychain loop length in millimeters.
            环长度 (mm)。
        loop_hole: Keychain loop hole diameter in millimeters.
            环孔直径 (mm)。
        loop_pos: Keychain loop position as (x, y) coordinates.
            环位置 (x, y)。
        enable_relief: Whether to enable 2.5D relief mode.
            是否启用 2.5D 浮雕模式。
        color_height_map: Color-to-height mapping for relief mode.
            颜色高度映射 {hex: mm}。
        heightmap_max_height: Maximum relief height in millimeters.
            最大浮雕高度 (mm)。
        enable_outline: Whether to enable outline stroke.
            是否启用描边。
        outline_width: Outline stroke width in millimeters.
            描边宽度 (mm)。
        enable_cloisonne: Whether to enable cloisonne wire frame.
            是否启用掐丝珐琅。
        wire_width_mm: Cloisonne wire width in millimeters.
            金属丝宽度 (mm)。
        wire_height_mm: Cloisonne wire height in millimeters.
            金属丝高度 (mm)。
        enable_coating: Whether to enable transparent coating.
            是否启用涂层。
        coating_height_mm: Coating height in millimeters.
            涂层高度 (mm)。
        replacement_regions: List of color replacement records.
            颜色替换列表。
        free_color_set: Set of hex colors marked as free colors.
            自由色集合 (hex)。
    """

    lut_name: str = Field(..., description="LUT 名称")
    target_width_mm: float = Field(
        60.0, ge=10, le=400, description="目标宽度 (mm)"
    )
    spacer_thick: float = Field(
        1.2, ge=0.2, le=3.5, description="底板厚度 (mm)"
    )
    structure_mode: StructureMode = Field(
        StructureMode.DOUBLE_SIDED, description="打印结构模式"
    )
    auto_bg: bool = Field(False, description="自动去背景")
    bg_tol: int = Field(40, ge=0, le=150, description="背景容差")
    color_mode: ColorMode = Field(
        ColorMode.FOUR_COLOR, description="颜色模式"
    )
    modeling_mode: ModelingMode = Field(
        ModelingMode.HIGH_FIDELITY, description="建模模式"
    )
    quantize_colors: int = Field(48, ge=8, le=256, description="K-Means 色彩细节")
    enable_cleanup: bool = Field(True, description="孤立像素清理")
    hue_weight: float = Field(0.0, ge=0.0, le=1.0, description="色相保护权重 (0=纯色差, 0.5=推荐, 1.0=最强)")
    separate_backing: bool = Field(False, description="底板作为独立对象")
    add_loop: bool = Field(False, description="启用挂件环")
    loop_width: float = Field(
        4.0, ge=2, le=10, description="环宽度 (mm)"
    )
    loop_length: float = Field(
        8.0, ge=4, le=15, description="环长度 (mm)"
    )
    loop_hole: float = Field(
        2.5, ge=1, le=5, description="环孔直径 (mm)"
    )
    loop_pos: Optional[Tuple[float, float]] = Field(
        None, description="环位置 (x, y)"
    )
    enable_relief: bool = Field(False, description="启用 2.5D 浮雕模式")
    height_mode: Optional[str] = Field(
        "color",
        description="浮雕高度模式: 'color' (按颜色) 或 'heightmap' (按高度图)",
    )
    color_height_map: Optional[Dict[str, float]] = Field(
        None, description="颜色高度映射 {hex: mm}"
    )
    heightmap_max_height: float = Field(
        5.0, ge=0.08, le=15.0, description="最大浮雕高度 (mm)"
    )
    enable_outline: bool = Field(False, description="启用描边")
    outline_width: float = Field(
        2.0, ge=0.5, le=10.0, description="描边宽度 (mm)"
    )
    enable_cloisonne: bool = Field(False, description="启用掐丝珐琅")
    wire_width_mm: float = Field(
        0.4, ge=0.2, le=1.2, description="金属丝宽度 (mm)"
    )
    wire_height_mm: float = Field(
        0.4, ge=0.04, le=1.0, description="金属丝高度 (mm)"
    )
    enable_coating: bool = Field(False, description="启用涂层")
    coating_height_mm: float = Field(
        0.08, ge=0.04, le=0.12, description="涂层高度 (mm)"
    )
    replacement_regions: Optional[List[ColorReplacementItem]] = Field(
        None, description="颜色替换列表"
    )
    free_color_set: Optional[Set[str]] = Field(
        None, description="自由色集合 (hex)"
    )


class ConvertBatchRequest(BaseModel):
    """Request model for batch image conversion.
    批量图像转换的请求模型。

    Used by ``POST /api/convert/batch`` to process multiple images with
    shared conversion parameters.
    用于 ``POST /api/convert/batch``,使用共享参数批量处理多张图像。

    Attributes:
        params: Shared conversion parameters applied to all images.
            应用于所有图像的共享转换参数。
    """

    params: ConvertGenerateRequest = Field(..., description="共享参数")


class ColorReplaceRequest(BaseModel):
    """Request model for replacing a single color in the preview.
    替换预览中单个颜色的请求模型。

    Used by ``POST /api/convert/replace-color`` to swap one color in the
    current session's color-matched result.
    用于 ``POST /api/convert/replace-color``,在当前 session 的
    颜色匹配结果中替换一个颜色。

    Attributes:
        session_id: Active session identifier.
            Session ID。
        selected_color: Original image color to replace (hex).
            选中的原图颜色 (hex)。
        replacement_color: Target replacement color (hex).
            替换目标色 (hex)。
    """

    session_id: str = Field(..., description="Session ID")
    selected_color: str = Field(..., description="选中的原图颜色 (hex)")
    replacement_color: str = Field(..., description="替换目标色 (hex)")


class ColorMergePreviewRequest(BaseModel):
    """Request model for previewing color merge results.
    预览颜色合并结果的请求模型。

    Used by ``POST /api/convert/merge-colors`` to preview the effect of
    merging similar colors based on CIELAB distance thresholds.
    用于 ``POST /api/convert/merge-colors``,预览基于 CIELAB 色差阈值
    合并相似颜色的效果。

    Attributes:
        session_id: Active session identifier.
            Session ID。
        merge_enable: Whether color merging is enabled.
            是否启用颜色合并。
        merge_threshold: CIELAB color difference threshold for merging.
            CIELAB 色差阈值。
        merge_max_distance: Maximum pixel distance for merge candidates.
            最大合并距离 (px)。
    """

    session_id: str = Field(..., description="Session ID")
    merge_enable: bool = Field(True, description="启用颜色合并")
    merge_threshold: float = Field(
        0.5, ge=0.1, le=5.0, description="CIELAB 色差阈值"
    )
    merge_max_distance: int = Field(
        20, ge=5, le=50, description="最大合并距离 (px)"
    )


class BedSizeItem(BaseModel):
    """A single printer bed size option.
    单个打印热床尺寸选项。

    Attributes:
        label: Display label for the bed size, e.g. "256×256 mm".
            热床尺寸显示标签。
        width_mm: Bed width in millimeters.
            热床宽度 (mm)。
        height_mm: Bed height in millimeters.
            热床高度 (mm)。
        is_default: Whether this is the default bed size.
            是否为默认热床尺寸。
    """

    label: str = Field(..., description="热床尺寸标签")
    width_mm: int = Field(..., description="热床宽度 (mm)")
    height_mm: int = Field(..., description="热床高度 (mm)")
    is_default: bool = Field(False, description="是否为默认热床尺寸")


class BedSizeListResponse(BaseModel):
    """Response model for the bed size list endpoint.
    热床尺寸列表响应模型。

    Attributes:
        beds: List of all available bed size options.
            所有可用的热床尺寸选项列表。
    """

    beds: List[BedSizeItem] = Field(..., description="热床尺寸列表")


================================================
FILE: api/schemas/extractor.py
================================================
"""Extractor domain Pydantic schemas and enums.
Extractor 领域的 Pydantic 数据模型与枚举定义。

This module defines all request models for the color extraction API,
including calibration board scanning and manual LUT cell correction.
本模块定义颜色提取 API 的所有请求模型,
包括校准板扫描和手动 LUT 单元格校正。
"""

from __future__ import annotations

from enum import Enum
from typing import List, Tuple

from pydantic import BaseModel, Field


# ========== Enums ==========


class CalibrationColorMode(str, Enum):
    """Color mode for calibration and extraction.
    校准与提取的颜色模式。

    Attributes:
        BW: Black & White grayscale mode (32 levels).
            黑白灰度模式 (32 级)。
        FOUR_COLOR: 4-Color CMYW/RYBW mode (1024 colors).
            4 色 CMYW/RYBW 模式 (1024 色)。
        FIVE_COLOR_EXT: 5-Color Extended mode (1444 colors).
            5 色扩展模式 (1444 色)。
        SIX_COLOR: 6-Color extended smart mode (1296 colors).
            6 色扩展智能模式 (1296 色)。
        EIGHT_COLOR: 8-Color professional mode (2738 colors).
            8 色专业模式 (2738 色)。
    """

    BW = "BW (Black & White)"
    FOUR_COLOR = "4-Color"
    CMYW = "CMYW"
    RYBW = "RYBW"
    FIVE_COLOR_EXT = "5-Color Extended (1444)"
    SIX_COLOR = "6-Color (Smart 1296)"
    EIGHT_COLOR = "8-Color Max"


class ExtractorPage(str, Enum):
    """Page selector for 8-Color two-page calibration workflow.
    8 色双页校准流程的页码选择器。

    Attributes:
        PAGE_1: First calibration page.
            第一页。
        PAGE_2: Second calibration page.
            第二页。
    """

    PAGE_1 = "Page 1"
    PAGE_2 = "Page 2"


# ========== Models ==========


class ExtractorExtractRequest(BaseModel):
    """Request model for extracting colors from a photographed calibration board.
    从拍摄的校准板照片中提取颜色的请求模型。

    Used by ``POST /api/extractor/extract`` to perform perspective correction
    and color sampling on a calibration board image.
    用于 ``POST /api/extractor/extract``,对校准板照片执行透视校正和颜色采样。

    Attributes:
        color_mode: Calibration color system mode.
            校准颜色模式。
        corner_points: Four corner coordinates for perspective correction [(x, y), ...].
            4 个角点坐标 [(x, y), ...]。
        offset_x: Horizontal sampling offset in pixels.
            水平采样偏移 (px)。
        offset_y: Vertical sampling offset in pixels.
            垂直采样偏移 (px)。
        zoom: Perspective correction zoom factor.
            透视校正缩放。
        distortion: Lens distortion correction factor.
            畸变校正。
        white_balance: Whether to apply white balance correction.
            白平衡校正。
        vignette_correction: Whether to apply vignette correction.
            暗角校正。
        page: Page number for 8-Color two-page workflow.
            8-Color 页码。
    """

    color_mode: CalibrationColorMode = Field(
        CalibrationColorMode.FOUR_COLOR, description="校准颜色模式"
    )
    corner_points: List[Tuple[int, int]] = Field(
        ..., min_length=4, max_length=4, description="4 个角点坐标 [(x,y), ...]"
    )
    offset_x: int = Field(0, ge=-30, le=30, description="水平采样偏移 (px)")
    offset_y: int = Field(0, ge=-30, le=30, description="垂直采样偏移 (px)")
    zoom: float = Field(1.0, ge=0.8, le=1.2, description="透视校正缩放")
    distortion: float = Field(0.0, ge=-0.2, le=0.2, description="畸变校正")
    white_balance: bool = Field(False, description="白平衡校正")
    vignette_correction: bool = Field(False, description="暗角校正")
    page: ExtractorPage = Field(
        ExtractorPage.PAGE_1, description="8-Color 页码"
    )


class ExtractorManualFixRequest(BaseModel):
    """Request model for manually overriding a single LUT cell color.
    手动覆盖单个 LUT 单元格颜色的请求模型。

    Used by ``POST /api/extractor/manual-fix`` to correct an incorrectly
    extracted color value in the LUT.
    用于 ``POST /api/extractor/manual-fix``,校正 LUT 中提取错误的颜色值。

    Attributes:
        lut_path: File path to the LUT being edited.
            LUT 文件路径。
        cell_coord: Cell coordinates as (row, col) in the LUT grid.
            单元格坐标 (row, col)。
        override_color: Replacement color value in hex format.
            覆盖颜色 (hex)。
    """

    lut_path: str = Field("", description="LUT 文件路径 (可选,优先使用 session_id 查找)")
    session_id: str = Field("", description="Session ID (用于查找 LUT 路径)")
    cell_coord: Tuple[int, int] = Field(..., description="单元格坐标 (row, col)")
    override_color: str = Field(..., description="覆盖颜色 (hex)")


================================================
FILE: api/schemas/five_color.py
================================================
"""Five-Color Query — Pydantic 数据模型。
五色组合查询的请求与响应模型定义。
"""

from typing import Optional

from pydantic import BaseModel, Field


class BaseColorEntry(BaseModel):
    """单个基础颜色条目。"""

    index: int = Field(..., description="颜色索引 (0-based)")
    rgb: tuple[int, int, int] = Field(..., description="RGB 值")
    name: str = Field(..., description="颜色名称")
    hex: str = Field(..., description="Hex 颜色代码")


class BaseColorsResponse(BaseModel):
    """基础颜色列表响应。"""

    lut_name: str = Field(..., description="LUT 显示名称")
    color_count: int = Field(..., description="基础颜色数量")
    colors: list[BaseColorEntry] = Field(..., description="基础颜色列表")


class FiveColorQueryRequest(BaseModel):
    """五色组合查询请求。"""

    lut_name: str = Field(..., description="LUT 显示名称")
    selected_indices: list[int] = Field(
        ..., min_length=5, max_length=5, description="5 个颜色索引"
    )


class FiveColorQueryResponse(BaseModel):
    """五色组合查询响应。"""

    found: bool = Field(..., description="是否找到匹配")
    selected_indices: list[int] = Field(..., description="用户选择的索引")
    result_rgb: Optional[tuple[int, int, int]] = Field(None, description="结果 RGB")
    result_hex: Optional[str] = Field(None, description="结果 Hex")
    row_index: int = Field(..., description="Stack LUT 行索引")
    message: str = Field(..., description="状态消息")


================================================
FILE: api/schemas/lut.py
================================================
"""LUT Manager domain Pydantic schemas.
LUT 管理领域的 Pydantic 数据模型。

This module defines request and response models for the LUT merge API,
including merge parameters, merge statistics, and LUT info queries.
本模块定义 LUT 合并 API 的请求和响应模型,
包括合并参数、合并统计信息和 LUT 信息查询。
"""

from __future__ import annotations

from pydantic import BaseModel, Field


# ========== Models ==========


class MergeStats(BaseModel):
    """Statistics from a LUT merge operation.
    LUT 合并操作的统计信息。

    Attributes:
        total_before: Total color count across all input LUTs before merging.
            合并前所有输入 LUT 的总颜色数。
        total_after: Color count in the merged result after deduplication.
            去重后合并结果的颜色数。
        exact_dupes: Number of exact duplicate colors removed.
            精确去重移除的颜色数。
        similar_removed: Number of perceptually similar colors removed by Delta-E threshold.
            通过 Delta-E 阈值移除的相近颜色数。
    """

    total_before: int = Field(..., description="合并前总颜色数")
    total_after: int = Field(..., description="合并后颜色数")
    exact_dupes: int = Field(..., description="精确去重数")
    similar_removed: int = Field(..., description="相近色去除数")


class MergeRequest(BaseModel):
    """Request model for merging multiple LUTs.
    合并多个 LUT 的请求模型。

    Used by ``POST /api/lut/merge`` to execute a LUT merge operation
    with a primary LUT, one or more secondary LUTs, and a deduplication
    threshold.
    用于 ``POST /api/lut/merge``,执行 LUT 合并操作,
    包含主 LUT、一个或多个辅助 LUT 和去重阈值。

    Attributes:
        primary_name: Display name of the primary LUT.
            主 LUT 显示名称。
        secondary_names: Display names of secondary LUTs to merge.
            辅助 LUT 显示名称列表。
        dedup_threshold: Delta-E threshold for removing perceptually similar colors.
            Delta-E 去重阈值(0 = 仅精确去重,值越大去除越多相近色)。
    """

    primary_name: str = Field(..., description="主 LUT 显示名称")
    secondary_names: list[str] = Field(..., description="辅助 LUT 显示名称列表")
    dedup_threshold: float = Field(
        3.0, ge=0.0, le=20.0, description="Delta-E 去重阈值"
    )


class MergeResponse(BaseModel):
    """Response model for a completed LUT merge operation.
    LUT 合并操作完成后的响应模型。

    Returned by ``POST /api/lut/merge`` on success, containing the
    output filename and detailed merge statistics.
    由 ``POST /api/lut/merge`` 成功时返回,包含输出文件名和详细合并统计。

    Attributes:
        status: Operation result status (e.g. ``"success"``).
            操作状态。
        message: Human-readable result message.
            结果描述信息。
        filename: Display name of the newly created merged LUT file.
            新创建的合并 LUT 文件显示名称。
        stats: Detailed merge statistics.
            合并统计信息。
    """

    status: str = Field(..., description="操作状态")
    message: str = Field(..., description="结果描述信息")
    filename: str = Field(..., description="合并 LUT 文件显示名称")
    stats: MergeStats = Field(..., description="合并统计信息")


class LutInfoResponse(BaseModel):
    """Response model for LUT information queries.
    LUT 信息查询的响应模型。

    Returned by ``GET /api/lut/{lut_name}/info`` with the detected
    color mode and color count for the specified LUT.
    由 ``GET /api/lut/{lut_name}/info`` 返回,包含指定 LUT 的
    检测到的颜色模式和颜色数量。

    Attributes:
        name: Display name of the LUT.
            LUT 显示名称。
        color_mode: Detected color mode (e.g. ``"8-Color Max"``, ``"BW"``).
            检测到的颜色模式。
        color_count: Number of colors in the LUT.
            LUT 中的颜色数量。
    """

    name: str = Field(..., description="LUT 显示名称")
    color_mode: str = Field(..., description="颜色模式")
    color_count: int = Field(..., ge=0, description="颜色数量")


================================================
FILE: api/schemas/responses.py
================================================
"""Lumina Studio API — Response Pydantic models.
Lumina Studio API — 响应 Pydantic 数据模型。

All API endpoint response schemas are defined here.
所有 API 端点的响应 Schema 均在此定义。
"""

from typing import Optional

from pydantic import BaseModel


class CalibrationResponse(BaseModel):
    """校准板生成响应。"""

    status: str
    message: str
    download_url: str
    preview_url: Optional[str] = None


class PreviewResponse(BaseModel):
    """预览生成响应。"""

    session_id: str
    status: str
    message: str
    preview_url: str
    preview_glb_url: Optional[str] = None
    palette: list[dict]
    dimensions: dict
    contours: Optional[dict[str, list[list[list[float]]]]] = None


class ColorReplaceResponse(BaseModel):
    """颜色替换响应。"""

    status: str
    message: str
    preview_url: str
    replacement_count: int


class MergePreviewResponse(BaseModel):
    """颜色合并预览响应。"""

    status: str
    message: str
    preview_url: str
    merge_map: dict[str, str]
    quality_metric: float
    colors_before: int
    colors_after: int


class GenerateResponse(BaseModel):
    """3MF 生成响应。"""

    status: str
    message: str
    download_url: str
    preview_3d_url: Optional[str] = None
    threemf_disk_path: Optional[str] = None


class BatchItemResult(BaseModel):
    """批量转换单项结果。"""

    filen
Download .txt
gitextract_1imuoy6m/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── CHANGELOG.md
├── CHANGELOG_CN.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_CN.md
├── api/
│   ├── __init__.py
│   ├── app.py
│   ├── dependencies.py
│   ├── file_bridge.py
│   ├── file_registry.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── calibration.py
│   │   ├── converter.py
│   │   ├── extractor.py
│   │   ├── five_color.py
│   │   ├── health.py
│   │   ├── lut.py
│   │   ├── slicer.py
│   │   └── system.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── calibration.py
│   │   ├── converter.py
│   │   ├── extractor.py
│   │   ├── five_color.py
│   │   ├── lut.py
│   │   ├── responses.py
│   │   ├── slicer.py
│   │   └── system.py
│   ├── session_store.py
│   ├── worker_pool.py
│   └── workers/
│       ├── __init__.py
│       └── converter_workers.py
├── api_server.py
├── assets/
│   └── smart_8color_stacks.npy
├── bambu_config_template.json
├── benchmark.py
├── config.py
├── core/
│   ├── __init__.py
│   ├── calibration.py
│   ├── color_analyzer.py
│   ├── color_matching_hue_aware.py
│   ├── color_merger.py
│   ├── color_replacement.py
│   ├── converter.py
│   ├── extractor.py
│   ├── five_color_combination.py
│   ├── geometry_utils.py
│   ├── heightmap_loader.py
│   ├── i18n.py
│   ├── image_preprocessor.py
│   ├── image_processing.py
│   ├── isolated_pixel_cleanup.py
│   ├── lut_merger.py
│   ├── mesh_generators.py
│   ├── naming.py
│   ├── slicer.py
│   ├── tray.py
│   └── vector_engine.py
├── docs/
│   └── api_mapping_blueprint.md
├── frontend/
│   ├── README.md
│   ├── eslint.config.js
│   ├── index.html
│   ├── package.json
│   ├── postcss.config.js
│   ├── public/
│   │   ├── hdr/
│   │   │   └── studio_small_09_1k.hdr
│   │   └── test.glb
│   ├── scripts/
│   │   └── generate-test-glb.mjs
│   ├── src/
│   │   ├── App.css
│   │   ├── App.tsx
│   │   ├── __tests__/
│   │   │   ├── App.test.tsx
│   │   │   ├── InteractiveModelViewer.test.ts
│   │   │   ├── KeychainRing3D.test.tsx
│   │   │   ├── LutColorGrid.test.ts
│   │   │   ├── Scene3D.test.tsx
│   │   │   ├── action-bar-always-visible.property.test.ts
│   │   │   ├── action-bar-always-visible.test.ts
│   │   │   ├── api-client.property.test.ts
│   │   │   ├── app-tabs.test.tsx
│   │   │   ├── autoPreview.test.ts
│   │   │   ├── bed-size-selector.property.test.ts
│   │   │   ├── calibration-hooks.property.test.ts
│   │   │   ├── calibration-panel.test.tsx
│   │   │   ├── calibration-store.property.test.ts
│   │   │   ├── colorRemap.property.test.ts
│   │   │   ├── colorSelect.property.test.ts
│   │   │   ├── converter-api.test.ts
│   │   │   ├── converter-store.property.test.ts
│   │   │   ├── extractor-5color.property.test.ts
│   │   │   ├── extractor-api.property.test.ts
│   │   │   ├── extractor-canvas.test.tsx
│   │   │   ├── extractor-panel.test.tsx
│   │   │   ├── extractor-store.property.test.ts
│   │   │   ├── five-color-store.property.test.ts
│   │   │   ├── health-status.property.test.tsx
│   │   │   ├── keychainRing.property.test.ts
│   │   │   ├── layout.test.tsx
│   │   │   ├── loading-spinner.test.tsx
│   │   │   ├── lut-manager-panel.test.tsx
│   │   │   ├── lut-manager-refresh.property.test.ts
│   │   │   ├── lut-manager-store.property.test.ts
│   │   │   ├── lutColorFilter.property.test.ts
│   │   │   ├── model-centering.property.test.ts
│   │   │   ├── paletteLutMerge.property.test.ts
│   │   │   ├── paletteLutMerge.test.tsx
│   │   │   ├── paletteOptimization.property.test.ts
│   │   │   ├── paletteOptimization.test.tsx
│   │   │   ├── realtimePreview.property.test.ts
│   │   │   ├── realtimePreview.test.ts
│   │   │   ├── reliefHeight.property.test.ts
│   │   │   ├── replace-preview.property.test.ts
│   │   │   ├── scaleUtils.property.test.ts
│   │   │   ├── settings-store.property.test.ts
│   │   │   ├── slicer.property.test.ts
│   │   │   ├── stack-positions-nonoverlap.property.test.ts
│   │   │   ├── tab-filter.property.test.ts
│   │   │   ├── tab-switch-layout.property.test.ts
│   │   │   ├── theme.property.test.ts
│   │   │   ├── theme.test.tsx
│   │   │   ├── ui-components.test.tsx
│   │   │   ├── widget-drag-perf.property.test.ts
│   │   │   ├── widget-registry-i18n.property.test.ts
│   │   │   ├── widget-workspace.property.test.ts
│   │   │   ├── widget-workspace.test.tsx
│   │   │   └── zoomable-image.property.test.ts
│   │   ├── api/
│   │   │   ├── __tests__/
│   │   │   │   └── batchApi.property.test.ts
│   │   │   ├── calibration.ts
│   │   │   ├── client.ts
│   │   │   ├── converter.ts
│   │   │   ├── extractor.ts
│   │   │   ├── fiveColor.ts
│   │   │   ├── lut.ts
│   │   │   ├── slicer.ts
│   │   │   ├── system.ts
│   │   │   └── types.ts
│   │   ├── components/
│   │   │   ├── AboutView.tsx
│   │   │   ├── BedPlatform.tsx
│   │   │   ├── CalibrationPanel.tsx
│   │   │   ├── ExtractorCanvas.tsx
│   │   │   ├── ExtractorPanel.tsx
│   │   │   ├── FiveColorCanvas.tsx
│   │   │   ├── FiveColorQueryPanel.tsx
│   │   │   ├── InteractiveModelViewer.tsx
│   │   │   ├── KeychainRing3D.tsx
│   │   │   ├── LanguageToggle.tsx
│   │   │   ├── LoadingSpinner.tsx
│   │   │   ├── LutManagerPanel.tsx
│   │   │   ├── ModelViewer.tsx
│   │   │   ├── Scene3D.tsx
│   │   │   ├── ThemeToggle.tsx
│   │   │   ├── __tests__/
│   │   │   │   ├── ActionBar.batch.test.tsx
│   │   │   │   ├── BasicSettings.batch.test.tsx
│   │   │   │   ├── BatchFileUploader.test.tsx
│   │   │   │   ├── BatchResultSummary.property.test.tsx
│   │   │   │   └── BatchResultSummary.test.tsx
│   │   │   ├── lightingConfig.ts
│   │   │   ├── sections/
│   │   │   │   ├── ActionBar.tsx
│   │   │   │   ├── AdvancedSettings.tsx
│   │   │   │   ├── BasicSettings.tsx
│   │   │   │   ├── BedSizeSelector.tsx
│   │   │   │   ├── CloisonneSettings.tsx
│   │   │   │   ├── CoatingSettings.tsx
│   │   │   │   ├── KeychainLoopSettings.tsx
│   │   │   │   ├── LutColorGrid.tsx
│   │   │   │   ├── OutlineSettings.tsx
│   │   │   │   ├── PalettePanel.tsx
│   │   │   │   ├── ReliefSettings.tsx
│   │   │   │   └── SlicerSelector.tsx
│   │   │   ├── themeConfig.ts
│   │   │   ├── ui/
│   │   │   │   ├── Accordion.tsx
│   │   │   │   ├── BatchFileUploader.tsx
│   │   │   │   ├── BatchResultSummary.tsx
│   │   │   │   ├── Button.tsx
│   │   │   │   ├── Checkbox.tsx
│   │   │   │   ├── ColorModeBadge.tsx
│   │   │   │   ├── CropModal.tsx
│   │   │   │   ├── Dropdown.tsx
│   │   │   │   ├── FullScreenModal.tsx
│   │   │   │   ├── ImageUpload.tsx
│   │   │   │   ├── RadioGroup.tsx
│   │   │   │   ├── Slider.tsx
│   │   │   │   └── ZoomableImage.tsx
│   │   │   └── widget/
│   │   │       ├── ActionBarWidgetContent.tsx
│   │   │       ├── AdvancedSettingsWidgetContent.tsx
│   │   │       ├── BasicSettingsWidgetContent.tsx
│   │   │       ├── CalibrationWidgetContent.tsx
│   │   │       ├── CloisonneSettingsWidgetContent.tsx
│   │   │       ├── CoatingSettingsWidgetContent.tsx
│   │   │       ├── ColorWorkstation.tsx
│   │   │       ├── ExtractorWidgetContent.tsx
│   │   │       ├── FiveColorWidgetContent.tsx
│   │   │       ├── KeychainLoopWidgetContent.tsx
│   │   │       ├── LutManagerWidgetContent.tsx
│   │   │       ├── OutlineSettingsWidgetContent.tsx
│   │   │       ├── ReliefSettingsWidgetContent.tsx
│   │   │       ├── SnapGuides.tsx
│   │   │       ├── TabNavBar.tsx
│   │   │       ├── WidgetHeader.tsx
│   │   │       ├── WidgetPanel.tsx
│   │   │       └── WidgetWorkspace.tsx
│   │   ├── hooks/
│   │   │   ├── useActiveModelUrl.ts
│   │   │   ├── useAutoPreview.ts
│   │   │   ├── useConverterDataInit.ts
│   │   │   └── useThemeConfig.ts
│   │   ├── i18n/
│   │   │   ├── context.tsx
│   │   │   └── translations.ts
│   │   ├── index.css
│   │   ├── main.tsx
│   │   ├── setupTests.ts
│   │   ├── stores/
│   │   │   ├── __tests__/
│   │   │   │   ├── batchStore.property.test.ts
│   │   │   │   ├── batchStore.test.ts
│   │   │   │   ├── converterStore.property.test.ts
│   │   │   │   ├── converterStore.test.ts
│   │   │   │   ├── cropStore.property.test.ts
│   │   │   │   ├── cropStore.test.ts
│   │   │   │   ├── slicerStore.property.test.ts
│   │   │   │   └── slicerStore.test.ts
│   │   │   ├── aboutStore.ts
│   │   │   ├── calibrationStore.ts
│   │   │   ├── converterStore.ts
│   │   │   ├── extractorStore.ts
│   │   │   ├── fiveColorStore.ts
│   │   │   ├── lutManagerStore.ts
│   │   │   ├── settingsStore.ts
│   │   │   ├── slicerStore.ts
│   │   │   └── widgetStore.ts
│   │   ├── types/
│   │   │   └── widget.ts
│   │   └── utils/
│   │       ├── __tests__/
│   │       │   ├── colorUtils.property.test.ts
│   │       │   └── colorUtils.test.ts
│   │       ├── colorUtils.ts
│   │       ├── scaleUtils.ts
│   │       └── widgetUtils.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── icon.icns
├── lumina_studio.spec
├── lut-npy预设/
│   ├── Aliz/
│   │   ├── PETG/
│   │   │   ├── Aliz&4色PETG&CMYW.npy
│   │   │   ├── Aliz&4色PETG&RYBW.npy
│   │   │   ├── Aliz&6色PETG&CMYWGK.npy
│   │   │   ├── Aliz&6色PETG&RYBWGK.npy
│   │   │   ├── Aliz&8色PETG.npy
│   │   │   └── Aliz&PETG&5color&红-黄-蓝-白-黑-20260304.npy
│   │   ├── PLA/
│   │   │   ├── Aliz&4色PLA&CMYW.npy
│   │   │   ├── Aliz&4色PLA&RYBW.npy
│   │   │   ├── Aliz&6色PLA&CMYWGK.npy
│   │   │   ├── Aliz&6色PLA&RYBWGK.npy
│   │   │   └── Aliz&8色PLA.npy
│   │   └── 使用须知&开发组精校版20260402.txt
│   ├── BIQU/
│   │   ├── 必趣&PLAGO&2色BW.npy
│   │   ├── 必趣&PLAGO&4色RYBW.npy
│   │   ├── 必趣&PLAGO&6色RYBWGK.npy
│   │   └── 必趣颜色说明.txt
│   ├── CooBeen/
│   │   └── RYBW-CooBeen PETG.npy
│   ├── Creality/
│   │   └── RYBW-Creality Hyper PLA.npy
│   ├── Custom/
│   │   ├── Bambulab&PLA&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   ├── Bambulab&PLA&BW&白-黑.npy
│   │   ├── Bambulab&PLA&CMYW&青-品红-黄-绿-白-黑.npy
│   │   ├── Bambulab&PLA&RYBW&红-黄-蓝-白.npy
│   │   ├── Merged_8-Color+4-Color+6-Color+4-Color+6-Color+BW_20260307_091510.npz
│   │   ├── Merged_8-Color+6-Color+BW+4-Color_20260227_222117.npz
│   │   └── Merged_8-Color+BW+6-Color+4-Color_20260305_213304.npz
│   ├── Elegoo/
│   │   └── Elegoo_pla_RYBW.npy
│   ├── Jayo/
│   │   ├── Jayo&OW_PLA+_RYBW.npy
│   │   └── Readme.md
│   ├── Karvax/
│   │   ├── Karvax&PLA&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   ├── Karvax&PLA&RYBW&红-蓝-黄-白.npy
│   │   └── Karvax&PLA&RYBW&红-蓝-黄-绿-白-黑.npy
│   ├── LUT命名规范 LUT Naming Convention.md
│   ├── R3D/
│   │   └── R3DPLA青春版RYBW-PLA哑光中国红-PLA哑光黄-PLA哑光宝石蓝-PLA哑光白.npy
│   ├── Sanci/
│   │   └── PETG/
│   │       ├── 4色/
│   │       │   ├── Sanci&PETG&CMYW&粉红-黄-蓝-白.npy
│   │       │   └── Sanci&PETG&RYBW&红-黄-蓝-白.npy
│   │       ├── 6色/
│   │       │   └── Sanci&PETG&RYBW&红-黄-蓝-白-绿-黑.npy
│   │       └── 7色/
│   │           └── Sanci&PETG&Merged&红-黄-蓝-粉-绿-黑-白&20260402_215742.npz
│   ├── Snapmaker/
│   │   ├── 使用说明&开发组精校版&20260405.txt
│   │   ├── 快造&2色PLA&BW.npy
│   │   ├── 快造&4色PLA&B(蓝)MYW.npy
│   │   └── 快造&4色PLA&RYBW.npy
│   ├── XYD小明/
│   │   ├── PETG/
│   │   │   ├── XYD小明&PETG&8色&红-品红-青-蓝-黄-白-绿-黑.npy
│   │   │   ├── XYD小明&PETG&CMYW&青-品红-黄-白.npy
│   │   │   ├── XYD小明&PETG&CMYW&青-品红-黄-绿-白-黑.npy
│   │   │   ├── XYD小明&PETG&RYBW&红-蓝-黄-白.npy
│   │   │   └── XYD小明&PETG&RYBW&红-蓝-黄-绿-白-黑.npy
│   │   └── PLA/
│   │       ├── XYD小明&PLA&8色&红-品红-青-克莱因蓝-黄-白-绿-黑-20260327.npy
│   │       ├── XYD小明&PLA&CMYW&品红-青-黄-绿-白-黑-20260327.npy
│   │       ├── XYD小明&PLA&CMYW&青-品红-黄-白-20260327.npy
│   │       ├── XYD小明&PLA&RYBW&红-蓝-黄-绿-白-黑-20260331.npz
│   │       └── XYD小明&PLA&RYBW&红-黄-蓝-白-20260327.npy
│   ├── bambulab/
│   │   ├── Bambulab&PLA&4色&RYBW&红-蓝-黄-白.npy
│   │   ├── Bambulab&PLA&8色+4色.npz
│   │   ├── Bambulab&PLA&8色-New.npy
│   │   ├── README.txt
│   │   ├── bambulab_pla_basic_cmyw.npy
│   │   ├── bambulab_pla_basic_cmyw_new.npy
│   │   └── bambulab_pla_basic_rybw.npy
│   ├── npy预设说明 npy explanation.txt
│   ├── 天瑞/
│   │   └── 天瑞-PETG-ECO-NGYX-RYBW.npy
│   ├── 必应/
│   │   ├── 必应PETGHF.npy
│   │   ├── 必应plaf.npy
│   │   └── 必应使用说明.txt
│   ├── 瑞贝思/
│   │   ├── 2色/
│   │   │   └── 瑞贝思&PLA&BW2色&20260328.npy
│   │   ├── 4色/
│   │   │   └── 瑞贝思&PLA&RYBW4色&20260328.npy
│   │   └── 6色/
│   │       └── 瑞贝思&PLA&RYBW&红-蓝-黄-绿-白-黑&20260331.npz
│   ├── 精亮/
│   │   ├── 精亮&PLA4色&RYBW.npy
│   │   ├── 精亮&PLA6色&黑 白 品红 青 黄 绿.npy
│   │   ├── 精亮&PLA6色&黑 白 黄 绿 红 蓝.npy
│   │   └── 精亮&PLA8色.npy
│   ├── 纵维立方/
│   │   ├── 纵维立方&4色CMYW.npy
│   │   ├── 纵维立方&4色RYBW.npy
│   │   ├── 纵维立方&6色CMYWGK.npy
│   │   ├── 纵维立方&6色RYBWGK.npy
│   │   ├── 纵维立方&7色(青色蓝色共用蓝色).npy
│   │   ├── 纵维立方&8色.npy
│   │   ├── 纵维立方&PLA2色&BW.npy
│   │   └── 纵维立方颜色说明开发组预设.txt
│   ├── 赛纳/
│   │   ├── 使用说明.txt
│   │   ├── 赛纳&2色PLA&BW-白-黑.npy
│   │   ├── 赛纳&4色PETG&CMYW-品红-青-黄-白.npy
│   │   ├── 赛纳&4色PLA&CMYW-品红-青-黄-白.npy
│   │   ├── 赛纳&4色PLA&RYBW-红-黄-蓝-白.npy
│   │   ├── 赛纳&6色PLA&CMYWGK-品红-青-黄-白-绿-黑.npy
│   │   ├── 赛纳&6色PLA&RYBWGK-红-黄-蓝-白-绿-黑.npy
│   │   └── 赛纳&8色PLA-红-黄-蓝-品红-青-白-绿-黑.npy
│   ├── 通用LUT[有色差]RYBW General for personal use.npy
│   └── 魔创/
│       ├── 魔创&PETG2色.npy
│       ├── 魔创&PETG4色&CMYW.npy
│       ├── 魔创&PETG4色&RYBW.npy
│       ├── 魔创&PETG6色&CMYWGK.npy
│       ├── 魔创&PETG6色&RYBWGK.npy
│       ├── 魔创&PETG8色.npy
│       └── 魔创使用说明.txt
├── main.py
├── requirements.txt
├── tests/
│   ├── test_5color_merge_properties.py
│   ├── test_5color_merge_unit.py
│   ├── test_api_app_unit.py
│   ├── test_api_routers_unit.py
│   ├── test_api_schemas_properties.py
│   ├── test_bed_size_properties.py
│   ├── test_calibration_integration_unit.py
│   ├── test_calibration_routing_properties.py
│   ├── test_cleanup_output_dir_properties.py
│   ├── test_clear_cache_response_properties.py
│   ├── test_color_merge_map_properties.py
│   ├── test_color_merge_unit.py
│   ├── test_color_replace_unit.py
│   ├── test_converter_batch_unit.py
│   ├── test_converter_generate_unit.py
│   ├── test_converter_preview_unit.py
│   ├── test_converter_vector_export_unit.py
│   ├── test_converter_workers_unit.py
│   ├── test_crop_properties.py
│   ├── test_crop_unit.py
│   ├── test_extractor_integration_unit.py
│   ├── test_file_bridge_properties.py
│   ├── test_file_registry_clear_properties.py
│   ├── test_file_registry_properties.py
│   ├── test_five_color_api_unit.py
│   ├── test_five_color_query_properties.py
│   ├── test_health_lut_unit.py
│   ├── test_heic_support_properties.py
│   ├── test_heightmap_color_height_properties.py
│   ├── test_heightmap_properties.py
│   ├── test_heightmap_unit.py
│   ├── test_heightmap_upload_unit.py
│   ├── test_layout_new_tabs_unit.py
│   ├── test_lut_api_properties.py
│   ├── test_lut_api_unit.py
│   ├── test_lut_list_properties.py
│   ├── test_lut_merger_properties.py
│   ├── test_mime_type_properties.py
│   ├── test_naming_properties.py
│   ├── test_naming_unit.py
│   ├── test_palette_connected_selection_unit.py
│   ├── test_preview_click_selection_unit.py
│   ├── test_relief_mode_fix_properties.py
│   ├── test_relief_mode_fix_unit.py
│   ├── test_segmented_glb_properties.py
│   ├── test_segmented_glb_unit.py
│   ├── test_session_store_clear_properties.py
│   ├── test_session_store_properties.py
│   ├── test_session_thread_safety_properties.py
│   ├── test_session_ttl_properties.py
│   ├── test_settings_api_properties.py
│   ├── test_settings_api_unit.py
│   ├── test_slicer_properties.py
│   ├── test_slicer_unit.py
│   ├── test_stack_order_properties.py
│   ├── test_user_replacement_list_ui_unit.py
│   ├── test_vector_engine_unit.py
│   ├── test_worker_pool_properties.py
│   ├── test_worker_pool_unit.py
│   └── verify_merge_remap.py
├── ui/
│   ├── __init__.py
│   ├── callbacks.py
│   ├── crop_extension.py
│   ├── fivecolor_tab_v2.py
│   ├── layout_new.py
│   ├── palette_extension.py
│   └── styles.py
└── utils/
    ├── __init__.py
    ├── bambu_3mf_writer.py
    ├── color_recipe_logger.py
    ├── helpers.py
    ├── lut_manager.py
    └── stats.py
Download .txt
SYMBOL INDEX (1617 symbols across 243 files)

FILE: api/app.py
  function lifespan (line 41) | async def lifespan(app: FastAPI) -> AsyncIterator[None]:
  function create_app (line 79) | def create_app() -> FastAPI:

FILE: api/dependencies.py
  function get_session_store (line 24) | def get_session_store() -> SessionStore:
  function get_file_registry (line 29) | def get_file_registry() -> FileRegistry:
  function get_worker_pool (line 34) | def get_worker_pool() -> WorkerPoolManager:

FILE: api/file_bridge.py
  function upload_to_ndarray (line 12) | async def upload_to_ndarray(file: UploadFile) -> np.ndarray:
  function upload_to_tempfile (line 34) | async def upload_to_tempfile(
  function pil_to_png_bytes (line 57) | def pil_to_png_bytes(img: Image.Image) -> bytes:
  function ndarray_to_png_bytes (line 64) | def ndarray_to_png_bytes(arr: np.ndarray) -> bytes:
  function pil_to_streaming_response (line 70) | def pil_to_streaming_response(img: Image.Image, fmt: str = "PNG") -> Str...
  function file_to_response (line 79) | def file_to_response(path: str, filename: Optional[str] = None) -> FileR...
  function _guess_media_type (line 87) | def _guess_media_type(path: str) -> str:

FILE: api/file_registry.py
  class FileRegistry (line 7) | class FileRegistry:
    method __init__ (line 15) | def __init__(self) -> None:
    method register_path (line 19) | def register_path(self, session_id: str, path: str,
    method register_bytes (line 33) | def register_bytes(self, session_id: str, data: bytes,
    method resolve (line 45) | def resolve(self, file_id: str) -> Optional[Tuple[str, str]]:
    method cleanup_session (line 56) | def cleanup_session(self, session_id: str) -> int:
    method clear_all (line 67) | def clear_all(self) -> int:

FILE: api/routers/calibration.py
  function _handle_core_error (line 25) | def _handle_core_error(e: Exception, context: str) -> None:
  function calibration_generate (line 32) | def calibration_generate(

FILE: api/routers/converter.py
  function _handle_core_error (line 62) | def _handle_core_error(e: Exception, context: str) -> None:
  function _require_session (line 68) | def _require_session(store: SessionStore, session_id: str) -> dict:
  function _require_preview_cache (line 76) | def _require_preview_cache(session_data: dict) -> dict:
  function _image_to_png_bytes (line 86) | def _image_to_png_bytes(img: object) -> bytes:
  function get_bed_sizes (line 96) | def get_bed_sizes() -> BedSizeListResponse:
  function get_bed_preview (line 113) | def get_bed_preview(
  function crop_image (line 140) | async def crop_image(
  function convert_preview (line 192) | async def convert_preview(
  function upload_heightmap (line 349) | async def upload_heightmap(
  class _GenerateBody (line 458) | class _GenerateBody(BaseModel):
  function convert_generate (line 466) | async def convert_generate(
  function convert_batch (line 606) | async def convert_batch(
  function replace_color (line 724) | def replace_color(
  function _rgb_to_lab (line 810) | def _rgb_to_lab(rgb_array: np.ndarray) -> np.ndarray:
  function merge_colors (line 826) | def merge_colors(

FILE: api/routers/extractor.py
  function _handle_core_error (line 26) | def _handle_core_error(e: Exception, context: str) -> None:
  function _image_to_png_bytes (line 32) | def _image_to_png_bytes(img: object) -> bytes:
  function extractor_extract (line 42) | async def extractor_extract(
  function extractor_manual_fix (line 165) | def extractor_manual_fix(
  function extractor_merge_5color_extended (line 205) | def extractor_merge_5color_extended(
  function extractor_merge_8color (line 255) | def extractor_merge_8color(

FILE: api/routers/five_color.py
  function _load_engine (line 29) | def _load_engine(lut_name: str) -> tuple[ColorQueryEngine, str]:
  function get_base_colors (line 89) | def get_base_colors(lut_name: str = Query(..., description="LUT 显示名称")) ...
  function query_five_color (line 115) | def query_five_color(request: FiveColorQueryRequest) -> FiveColorQueryRe...

FILE: api/routers/health.py
  function health_check (line 23) | def health_check(

FILE: api/routers/lut.py
  function list_luts (line 29) | def list_luts() -> LUTListResponse:
  function merge_luts_endpoint (line 46) | def merge_luts_endpoint(request: MergeRequest) -> MergeResponse:
  function get_lut_colors (line 151) | def get_lut_colors(lut_name: str) -> LutColorsResponse:
  function get_lut_info (line 169) | def get_lut_info(lut_name: str) -> LutInfoResponse:

FILE: api/routers/slicer.py
  function detect_slicers (line 24) | def detect_slicers() -> SlicerDetectResponse:
  function launch_slicer_endpoint (line 40) | def launch_slicer_endpoint(request: SlicerLaunchRequest) -> SlicerLaunch...

FILE: api/routers/system.py
  function cleanup_output_dir (line 35) | def cleanup_output_dir(output_dir: str) -> tuple[int, int]:
  function perform_cache_cleanup (line 68) | def perform_cache_cleanup(
  function clear_cache (line 96) | def clear_cache(
  function get_settings (line 133) | def get_settings() -> UserSettingsResponse:
  function save_settings (line 145) | def save_settings(settings: UserSettings) -> SaveSettingsResponse:
  function get_stats (line 160) | def get_stats() -> StatsResponse:

FILE: api/schemas/calibration.py
  class BackingColor (line 22) | class BackingColor(str, Enum):
  class CalibrationGenerateRequest (line 52) | class CalibrationGenerateRequest(BaseModel):

FILE: api/schemas/converter.py
  class ColorMode (line 21) | class ColorMode(str, Enum):
  class ModelingMode (line 47) | class ModelingMode(str, Enum):
  class StructureMode (line 65) | class StructureMode(str, Enum):
  class AutoHeightMode (line 80) | class AutoHeightMode(str, Enum):
  class ColorReplacementItem (line 101) | class ColorReplacementItem(BaseModel):
  class ConvertPreviewRequest (line 123) | class ConvertPreviewRequest(BaseModel):
  class ConvertGenerateRequest (line 168) | class ConvertGenerateRequest(BaseModel):
  class ConvertBatchRequest (line 306) | class ConvertBatchRequest(BaseModel):
  class ColorReplaceRequest (line 322) | class ColorReplaceRequest(BaseModel):
  class ColorMergePreviewRequest (line 345) | class ColorMergePreviewRequest(BaseModel):
  class BedSizeItem (line 375) | class BedSizeItem(BaseModel):
  class BedSizeListResponse (line 396) | class BedSizeListResponse(BaseModel):

FILE: api/schemas/extractor.py
  class CalibrationColorMode (line 21) | class CalibrationColorMode(str, Enum):
  class ExtractorPage (line 47) | class ExtractorPage(str, Enum):
  class ExtractorExtractRequest (line 65) | class ExtractorExtractRequest(BaseModel):
  class ExtractorManualFixRequest (line 111) | class ExtractorManualFixRequest(BaseModel):

FILE: api/schemas/five_color.py
  class BaseColorEntry (line 10) | class BaseColorEntry(BaseModel):
  class BaseColorsResponse (line 19) | class BaseColorsResponse(BaseModel):
  class FiveColorQueryRequest (line 27) | class FiveColorQueryRequest(BaseModel):
  class FiveColorQueryResponse (line 36) | class FiveColorQueryResponse(BaseModel):

FILE: api/schemas/lut.py
  class MergeStats (line 18) | class MergeStats(BaseModel):
  class MergeRequest (line 39) | class MergeRequest(BaseModel):
  class MergeResponse (line 65) | class MergeResponse(BaseModel):
  class LutInfoResponse (line 90) | class LutInfoResponse(BaseModel):

FILE: api/schemas/responses.py
  class CalibrationResponse (line 13) | class CalibrationResponse(BaseModel):
  class PreviewResponse (line 22) | class PreviewResponse(BaseModel):
  class ColorReplaceResponse (line 35) | class ColorReplaceResponse(BaseModel):
  class MergePreviewResponse (line 44) | class MergePreviewResponse(BaseModel):
  class GenerateResponse (line 56) | class GenerateResponse(BaseModel):
  class BatchItemResult (line 66) | class BatchItemResult(BaseModel):
  class BatchResponse (line 74) | class BatchResponse(BaseModel):
  class LutInfo (line 83) | class LutInfo(BaseModel):
  class LUTListResponse (line 91) | class LUTListResponse(BaseModel):
  class WorkerPoolStatus (line 97) | class WorkerPoolStatus(BaseModel):
  class HealthResponse (line 104) | class HealthResponse(BaseModel):
  class ExtractResponse (line 113) | class ExtractResponse(BaseModel):
  class ManualFixResponse (line 124) | class ManualFixResponse(BaseModel):
  class HeightmapUploadResponse (line 132) | class HeightmapUploadResponse(BaseModel):
  class LutColorEntry (line 143) | class LutColorEntry(BaseModel):
  class LutColorsResponse (line 150) | class LutColorsResponse(BaseModel):
  class CropResponse (line 158) | class CropResponse(BaseModel):

FILE: api/schemas/slicer.py
  class SlicerInfo (line 18) | class SlicerInfo(BaseModel):
  class SlicerDetectResponse (line 36) | class SlicerDetectResponse(BaseModel):
  class SlicerLaunchRequest (line 48) | class SlicerLaunchRequest(BaseModel):
  class SlicerLaunchResponse (line 63) | class SlicerLaunchResponse(BaseModel):

FILE: api/schemas/system.py
  class CacheCleanupDetails (line 13) | class CacheCleanupDetails(BaseModel):
  class ClearCacheResponse (line 21) | class ClearCacheResponse(BaseModel):
  class ClearCacheResult (line 32) | class ClearCacheResult:
  class UserSettings (line 41) | class UserSettings(BaseModel):
  class UserSettingsResponse (line 52) | class UserSettingsResponse(BaseModel):
  class SaveSettingsResponse (line 59) | class SaveSettingsResponse(BaseModel):
  class StatsResponse (line 66) | class StatsResponse(BaseModel):

FILE: api/session_store.py
  class SessionStore (line 8) | class SessionStore:
    method __init__ (line 17) | def __init__(self, ttl: int = DEFAULT_TTL) -> None:
    method create (line 24) | def create(self) -> str:
    method get (line 33) | def get(self, session_id: str) -> Optional[Dict[str, Any]]:
    method put (line 41) | def put(self, session_id: str, key: str, value: Any) -> None:
    method register_temp_file (line 51) | def register_temp_file(self, session_id: str, path: str) -> None:
    method cleanup_expired (line 57) | def cleanup_expired(self) -> int:
    method _remove_session (line 69) | def _remove_session(self, session_id: str) -> None:
    method clear_all (line 81) | def clear_all(self) -> int:
    method exists (line 89) | def exists(self, session_id: str) -> bool:

FILE: api/worker_pool.py
  class WorkerPoolManager (line 18) | class WorkerPoolManager:
    method __init__ (line 27) | def __init__(self, max_workers: int | None = None) -> None:
    method start (line 38) | def start(self) -> None:
    method submit (line 44) | async def submit(
    method shutdown (line 74) | def shutdown(self, wait: bool = True) -> None:
    method is_alive (line 87) | def is_alive(self) -> bool:
    method max_workers (line 97) | def max_workers(self) -> int:

FILE: api/workers/converter_workers.py
  function worker_generate_preview (line 18) | def worker_generate_preview(
  function worker_batch_convert_item (line 112) | def worker_batch_convert_item(
  function worker_generate_model (line 183) | def worker_generate_model(

FILE: benchmark.py
  class _Tee (line 35) | class _Tee:
    method __init__ (line 36) | def __init__(self, log_path, console_stream=None, lock=None):
    method write (line 43) | def write(self, msg):
    method flush (line 63) | def flush(self):
    method __getattr__ (line 70) | def __getattr__(self, name):
  class _TeeStderr (line 74) | class _TeeStderr:
    method __init__ (line 75) | def __init__(self, log_file, lock):
    method write (line 82) | def write(self, msg):
    method flush (line 101) | def flush(self):
    method __getattr__ (line 108) | def __getattr__(self, name):
  function _pick_first_existing (line 128) | def _pick_first_existing(candidates):
  function _pick_latest_lut (line 137) | def _pick_latest_lut():
  function _calc_target_width (line 160) | def _calc_target_width(image_path: str, long_edge_mm: float) -> float:
  function _bench_header (line 195) | def _bench_header(label: str):
  function _bench_result (line 201) | def _bench_result(label: str, timings: dict, total: float):
  function run_svg_benchmark (line 207) | def run_svg_benchmark(run_preview: bool = True):
  function run_hifi_benchmark (line 262) | def run_hifi_benchmark(run_preview: bool = True):
  function main (line 322) | def main():

FILE: config.py
  function get_asset_path (line 20) | def get_asset_path(relative_path: str) -> str:
  class PrinterConfig (line 57) | class PrinterConfig:
  class WorkerPoolConfig (line 66) | class WorkerPoolConfig:
    method from_env (line 78) | def from_env(cls) -> "WorkerPoolConfig":
  class SmartConfig (line 98) | class SmartConfig:
  class ModelingMode (line 115) | class ModelingMode(str, Enum):
    method get_display_name (line 121) | def get_display_name(self) -> str:
  class ColorSystem (line 131) | class ColorSystem:
    method get (line 223) | def get(mode: str):
  class BedManager (line 284) | class BedManager:
    method get_choices (line 307) | def get_choices(cls):
    method get_bed_size (line 312) | def get_bed_size(cls, label: str):
    method compute_scale (line 320) | def compute_scale(cls, bed_w_mm, bed_h_mm):
  class VectorConfig (line 328) | class VectorConfig:
  function _env_flag (line 353) | def _env_flag(name: str) -> bool:
  function is_wsl_runtime (line 358) | def is_wsl_runtime() -> bool:
  function get_tray_runtime_policy (line 368) | def get_tray_runtime_policy():

FILE: core/calibration.py
  function _generate_voxel_mesh (line 26) | def _generate_voxel_mesh(voxel_matrix: np.ndarray, material_index: int,
  function generate_calibration_board (line 86) | def generate_calibration_board(color_mode: str, block_size_mm: float,
  function get_top_1296_colors (line 204) | def get_top_1296_colors():
  function generate_smart_board (line 301) | def generate_smart_board(block_size_mm=5.0, gap_mm=0.8):
  function generate_8color_board (line 449) | def generate_8color_board(page_index=0):
  function generate_8color_batch_zip (line 565) | def generate_8color_batch_zip():
  function generate_bw_calibration_board (line 581) | def generate_bw_calibration_board(block_size_mm=5.0, gap_mm=0.8, backing...
  function select_extended_1444_colors (line 737) | def select_extended_1444_colors(base_1024_stacks):
  function get_top_1444_colors (line 855) | def get_top_1444_colors():
  function generate_5color1444_board (line 965) | def generate_5color1444_board(block_size_mm=5.0, gap_mm=0.8):
  function merge_5color_extended (line 1096) | def merge_5color_extended(base_lut_path, extended_lut_path, output_path=...
  function generate_5color_extended_board (line 1159) | def generate_5color_extended_board(block_size_mm=5.0, gap_mm=0.8, page_i...
  function _generate_5color_base_page (line 1197) | def _generate_5color_base_page(block_size_mm, gap_mm, preview_colors, sl...
  function _generate_5color_extended_page (line 1305) | def _generate_5color_extended_page(block_size_mm, gap_mm, preview_colors...
  function generate_5color_extended_batch_zip (line 1427) | def generate_5color_extended_batch_zip(block_size_mm=5.0, gap_mm=0.8):

FILE: core/color_analyzer.py
  class ColorAnalysisResult (line 27) | class ColorAnalysisResult:
    method to_dict (line 41) | def to_dict(self) -> dict:
  class ColorAnalyzer (line 50) | class ColorAnalyzer:
    method analyze (line 71) | def analyze(cls, image_path: str, target_width_mm: float = 60.0,
    method _load_image (line 169) | def _load_image(cls, image_path: str, verbose: bool):
    method _resize_for_analysis (line 194) | def _resize_for_analysis(cls, img_rgb, original_w: int,
    method _calc_unique_colors (line 214) | def _calc_unique_colors(cls, img_rgb, verbose: bool) -> int:
    method _calc_hue_distribution (line 228) | def _calc_hue_distribution(cls, img_rgb, pixel_count: int, verbose: bo...
    method _calc_color_concentration (line 257) | def _calc_color_concentration(cls, img_rgb, verbose: bool):
    method _calc_edge_complexity (line 278) | def _calc_edge_complexity(cls, img_rgb, pixel_count: int, verbose: boo...
    method _score_hue (line 292) | def _score_hue(hue_bins: int, colored_ratio: float) -> int:
    method _score_concentration (line 316) | def _score_concentration(top8_ratio: float) -> int:
    method _score_unique_colors (line 332) | def _score_unique_colors(unique_colors: int) -> int:
    method _score_edge (line 346) | def _score_edge(edge_ratio: float) -> int:
    method _complexity_to_colors (line 358) | def _complexity_to_colors(complexity_score: int) -> tuple:
    method _calc_width_factor (line 374) | def _calc_width_factor(target_width_mm: float) -> float:
    method _align_to_common (line 381) | def _align_to_common(cls, value: int) -> int:
  function analyze_recommended_colors (line 387) | def analyze_recommended_colors(image_path: str, target_width_mm: float =...

FILE: core/color_matching_hue_aware.py
  class HueAwareColorMatcher (line 26) | class HueAwareColorMatcher:
    method __init__ (line 43) | def __init__(self, lut_rgb: np.ndarray, lut_lab: np.ndarray,
    method _resolve_weights (line 74) | def _resolve_weights(self, hue_weight, preset, w_L, w_C, w_H):
    method _lab_to_lch (line 103) | def _lab_to_lch(lab: np.ndarray) -> np.ndarray:
    method _delta_hue (line 121) | def _delta_hue(h1_deg, h2_deg, c1, c2):
    method _weighted_distance (line 138) | def _weighted_distance(self, input_lch, candidate_lch):
    method match_colors_batch (line 156) | def match_colors_batch(self, input_rgb: np.ndarray, k: int = 16) -> np...
    method _rgb_to_lab (line 202) | def _rgb_to_lab(rgb: np.ndarray) -> np.ndarray:

FILE: core/color_merger.py
  class ColorMerger (line 13) | class ColorMerger:
    method __init__ (line 28) | def __init__(self, rgb_to_lab_func: Callable):
    method identify_low_usage_colors (line 40) | def identify_low_usage_colors(self, palette: List[dict],
    method calculate_color_distance (line 75) | def calculate_color_distance(self, color1_rgb: Tuple[int, int, int],
    method find_merge_target (line 109) | def find_merge_target(self, source_color: str, palette: List[dict],
    method build_merge_map (line 183) | def build_merge_map(self, palette: List[dict],
    method apply_color_merging (line 258) | def apply_color_merging(self, matched_rgb: np.ndarray,
    method calculate_quality_metric (line 300) | def calculate_quality_metric(self, original_palette: List[dict],
    method _hex_to_rgb (line 391) | def _hex_to_rgb(hex_str: str) -> Tuple[int, int, int]:

FILE: core/color_replacement.py
  class ColorReplacementManager (line 12) | class ColorReplacementManager:
    method __init__ (line 20) | def __init__(self):
    method add_replacement (line 24) | def add_replacement(self, original: Tuple[int, int, int],
    method remove_replacement (line 46) | def remove_replacement(self, original: Tuple[int, int, int]) -> bool:
    method get_replacement (line 62) | def get_replacement(self, original: Tuple[int, int, int]) -> Optional[...
    method apply_to_image (line 75) | def apply_to_image(self, rgb_array: np.ndarray) -> np.ndarray:
    method clear (line 97) | def clear(self) -> None:
    method __len__ (line 101) | def __len__(self) -> int:
    method __contains__ (line 105) | def __contains__(self, original: Tuple[int, int, int]) -> bool:
    method get_all_replacements (line 110) | def get_all_replacements(self) -> Dict[Tuple[int, int, int], Tuple[int...
    method to_dict (line 119) | def to_dict(self) -> Dict:
    method from_dict (line 132) | def from_dict(cls, data: Dict) -> 'ColorReplacementManager':
    method _validate_color (line 150) | def _validate_color(color: Tuple[int, int, int]) -> Tuple[int, int, int]:
    method _color_to_hex (line 169) | def _color_to_hex(color: Tuple[int, int, int]) -> str:
    method _hex_to_color (line 174) | def _hex_to_color(hex_str: str) -> Tuple[int, int, int]:

FILE: core/converter.py
  function extract_lut_available_colors (line 43) | def extract_lut_available_colors(lut_path: str) -> List[dict]:
  function get_lut_color_choices (line 99) | def get_lut_color_choices(lut_path: str) -> List[tuple]:
  function generate_lut_color_dropdown_html (line 126) | def generate_lut_color_dropdown_html(lut_path: str, selected_color: str ...
  function _compute_connected_region_mask_4n (line 151) | def _compute_connected_region_mask_4n(quantized_image, mask_solid, x, y):
  function _recommend_lut_colors_by_rgb (line 173) | def _recommend_lut_colors_by_rgb(base_rgb, lut_colors, top_k=10):
  function _ensure_quantized_image_in_cache (line 209) | def _ensure_quantized_image_in_cache(cache):
  function _rgb_to_hex (line 223) | def _rgb_to_hex(rgb):
  function _hex_to_rgb_tuple (line 229) | def _hex_to_rgb_tuple(hex_color):
  function _build_selection_meta (line 243) | def _build_selection_meta(q_rgb, m_rgb, scope="region"):
  function _resolve_highlight_mask (line 252) | def _resolve_highlight_mask(color_match, mask_solid, region_mask=None, s...
  function _normalize_color_replacements_input (line 259) | def _normalize_color_replacements_input(color_replacements):
  function _apply_region_replacement (line 291) | def _apply_region_replacement(image_rgb, region_mask, replacement_rgb):
  function _apply_regions_to_raster_outputs (line 298) | def _apply_regions_to_raster_outputs(matched_rgb, material_matrix, mask_...
  function _build_dual_recommendations (line 323) | def _build_dual_recommendations(q_rgb, m_rgb, lut_colors, top_k=10):
  function _resolve_click_selection_hexes (line 331) | def _resolve_click_selection_hexes(cache, default_hex):
  function extract_color_palette (line 349) | def extract_color_palette(preview_cache: dict) -> List[dict]:
  function _save_debug_preview (line 405) | def _save_debug_preview(debug_data, material_matrix, mask_solid, image_p...
  function _get_actual_lut_slot_colors (line 466) | def _get_actual_lut_slot_colors(processor) -> dict:
  function convert_image_to_3d (line 509) | def convert_image_to_3d(image_path, lut_path, target_width_mm, spacer_th...
  function _parse_outline_slot (line 1596) | def _parse_outline_slot(slot_str, num_materials):
  function _generate_outline_mesh (line 1613) | def _generate_outline_mesh(mask_solid, pixel_scale, outline_width_mm, ou...
  function _calculate_loop_info (line 1739) | def _calculate_loop_info(loop_pos, loop_width, loop_length, loop_hole,
  function _draw_loop_on_preview (line 1793) | def _draw_loop_on_preview(preview_rgba, loop_info, color_conf, pixel_sca...
  function calculate_luminance (line 1840) | def calculate_luminance(hex_color):
  function generate_auto_height_map (line 1866) | def generate_auto_height_map(color_list, mode, base_thickness, max_relie...
  function _build_relief_voxel_matrix (line 1954) | def _build_relief_voxel_matrix(matched_rgb, material_matrix, mask_solid,...
  function _build_cloisonne_voxel_matrix (line 2079) | def _build_cloisonne_voxel_matrix(material_matrix, mask_solid, mask_wire...
  function _build_voxel_matrix (line 2150) | def _build_voxel_matrix(material_matrix, mask_solid, spacer_thick, struc...
  function _build_voxel_matrix_6layer (line 2215) | def _build_voxel_matrix_6layer(material_matrix, mask_solid, spacer_thick...
  function _build_voxel_matrix_faceup (line 2238) | def _build_voxel_matrix_faceup(material_matrix, mask_solid, spacer_thick...
  function _create_bed_mesh (line 2280) | def _create_bed_mesh(bed_w_mm, bed_h_mm, is_dark=True):
  function _create_preview_mesh (line 2395) | def _create_preview_mesh(matched_rgb, mask_solid, total_layers, backing_...
  function generate_empty_bed_glb (line 2554) | def generate_empty_bed_glb(bed_w: int = None, bed_h: int = None, is_dark...
  function _merge_low_frequency_colors (line 2582) | def _merge_low_frequency_colors(
  function _build_color_voxel_mesh (line 2622) | def _build_color_voxel_mesh(
  function generate_segmented_glb (line 2693) | def generate_segmented_glb(cache: dict, max_meshes: int = 64) -> Optiona...
  function generate_realtime_glb (line 2883) | def generate_realtime_glb(cache):
  function generate_preview_cached (line 2949) | def generate_preview_cached(image_path, lut_path, target_width_mm,
  function render_preview (line 3059) | def render_preview(preview_rgba, loop_pos, loop_width, loop_length,
  function _draw_loop_on_canvas (line 3184) | def _draw_loop_on_canvas(pil_img, loop_pos, loop_width, loop_length,
  function on_preview_click (line 3250) | def on_preview_click(cache, loop_pos, evt: gr.SelectData, bed_label=None):
  function update_preview_with_loop (line 3307) | def update_preview_with_loop(cache, loop_pos, add_loop,
  function on_remove_loop (line 3329) | def on_remove_loop():
  function generate_final_model (line 3334) | def generate_final_model(image_path, lut_path, target_width_mm, spacer_t...
  function update_preview_with_backing_color (line 3417) | def update_preview_with_backing_color(cache, backing_color_id: int):
  function update_preview_with_replacements (line 3540) | def update_preview_with_replacements(cache, replacement_regions=None,
  function generate_highlight_preview (line 3662) | def generate_highlight_preview(cache, highlight_color: str,
  function clear_highlight_preview (line 3802) | def clear_highlight_preview(cache, loop_pos=None, add_loop=False,
  function on_preview_click_select_color (line 3850) | def on_preview_click_select_color(cache, evt: gr.SelectData, bed_label=N...
  function generate_lut_grid_html (line 3953) | def generate_lut_grid_html(lut_path, lang: str = "zh"):
  function generate_lut_card_grid_html (line 4032) | def generate_lut_card_grid_html(lut_path, lang: str = "zh"):
  function _infer_4color_subtype (line 4142) | def _infer_4color_subtype(lut_path: str) -> str:
  function detect_lut_color_mode (line 4153) | def detect_lut_color_mode(lut_path):
  function detect_image_type (line 4252) | def detect_image_type(image_path):

FILE: core/extractor.py
  function generate_simulated_reference (line 23) | def generate_simulated_reference():
  function rotate_image (line 47) | def rotate_image(img, direction):
  function draw_corner_points (line 58) | def draw_corner_points(img, points, color_mode: str, page_choice: str | ...
  function apply_auto_white_balance (line 133) | def apply_auto_white_balance(img):
  function apply_brightness_correction (line 143) | def apply_brightness_correction(img):
  function run_extraction (line 163) | def run_extraction(img, points, offset_x, offset_y, zoom, barrel, wb, br...
  function probe_lut_cell (line 290) | def probe_lut_cell(lut_path, evt: gr.SelectData):
  function manual_fix_cell (line 327) | def manual_fix_cell(coord, color_input, lut_path=None):

FILE: core/five_color_combination.py
  class ColorQueryResult (line 15) | class ColorQueryResult:
  class ColorCountDetector (line 24) | class ColorCountDetector:
    method detect_color_count (line 36) | def detect_color_count(lut_data: np.ndarray) -> Tuple[int, int]:
  class StackFileManager (line 59) | class StackFileManager:
    method find_stack_file (line 66) | def find_stack_file(color_count: int) -> Optional[str]:
    method validate_stack_format (line 83) | def validate_stack_format(stack_data: np.ndarray, color_count: int) ->...
  class StackLUTLoader (line 104) | class StackLUTLoader:
    method load_stack_lut (line 108) | def load_stack_lut(file_path: str) -> Tuple[bool, str, Optional[np.nda...
    method load_npz_file (line 140) | def load_npz_file(file_path: str) -> Tuple[bool, str, Optional[np.ndar...
    method load_lut_rgb (line 188) | def load_lut_rgb(file_path: str) -> Tuple[bool, str, Optional[np.ndarr...
  class ColorQueryEngine (line 223) | class ColorQueryEngine:
    method __init__ (line 226) | def __init__(self, stack_lut: Optional[np.ndarray], lut_rgb: np.ndarra...
    method query (line 262) | def query(self, selected_indices: List[int]) -> ColorQueryResult:
    method _query_with_stack (line 288) | def _query_with_stack(self, selected_indices: List[int]) -> ColorQuery...
    method _query_without_stack (line 312) | def _query_without_stack(self, selected_indices: List[int]) -> ColorQu...
    method get_base_colors (line 340) | def get_base_colors(self) -> List[Tuple[int, int, int]]:
    method get_color_names (line 348) | def get_color_names(self) -> List[str]:
    method reverse_selection (line 356) | def reverse_selection(self, selected_indices: List[int]) -> List[int]:
  function rgb_to_hex (line 369) | def rgb_to_hex(rgb: Tuple[int, int, int]) -> str:
  function format_selection_sequence (line 381) | def format_selection_sequence(selected_indices: List[int], color_names: ...
  function get_color_name_from_rgb (line 408) | def get_color_name_from_rgb(rgb: Tuple[int, int, int]) -> str:

FILE: core/geometry_utils.py
  function create_keychain_loop (line 10) | def create_keychain_loop(width_mm, length_mm, hole_dia_mm, thickness_mm,
  function _connect_rings (line 144) | def _connect_rings(outer_indices, hole_indices, vertices_arr, is_top=True):

FILE: core/heightmap_loader.py
  class HeightmapLoader (line 20) | class HeightmapLoader:
    method _to_grayscale (line 27) | def _to_grayscale(image: np.ndarray) -> np.ndarray:
    method _resize_to_target (line 57) | def _resize_to_target(grayscale: np.ndarray, target_w: int, target_h: ...
    method _map_grayscale_to_height (line 75) | def _map_grayscale_to_height(
    method _check_aspect_ratio (line 99) | def _check_aspect_ratio(
    method _check_contrast (line 124) | def _check_contrast(grayscale: np.ndarray) -> str | None:
    method load_and_validate (line 136) | def load_and_validate(heightmap_path: str) -> dict:
    method load_and_process (line 213) | def load_and_process(

FILE: core/i18n.py
  class I18n (line 7) | class I18n:
    method get (line 1338) | def get(key: str, lang: str = 'zh') -> str:
    method get_all (line 1354) | def get_all(lang: str = 'zh') -> dict:

FILE: core/image_preprocessor.py
  class CropRegion (line 28) | class CropRegion:
    method to_tuple (line 35) | def to_tuple(self) -> Tuple[int, int, int, int]:
    method clamp (line 39) | def clamp(self, img_width: int, img_height: int) -> 'CropRegion':
  class ImageInfo (line 49) | class ImageInfo:
  class ImagePreprocessor (line 59) | class ImagePreprocessor:
    method detect_format (line 71) | def detect_format(image_path: str) -> str:
    method get_image_dimensions (line 112) | def get_image_dimensions(image_path: str) -> Tuple[int, int]:
    method convert_to_png (line 135) | def convert_to_png(image_path: str, output_path: Optional[str] = None)...
    method crop_image (line 177) | def crop_image(image_path: str, x: int, y: int,
    method validate_crop_region (line 238) | def validate_crop_region(img_width: int, img_height: int,
    method process_upload (line 260) | def process_upload(cls, image_path: str) -> ImageInfo:
    method analyze_recommended_colors (line 298) | def analyze_recommended_colors(image_path: str, target_width_mm: float...

FILE: core/image_processing.py
  class LuminaImageProcessor (line 36) | class LuminaImageProcessor:
    method _rgb_to_lab (line 44) | def _rgb_to_lab(rgb_array):
    method __init__ (line 64) | def __init__(self, lut_path, color_mode, hue_weight: float = 0.0):
    method _load_svg (line 89) | def _load_svg(self, svg_path, target_width_mm, pixels_per_mm: float = ...
    method _load_lut (line 218) | def _load_lut(self, lut_path):
    method process_image (line 489) | def process_image(self, image_path, target_width_mm, modeling_mode,
    method _process_high_fidelity_mode (line 654) | def _process_high_fidelity_mode(self, rgb_arr, target_h, target_w, qua...
    method _process_pixel_mode (line 872) | def _process_pixel_mode(self, rgb_arr, target_h, target_w):
    method _extract_wireframe_mask (line 897) | def _extract_wireframe_mask(self, rgb_arr, target_w, pixel_scale, wire...

FILE: core/isolated_pixel_cleanup.py
  function _encode_stacks (line 17) | def _encode_stacks(material_matrix: np.ndarray, base: int) -> np.ndarray:
  function _detect_isolated (line 39) | def _detect_isolated(encoded: np.ndarray) -> np.ndarray:
  function _find_neighbor_mode (line 96) | def _find_neighbor_mode(encoded: np.ndarray, isolated_mask: np.ndarray) ...
  function cleanup_isolated_pixels (line 135) | def cleanup_isolated_pixels(

FILE: core/lut_merger.py
  function _detect_mode_by_size (line 47) | def _detect_mode_by_size(count):
  function _detect_4color_subtype (line 91) | def _detect_4color_subtype(lut_path):
  function _detect_6color_subtype (line 103) | def _detect_6color_subtype(lut_path):
  function _remap_stacks (line 115) | def _remap_stacks(stacks, color_mode, lut_path=None):
  class LUTMerger (line 149) | class LUTMerger:
    method detect_color_mode (line 153) | def detect_color_mode(lut_path: str):
    method validate_compatibility (line 184) | def validate_compatibility(modes):
    method load_lut_with_stacks (line 224) | def load_lut_with_stacks(lut_path: str, color_mode: str):
    method merge_luts (line 347) | def merge_luts(lut_entries, dedup_threshold=3.0):
    method save_merged_lut (line 439) | def save_merged_lut(rgb, stacks, output_path):

FILE: core/mesh_generators.py
  function _greedy_rect_numba (line 39) | def _greedy_rect_numba(mask):
  function _greedy_rect_numba (line 80) | def _greedy_rect_numba(mask):
  class BaseMesher (line 84) | class BaseMesher(ABC):
    method generate_mesh (line 88) | def generate_mesh(self, voxel_matrix, mat_id, height_px):
    method generate_backing_mesh (line 102) | def generate_backing_mesh(self, voxel_matrix, height_px):
  class VoxelMesher (line 116) | class VoxelMesher(BaseMesher):
    method generate_mesh (line 124) | def generate_mesh(self, voxel_matrix, mat_id, height_px):
  class HighFidelityMesher (line 177) | class HighFidelityMesher(BaseMesher):
    method generate_mesh (line 198) | def generate_mesh(self, voxel_matrix, mat_id, height_px):
    method _greedy_rect_merge (line 300) | def _greedy_rect_merge(self, mask, height_px):
    method _merge_layers_with_dilation (line 378) | def _merge_layers_with_dilation(self, voxel_matrix, mat_id):
  function get_mesher (line 427) | def get_mesher(mode_name: ModelingMode):

FILE: core/naming.py
  function _get_timestamp (line 38) | def _get_timestamp() -> str:
  function _sanitize (line 43) | def _sanitize(name: str) -> str:
  function _strip_temp_prefix (line 55) | def _strip_temp_prefix(name: str) -> str:
  function generate_model_filename (line 60) | def generate_model_filename(
  function generate_preview_filename (line 81) | def generate_preview_filename(
  function generate_calibration_filename (line 96) | def generate_calibration_filename(
  function generate_batch_filename (line 113) | def generate_batch_filename(
  function parse_filename (line 146) | def parse_filename(filename: str) -> Optional[Dict[str, str]]:

FILE: core/slicer.py
  class DetectedSlicer (line 32) | class DetectedSlicer:
  function _match_slicer_id (line 43) | def _match_slicer_id(display_name: str) -> tuple[str, str] | None:
  function _extract_exe_from_icon (line 59) | def _extract_exe_from_icon(icon_value: str) -> str | None:
  function _find_exe_in_directory (line 77) | def _find_exe_in_directory(directory: str) -> str | None:
  function scan_registry (line 89) | def scan_registry() -> list[DetectedSlicer]:
  function detect_installed_slicers (line 176) | def detect_installed_slicers() -> list[DetectedSlicer]:
  function launch_slicer (line 191) | def launch_slicer(

FILE: core/tray.py
  class LuminaTray (line 21) | class LuminaTray:
    method __init__ (line 22) | def __init__(self, port=7860):
    method _get_system_language (line 28) | def _get_system_language(self):
    method _get_text (line 38) | def _get_text(self, key):
    method open_browser (line 56) | def open_browser(self, icon=None, item=None):
    method open_github (line 60) | def open_github(self, icon=None, item=None):
    method exit_app (line 64) | def exit_app(self, icon=None, item=None):
    method setup_tray (line 72) | def setup_tray(self):
    method run (line 130) | def run(self):

FILE: core/vector_engine.py
  function _get_image_processor_class (line 37) | def _get_image_processor_class():
  class VectorProcessor (line 45) | class VectorProcessor:
    method __init__ (line 59) | def __init__(self, lut_path: str, color_mode: str):
    method svg_to_mesh (line 72) | def svg_to_mesh(
    method _clip_occlusion (line 308) | def _clip_occlusion(shape_data, return_silhouette=False):
    method _match_colors (line 394) | def _match_colors(self, clipped_shapes, replacement_manager, num_chann...
    method _build_channel_runs (line 458) | def _build_channel_runs(recipe, layers_to_use, num_channels):
    method _run_length_extrude (line 481) | def _run_length_extrude(matched_shapes, num_layers, layer_h,
    method _add_double_sided_layers (line 534) | def _add_double_sided_layers(matched_shapes, num_layers, layer_h,
    method _parse_svg (line 575) | def _parse_svg(self, svg_path: str, target_width_mm: float):
    method _extrude_geometry (line 729) | def _extrude_geometry(geometry, height, z_offset, scale, extrude_cache...
    method _fix_coordinates (line 771) | def _fix_coordinates(mesh, svg_height_mm):

FILE: frontend/scripts/generate-test-glb.mjs
  function buildGlb (line 12) | function buildGlb() {

FILE: frontend/src/App.tsx
  type ErrorBoundaryProps (line 24) | interface ErrorBoundaryProps {
  type ErrorBoundaryState (line 29) | interface ErrorBoundaryState {
  class SceneErrorBoundary (line 33) | class SceneErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoun...
    method constructor (line 34) | constructor(props: ErrorBoundaryProps) {
    method getDerivedStateFromError (line 39) | static getDerivedStateFromError(): ErrorBoundaryState {
    method render (line 43) | render() {
  function WidgetToggles (line 53) | function WidgetToggles() {
  constant MODAL_TABS (line 96) | const MODAL_TABS: TabId[] = ['calibration', 'extractor', 'lut-manager', ...
  constant MODAL_TITLE_KEYS (line 98) | const MODAL_TITLE_KEYS: Record<string, string> = {
  function AppContent (line 107) | function AppContent() {
  function App (line 212) | function App() {

FILE: frontend/src/__tests__/LutColorGrid.test.ts
  function entry (line 6) | function entry(hex: string, rgb: [number, number, number]): LutColorEntry {

FILE: frontend/src/__tests__/action-bar-always-visible.property.test.ts
  function computeIsDisabled (line 20) | function computeIsDisabled(

FILE: frontend/src/__tests__/autoPreview.test.ts
  function fakeFile (line 7) | function fakeFile(name = "test.png"): File {

FILE: frontend/src/__tests__/bed-size-selector.property.test.ts
  function resetStore (line 8) | function resetStore(): void {

FILE: frontend/src/__tests__/calibration-store.property.test.ts
  function snapshotCalibrationState (line 75) | function snapshotCalibrationState() {
  function snapshotConverterState (line 92) | function snapshotConverterState() {

FILE: frontend/src/__tests__/colorRemap.property.test.ts
  function resetRemapState (line 16) | function resetRemapState(): void {

FILE: frontend/src/__tests__/converter-store.property.test.ts
  constant NUMERIC_FIELDS (line 12) | const NUMERIC_FIELDS = [
  constant ISOLATED_NUMERIC_FIELDS (line 29) | const ISOLATED_NUMERIC_FIELDS = NUMERIC_FIELDS.filter(
  function resetStore (line 34) | function resetStore(): void {

FILE: frontend/src/__tests__/extractor-5color.property.test.ts
  constant DEFAULT_STATE (line 42) | const DEFAULT_STATE: Partial<ExtractorState> = {
  function resetStore (line 72) | function resetStore(): void {
  function mockExtractResponse (line 77) | function mockExtractResponse(): ExtractResponse {

FILE: frontend/src/__tests__/extractor-api.property.test.ts
  function resetExtractorStore (line 30) | function resetExtractorStore(): void {

FILE: frontend/src/__tests__/extractor-canvas.test.tsx
  method fillStyle (line 27) | set fillStyle(_v: string) {}
  method strokeStyle (line 28) | set strokeStyle(_v: string) {}
  method lineWidth (line 29) | set lineWidth(_v: number) {}
  method font (line 30) | set font(_v: string) {}
  method textAlign (line 31) | set textAlign(_v: string) {}
  method textBaseline (line 32) | set textBaseline(_v: string) {}
  class MockImage (line 37) | class MockImage {
    method src (line 42) | get src() {
    method src (line 45) | set src(val: string) {
  function resetExtractorStore (line 55) | function resetExtractorStore(): void {

FILE: frontend/src/__tests__/extractor-panel.test.tsx
  class MockImage (line 18) | class MockImage {
    method src (line 23) | get src() {
    method src (line 26) | set src(val: string) {
  function resetExtractorStore (line 41) | function resetExtractorStore(): void {

FILE: frontend/src/__tests__/extractor-store.property.test.ts
  function resetExtractorStore (line 26) | function resetExtractorStore(): void {
  function snapshotExtractorState (line 53) | function snapshotExtractorState() {
  function snapshotConverterState (line 81) | function snapshotConverterState() {
  function snapshotCalibrationState (line 120) | function snapshotCalibrationState() {

FILE: frontend/src/__tests__/five-color-store.property.test.ts
  constant DEFAULT_STATE (line 19) | const DEFAULT_STATE: Partial<FiveColorState> = {
  function resetStore (line 28) | function resetStore() {

FILE: frontend/src/__tests__/lut-manager-store.property.test.ts
  constant COLOR_MODES (line 8) | const COLOR_MODES = [
  constant PRIMARY_MODES (line 17) | const PRIMARY_MODES = ["6-Color", "8-Color"] as const;
  constant ALLOWED_SECONDARY (line 19) | const ALLOWED_SECONDARY: Record<string, string[]> = {

FILE: frontend/src/__tests__/lutColorFilter.property.test.ts
  constant VALID_HUE_CATEGORIES (line 54) | const VALID_HUE_CATEGORIES: HueCategory[] = [

FILE: frontend/src/__tests__/paletteLutMerge.test.tsx
  function renderWithI18n (line 48) | function renderWithI18n(ui: React.ReactElement) {

FILE: frontend/src/__tests__/paletteOptimization.test.tsx
  constant PALETTE (line 18) | const PALETTE: PaletteEntry[] = [
  constant LUT_COLORS (line 24) | const LUT_COLORS: LutColorEntry[] = [
  function resetStore (line 40) | function resetStore() {
  function importLutColorGrid (line 61) | async function importLutColorGrid() {
  function importPalettePanel (line 66) | async function importPalettePanel() {

FILE: frontend/src/__tests__/realtimePreview.property.test.ts
  type MutualExclusionOp (line 146) | type MutualExclusionOp = "relief" | "cloisonne";
  function resetMutualExclusionState (line 155) | function resetMutualExclusionState(): void {

FILE: frontend/src/__tests__/reliefHeight.property.test.ts
  function resetReliefState (line 44) | function resetReliefState(palette: PaletteEntry[] = []): void {
  function computeMeshScaleZ (line 58) | function computeMeshScaleZ(heightMm: number, baseHeight: number): number {

FILE: frontend/src/__tests__/replace-preview.property.test.ts
  method src (line 37) | set src(_: string) {
  constant DEFAULT_STATE (line 46) | const DEFAULT_STATE: Partial<ConverterState> = {
  function resetStore (line 66) | function resetStore(): void {

FILE: frontend/src/__tests__/settings-store.property.test.ts
  function resetStore (line 8) | function resetStore() {

FILE: frontend/src/__tests__/slicer.property.test.ts
  function mapThreemfDiskPath (line 16) | function mapThreemfDiskPath(
  function mapDownloadUrl (line 26) | function mapDownloadUrl(download_url: string | null | undefined): string...
  function seedThreemfPath (line 115) | function seedThreemfPath() {
  function expectInvalidated (line 123) | function expectInvalidated() {
  function resolveSelectedSlicerId (line 382) | function resolveSelectedSlicerId(

FILE: frontend/src/__tests__/stack-positions-nonoverlap.property.test.ts
  constant ALL_WIDGET_IDS (line 24) | const ALL_WIDGET_IDS: WidgetId[] = [
  function assertNoOverlap (line 122) | function assertNoOverlap(

FILE: frontend/src/__tests__/tab-filter.property.test.ts
  constant ALL_TAB_IDS (line 19) | const ALL_TAB_IDS: TabId[] = ['converter', 'calibration', 'extractor', '...

FILE: frontend/src/__tests__/tab-switch-layout.property.test.ts
  constant ALL_TAB_IDS (line 19) | const ALL_TAB_IDS: TabId[] = ['converter', 'calibration', 'extractor', '...
  constant ALL_WIDGET_IDS (line 22) | const ALL_WIDGET_IDS: WidgetId[] = [

FILE: frontend/src/__tests__/theme.property.test.ts
  constant STRING_FIELDS (line 11) | const STRING_FIELDS: (keyof ThemeColors)[] = [
  constant NUMBER_FIELDS (line 21) | const NUMBER_FIELDS: (keyof ThemeColors)[] = [
  constant ALL_FIELDS (line 26) | const ALL_FIELDS = [...STRING_FIELDS, ...NUMBER_FIELDS];

FILE: frontend/src/__tests__/widget-workspace.test.tsx
  function renderWithI18n (line 13) | function renderWithI18n(ui: React.ReactElement) {

FILE: frontend/src/api/calibration.ts
  function calibrationGenerate (line 5) | async function calibrationGenerate(

FILE: frontend/src/api/converter.ts
  function convertPreview (line 17) | async function convertPreview(
  function convertGenerate (line 36) | async function convertGenerate(
  function fetchLutList (line 49) | async function fetchLutList(): Promise<LutListResponse> {
  function getFileUrl (line 57) | function getFileUrl(fileId: string): string {
  function fetchBedSizes (line 62) | async function fetchBedSizes(): Promise<BedSizeListResponse> {
  function fetchBedPreview (line 68) | async function fetchBedPreview(bedLabel: string): Promise<string> {
  function uploadHeightmap (line 77) | async function uploadHeightmap(
  function fetchLutColors (line 94) | async function fetchLutColors(
  type CropResponse (line 105) | interface CropResponse {
  function cropImage (line 114) | async function cropImage(
  function convertBatch (line 132) | async function convertBatch(
  function replaceColor (line 153) | async function replaceColor(

FILE: frontend/src/api/extractor.ts
  function extractColors (line 5) | async function extractColors(
  function manualFixCell (line 40) | async function manualFixCell(
  function mergeEightColor (line 53) | async function mergeEightColor(): Promise<ExtractResponse> {
  function mergeFiveColorExtended (line 63) | async function mergeFiveColorExtended(): Promise<ExtractResponse> {

FILE: frontend/src/api/fiveColor.ts
  function fetchBaseColors (line 5) | async function fetchBaseColors(lutName: string): Promise<BaseColorsRespo...
  function queryFiveColor (line 14) | async function queryFiveColor(request: FiveColorQueryRequest): Promise<F...

FILE: frontend/src/api/lut.ts
  function fetchLutInfo (line 5) | async function fetchLutInfo(
  function mergeLuts (line 15) | async function mergeLuts(

FILE: frontend/src/api/slicer.ts
  function detectSlicers (line 9) | async function detectSlicers(): Promise<SlicerDetectResponse> {
  function launchSlicer (line 15) | async function launchSlicer(

FILE: frontend/src/api/system.ts
  function clearCache (line 11) | async function clearCache(): Promise<ClearCacheResponse> {
  function getSettings (line 19) | async function getSettings(): Promise<UserSettingsResponse> {
  function saveSettings (line 25) | async function saveSettings(settings: UserSettings): Promise<SaveSetting...
  function getStats (line 34) | async function getStats(): Promise<StatsResponse> {

FILE: frontend/src/api/types.ts
  type HealthResponse (line 1) | interface HealthResponse {
  type ColorMode (line 9) | enum ColorMode {
  type ModelingMode (line 19) | enum ModelingMode {
  type StructureMode (line 25) | enum StructureMode {
  type ConvertPreviewRequest (line 32) | interface ConvertPreviewRequest {
  type ConvertGenerateRequest (line 45) | interface ConvertGenerateRequest extends ConvertPreviewRequest {
  type ColorReplacementItem (line 69) | interface ColorReplacementItem {
  type PaletteEntry (line 78) | interface PaletteEntry {
  type AutoHeightMode (line 86) | type AutoHeightMode =
  type HeightmapUploadResponse (line 92) | interface HeightmapUploadResponse {
  type PreviewResponse (line 104) | interface PreviewResponse {
  type GenerateResponse (line 116) | interface GenerateResponse {
  type LutListResponse (line 124) | interface LutListResponse {
  type LutInfo (line 128) | interface LutInfo {
  type BedSizeItem (line 134) | interface BedSizeItem {
  type BedSizeListResponse (line 141) | interface BedSizeListResponse {
  type CalibrationColorMode (line 147) | enum CalibrationColorMode {
  type BackingColor (line 157) | enum BackingColor {
  type CalibrationGenerateRequest (line 168) | interface CalibrationGenerateRequest {
  type CalibrationResponse (line 177) | interface CalibrationResponse {
  type ExtractorColorMode (line 186) | enum ExtractorColorMode {
  type ExtractorPage (line 196) | enum ExtractorPage {
  type ExtractResponse (line 203) | interface ExtractResponse {
  type ManualFixResponse (line 212) | interface ManualFixResponse {
  type LutInfoResponse (line 220) | interface LutInfoResponse {
  type MergeStats (line 226) | interface MergeStats {
  type MergeRequest (line 233) | interface MergeRequest {
  type MergeResponse (line 239) | interface MergeResponse {
  type ClearCacheResponse (line 248) | interface ClearCacheResponse {
  type LutColorEntry (line 262) | interface LutColorEntry {
  type LutColorsResponse (line 267) | interface LutColorsResponse {
  type SlicerInfo (line 275) | interface SlicerInfo {
  type SlicerDetectResponse (line 281) | interface SlicerDetectResponse {
  type SlicerLaunchRequest (line 285) | interface SlicerLaunchRequest {
  type SlicerLaunchResponse (line 290) | interface SlicerLaunchResponse {
  type BatchItemResult (line 297) | interface BatchItemResult {
  type BatchResponse (line 303) | interface BatchResponse {
  type BatchConvertParams (line 310) | interface BatchConvertParams {
  type BaseColorEntry (line 326) | interface BaseColorEntry {
  type BaseColorsResponse (line 333) | interface BaseColorsResponse {
  type FiveColorQueryRequest (line 339) | interface FiveColorQueryRequest {
  type FiveColorQueryResponse (line 344) | interface FiveColorQueryResponse {
  type ColorReplaceResponse (line 355) | interface ColorReplaceResponse {
  type UserSettings (line 364) | interface UserSettings {
  type UserSettingsResponse (line 373) | interface UserSettingsResponse {
  type SaveSettingsResponse (line 378) | interface SaveSettingsResponse {
  type StatsResponse (line 383) | interface StatsResponse {

FILE: frontend/src/components/AboutView.tsx
  function AboutView (line 5) | function AboutView() {

FILE: frontend/src/components/BedPlatform.tsx
  function createBedTexture (line 13) | function createBedTexture(
  function createRoundedBedGeometry (line 89) | function createRoundedBedGeometry(
  function BedPlatform (line 124) | function BedPlatform() {

FILE: frontend/src/components/CalibrationPanel.tsx
  function CalibrationPanel (line 18) | function CalibrationPanel() {

FILE: frontend/src/components/ExtractorCanvas.tsx
  constant CORNER_LABELS (line 8) | const CORNER_LABELS: Record<string, string[]> = {
  function canvasClickToImageCoord (line 38) | function canvasClickToImageCoord(
  constant MARKER_RADIUS (line 54) | const MARKER_RADIUS = 8;
  constant MARKER_FONT (line 55) | const MARKER_FONT = "bold 12px sans-serif";
  constant MARKER_FILL (line 56) | const MARKER_FILL = "rgba(255, 50, 50, 0.85)";
  constant MARKER_STROKE (line 57) | const MARKER_STROKE = "#ffffff";
  constant MARKER_TEXT_COLOR (line 58) | const MARKER_TEXT_COLOR = "#ffffff";
  function drawCanvas (line 62) | function drawCanvas(
  constant LUT_GRID_SIZE (line 97) | const LUT_GRID_SIZE: Record<string, number> = {
  function ExtractorCanvas (line 107) | function ExtractorCanvas() {

FILE: frontend/src/components/ExtractorPanel.tsx
  function ExtractorPanel (line 20) | function ExtractorPanel() {

FILE: frontend/src/components/FiveColorCanvas.tsx
  type SliceColor (line 8) | interface SliceColor {
  type FiveColorCanvasProps (line 13) | interface FiveColorCanvasProps {
  constant SLICE_W (line 23) | const SLICE_W = 160;
  constant SLICE_H (line 24) | const SLICE_H = 24;
  constant SLICE_SKEW (line 25) | const SLICE_SKEW = 20;
  constant SLICE_GAP (line 26) | const SLICE_GAP = 6;
  constant CORNER_R (line 27) | const CORNER_R = 6;
  function drawSlice (line 30) | function drawSlice(
  function drawResultCircle (line 84) | function drawResultCircle(
  function FiveColorCanvas (line 114) | function FiveColorCanvas({ slices, resultHex, isLoading }: FiveColorCanv...

FILE: frontend/src/components/FiveColorQueryPanel.tsx
  function FiveColorQueryPanel (line 8) | function FiveColorQueryPanel() {

FILE: frontend/src/components/InteractiveModelViewer.tsx
  function extractHexFromMeshName (line 16) | function extractHexFromMeshName(meshName: string): string {
  function toggleColorSelection (line 28) | function toggleColorSelection(
  type InteractiveModelViewerProps (line 37) | interface InteractiveModelViewerProps {
  constant COLOR_LAYER_HEIGHT (line 52) | const COLOR_LAYER_HEIGHT = 0.4;
  function InteractiveModelViewer (line 54) | function InteractiveModelViewer({

FILE: frontend/src/components/KeychainRing3D.tsx
  constant EXTRUDE_DEPTH (line 13) | const EXTRUDE_DEPTH = 1.2;
  constant TOP_OFFSET (line 16) | const TOP_OFFSET = 0.1;
  constant HOLE_SEGMENTS (line 19) | const HOLE_SEGMENTS = 32;
  type KeychainRing3DProps (line 21) | interface KeychainRing3DProps {
  function createKeychainRingGeometry (line 44) | function createKeychainRingGeometry(
  function KeychainRing3D (line 85) | function KeychainRing3D({

FILE: frontend/src/components/LanguageToggle.tsx
  function LanguageToggle (line 4) | function LanguageToggle() {

FILE: frontend/src/components/LoadingSpinner.tsx
  function LoadingSpinner (line 1) | function LoadingSpinner() {

FILE: frontend/src/components/LutManagerPanel.tsx
  function LutManagerPanel (line 8) | function LutManagerPanel() {

FILE: frontend/src/components/ModelViewer.tsx
  function computeCenterOffset (line 10) | function computeCenterOffset(
  function computeFitDistance (line 25) | function computeFitDistance(
  type ModelViewerProps (line 33) | interface ModelViewerProps {
  function ModelViewer (line 37) | function ModelViewer({ url }: ModelViewerProps) {

FILE: frontend/src/components/Scene3D.tsx
  type Scene3DProps (line 15) | interface Scene3DProps {
  function ScreenshotHelper (line 23) | function ScreenshotHelper({
  function CameraDebugHelper (line 40) | function CameraDebugHelper() {
  function ThemeUpdater (line 63) | function ThemeUpdater() {
  function Scene3D (line 72) | function Scene3D({ modelUrl }: Scene3DProps) {

FILE: frontend/src/components/ThemeToggle.tsx
  function ThemeToggle (line 5) | function ThemeToggle() {

FILE: frontend/src/components/__tests__/ActionBar.batch.test.tsx
  function makeFile (line 15) | function makeFile(name: string, type = "image/png"): File {

FILE: frontend/src/components/__tests__/BasicSettings.batch.test.tsx
  function makeFile (line 29) | function makeFile(name: string, type = "image/png"): File {

FILE: frontend/src/components/__tests__/BatchFileUploader.test.tsx
  function makeFile (line 5) | function makeFile(name: string): File {

FILE: frontend/src/components/lightingConfig.ts
  constant LIGHTING_CONFIG (line 4) | const LIGHTING_CONFIG = {
  type LightingConfig (line 26) | type LightingConfig = typeof LIGHTING_CONFIG;

FILE: frontend/src/components/sections/ActionBar.tsx
  function ActionBar (line 9) | function ActionBar() {

FILE: frontend/src/components/sections/AdvancedSettings.tsx
  function AdvancedSettings (line 6) | function AdvancedSettings() {

FILE: frontend/src/components/sections/BasicSettings.tsx
  function BasicSettings (line 18) | function BasicSettings() {

FILE: frontend/src/components/sections/BedSizeSelector.tsx
  function BedSizeSelector (line 5) | function BedSizeSelector() {

FILE: frontend/src/components/sections/CloisonneSettings.tsx
  function CloisonneSettings (line 7) | function CloisonneSettings() {

FILE: frontend/src/components/sections/CoatingSettings.tsx
  function CoatingSettings (line 6) | function CoatingSettings() {

FILE: frontend/src/components/sections/KeychainLoopSettings.tsx
  function KeychainLoopSettings (line 6) | function KeychainLoopSettings() {

FILE: frontend/src/components/sections/LutColorGrid.tsx
  type HueCategory (line 7) | type HueCategory =
  function classifyHue (line 21) | function classifyHue(r: number, g: number, b: number): HueCategory {
  function matchesSearch (line 55) | function matchesSearch(entry: LutColorEntry, query: string): boolean {
  function loadFavorites (line 71) | function loadFavorites(lutKey: string): Set<string> {
  function saveFavorites (line 78) | function saveFavorites(lutKey: string, favs: Set<string>) {
  function ColorSwatch (line 86) | function ColorSwatch({
  function ColorSection (line 128) | function ColorSection({
  function LutColorGrid (line 177) | function LutColorGrid() {

FILE: frontend/src/components/sections/OutlineSettings.tsx
  function OutlineSettings (line 7) | function OutlineSettings() {

FILE: frontend/src/components/sections/PalettePanel.tsx
  type PaletteItemProps (line 9) | interface PaletteItemProps {
  function PaletteItem (line 20) | function PaletteItem({
  type ColorBlockProps (line 120) | interface ColorBlockProps {
  function ColorBlock (line 125) | function ColorBlock({ label, hex }: ColorBlockProps) {
  type SelectedColorDetailProps (line 140) | interface SelectedColorDetailProps {
  function SelectedColorDetail (line 145) | function SelectedColorDetail({ entry, remappedHex }: SelectedColorDetail...
  function PalettePanel (line 158) | function PalettePanel() {

FILE: frontend/src/components/sections/ReliefSettings.tsx
  function ReliefSettings (line 11) | function ReliefSettings() {

FILE: frontend/src/components/sections/SlicerSelector.tsx
  type SlicerBrandStyle (line 7) | interface SlicerBrandStyle {
  constant DEFAULT_BRAND_STYLE (line 14) | const DEFAULT_BRAND_STYLE: SlicerBrandStyle = {
  constant SLICER_BRAND_COLORS (line 24) | const SLICER_BRAND_COLORS: Record<string, SlicerBrandStyle> = {
  function getSlicerBrandStyle (line 39) | function getSlicerBrandStyle(slicerId: string): SlicerBrandStyle {
  function getButtonLabel (line 54) | function getButtonLabel(
  type SlicerSelectorProps (line 68) | interface SlicerSelectorProps {
  function SlicerSelector (line 75) | function SlicerSelector({

FILE: frontend/src/components/themeConfig.ts
  type ThemeColors (line 10) | interface ThemeColors {
  constant THEME_CONFIG (line 35) | const THEME_CONFIG: Record<"light" | "dark", ThemeColors> = {

FILE: frontend/src/components/ui/Accordion.tsx
  type AccordionProps (line 3) | interface AccordionProps {
  function Accordion (line 9) | function Accordion({

FILE: frontend/src/components/ui/BatchFileUploader.tsx
  type BatchFileUploaderProps (line 4) | interface BatchFileUploaderProps {
  function BatchFileUploader (line 11) | function BatchFileUploader({

FILE: frontend/src/components/ui/BatchResultSummary.tsx
  type BatchResultSummaryProps (line 4) | interface BatchResultSummaryProps {
  function BatchResultSummary (line 8) | function BatchResultSummary({ result }: BatchResultSummaryProps) {

FILE: frontend/src/components/ui/Button.tsx
  type ButtonProps (line 1) | interface ButtonProps {
  function Button (line 9) | function Button({

FILE: frontend/src/components/ui/Checkbox.tsx
  type CheckboxProps (line 1) | interface CheckboxProps {
  function Checkbox (line 8) | function Checkbox({

FILE: frontend/src/components/ui/ColorModeBadge.tsx
  type ColorModeBadgeProps (line 1) | interface ColorModeBadgeProps {
  constant DOTS_RYBW (line 6) | const DOTS_RYBW    = ["#DC143C", "#FFE600", "#0064F0", "#F0F0F0"];
  constant DOTS_CMYW (line 7) | const DOTS_CMYW    = ["#00FFFF", "#FF00FF", "#FFFF00", "#F0F0F0"];
  constant DOTS_RYBWGK (line 8) | const DOTS_RYBWGK  = ["#00AE42", "#111111", "#DC143C", "#FFE600", "#0064...
  constant DOTS_CMYWGK (line 9) | const DOTS_CMYWGK  = ["#00AE42", "#111111", "#00FFFF", "#FF00FF", "#FFFF...
  constant DOTS_8COLOR (line 10) | const DOTS_8COLOR  = ["#C12E1F", "#FFFF00", "#0064F0", "#FF00FF", "#00FF...
  constant DOTS_BW (line 11) | const DOTS_BW      = ["#F0F0F0", "#111111"];
  constant DOTS_5COLOR (line 12) | const DOTS_5COLOR  = ["#DC143C", "#FFE600", "#0064F0", "#F0F0F0", "#1111...
  type BadgeInfo (line 14) | interface BadgeInfo {
  function resolveBadge (line 19) | function resolveBadge(mode: string): BadgeInfo {
  function Dot (line 32) | function Dot({ color }: { color: string }) {
  function RainbowDot (line 44) | function RainbowDot() {
  function ColorModeBadge (line 56) | function ColorModeBadge({ mode }: ColorModeBadgeProps) {

FILE: frontend/src/components/ui/CropModal.tsx
  type CropData (line 11) | interface CropData {
  type CropModalProps (line 18) | interface CropModalProps {
  type AspectRatioPreset (line 29) | interface AspectRatioPreset {
  constant ASPECT_RATIO_PRESETS (line 34) | const ASPECT_RATIO_PRESETS: AspectRatioPreset[] = [
  function CropModal (line 46) | function CropModal({

FILE: frontend/src/components/ui/Dropdown.tsx
  type DropdownProps (line 3) | interface DropdownProps {
  function Dropdown (line 12) | function Dropdown({

FILE: frontend/src/components/ui/FullScreenModal.tsx
  type FullScreenModalProps (line 10) | interface FullScreenModalProps {
  function FullScreenModal (line 17) | function FullScreenModal({ open, title, onClose, children }: FullScreenM...

FILE: frontend/src/components/ui/ImageUpload.tsx
  type ImageUploadProps (line 4) | interface ImageUploadProps {
  function ImageUpload (line 10) | function ImageUpload({

FILE: frontend/src/components/ui/RadioGroup.tsx
  type RadioGroupProps (line 3) | interface RadioGroupProps {
  function RadioGroup (line 11) | function RadioGroup({

FILE: frontend/src/components/ui/Slider.tsx
  type SliderProps (line 3) | interface SliderProps {
  function Slider (line 14) | function Slider({

FILE: frontend/src/components/ui/ZoomableImage.tsx
  function clampScale (line 5) | function clampScale(value: number): number {
  function computeZoomTranslate (line 14) | function computeZoomTranslate(
  type ZoomableImageProps (line 27) | interface ZoomableImageProps {
  function ZoomableImage (line 33) | function ZoomableImage({ src, alt, className }: ZoomableImageProps) {

FILE: frontend/src/components/widget/ActionBarWidgetContent.tsx
  function ActionBarWidgetContent (line 8) | function ActionBarWidgetContent() {

FILE: frontend/src/components/widget/AdvancedSettingsWidgetContent.tsx
  function AdvancedSettingsWidgetContent (line 8) | function AdvancedSettingsWidgetContent() {

FILE: frontend/src/components/widget/BasicSettingsWidgetContent.tsx
  function BasicSettingsWidgetContent (line 8) | function BasicSettingsWidgetContent() {

FILE: frontend/src/components/widget/CalibrationWidgetContent.tsx
  function CalibrationWidgetContent (line 8) | function CalibrationWidgetContent() {

FILE: frontend/src/components/widget/CloisonneSettingsWidgetContent.tsx
  function CloisonneSettingsWidgetContent (line 8) | function CloisonneSettingsWidgetContent() {

FILE: frontend/src/components/widget/CoatingSettingsWidgetContent.tsx
  function CoatingSettingsWidgetContent (line 8) | function CoatingSettingsWidgetContent() {

FILE: frontend/src/components/widget/ColorWorkstation.tsx
  constant TITLE_BAR_HEIGHT (line 19) | const TITLE_BAR_HEIGHT = 32;
  function ChevronUp (line 22) | function ChevronUp() {
  function ChevronDown (line 31) | function ChevronDown() {
  function ColorWorkstation (line 39) | function ColorWorkstation() {

FILE: frontend/src/components/widget/ExtractorWidgetContent.tsx
  function ExtractorWidgetContent (line 8) | function ExtractorWidgetContent() {

FILE: frontend/src/components/widget/FiveColorWidgetContent.tsx
  function FiveColorWidgetContent (line 8) | function FiveColorWidgetContent() {

FILE: frontend/src/components/widget/KeychainLoopWidgetContent.tsx
  function KeychainLoopWidgetContent (line 8) | function KeychainLoopWidgetContent() {

FILE: frontend/src/components/widget/LutManagerWidgetContent.tsx
  function LutManagerWidgetContent (line 8) | function LutManagerWidgetContent() {

FILE: frontend/src/components/widget/OutlineSettingsWidgetContent.tsx
  function OutlineSettingsWidgetContent (line 8) | function OutlineSettingsWidgetContent() {

FILE: frontend/src/components/widget/ReliefSettingsWidgetContent.tsx
  function ReliefSettingsWidgetContent (line 8) | function ReliefSettingsWidgetContent() {

FILE: frontend/src/components/widget/SnapGuides.tsx
  type SnapGuidesProps (line 12) | interface SnapGuidesProps {
  function SnapGuides (line 18) | function SnapGuides({ isDraggingRef, dragPositionRef, containerRef }: Sn...

FILE: frontend/src/components/widget/TabNavBar.tsx
  type TabNavBarProps (line 8) | interface TabNavBarProps {
  constant TAB_LIST (line 14) | const TAB_LIST: { id: TabId; titleKey: string }[] = [
  function TabNavBar (line 22) | function TabNavBar({ activeTab, modalTab, onTabChange }: TabNavBarProps) {

FILE: frontend/src/components/widget/WidgetHeader.tsx
  type WidgetHeaderProps (line 10) | interface WidgetHeaderProps {
  function WidgetHeader (line 19) | function WidgetHeader({

FILE: frontend/src/components/widget/WidgetPanel.tsx
  type ErrorBoundaryProps (line 24) | interface ErrorBoundaryProps {
  type ErrorBoundaryState (line 29) | interface ErrorBoundaryState {
  class WidgetErrorBoundary (line 33) | class WidgetErrorBoundary extends Component<ErrorBoundaryProps, ErrorBou...
    method getDerivedStateFromError (line 36) | static getDerivedStateFromError(): ErrorBoundaryState {
    method render (line 40) | render() {
  function WidgetErrorFallback (line 50) | function WidgetErrorFallback({ onRetry }: { onRetry: () => void }) {
  type WidgetPanelProps (line 67) | interface WidgetPanelProps {
  constant TRANSITION_CONFIG (line 80) | const TRANSITION_CONFIG = {

FILE: frontend/src/components/widget/WidgetWorkspace.tsx
  constant WIDGET_CONTENT_MAP (line 52) | const WIDGET_CONTENT_MAP: Record<WidgetId, ComponentType> = {
  type WidgetWorkspaceProps (line 67) | interface WidgetWorkspaceProps {
  function WidgetWorkspace (line 71) | function WidgetWorkspace({ children }: WidgetWorkspaceProps) {

FILE: frontend/src/hooks/useActiveModelUrl.ts
  function useActiveModelUrl (line 11) | function useActiveModelUrl(

FILE: frontend/src/hooks/useAutoPreview.ts
  function useAutoPreview (line 12) | function useAutoPreview(): void {

FILE: frontend/src/hooks/useConverterDataInit.ts
  function useConverterDataInit (line 11) | function useConverterDataInit() {

FILE: frontend/src/hooks/useThemeConfig.ts
  function useThemeConfig (line 14) | function useThemeConfig(): ThemeColors {

FILE: frontend/src/i18n/context.tsx
  type I18nContextValue (line 6) | interface I18nContextValue {
  function I18nProvider (line 16) | function I18nProvider({ children }: { children: ReactNode }) {
  function useI18n (line 34) | function useI18n(): I18nContextValue {

FILE: frontend/src/setupTests.ts
  method observe (line 7) | observe() {}
  method unobserve (line 8) | unobserve() {}
  method disconnect (line 9) | disconnect() {}

FILE: frontend/src/stores/__tests__/batchStore.property.test.ts
  constant VALID_MIME_TYPES (line 8) | const VALID_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml"] as...
  constant INVALID_MIME_TYPES (line 10) | const INVALID_MIME_TYPES = [
  function resetStore (line 60) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/batchStore.test.ts
  function resetStore (line 28) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/converterStore.property.test.ts
  type ApplyOp (line 33) | type ApplyOp = { type: 'apply'; origHex: string; newHex: string };
  type UndoOp (line 34) | type UndoOp = { type: 'undo' };
  type Op (line 35) | type Op = ApplyOp | UndoOp;
  function resetStore (line 56) | function resetStore(): void {
  function simulateOps (line 72) | function simulateOps(ops: Op[]): Record<string, string> {

FILE: frontend/src/stores/__tests__/converterStore.test.ts
  function resetStore (line 9) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/cropStore.property.test.ts
  class MockImage (line 19) | class MockImage {
  method length (line 34) | get length() { return localStorageMap.size; }
  function resetStore (line 41) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/cropStore.test.ts
  function resetStore (line 31) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/slicerStore.property.test.ts
  function resetStore (line 22) | function resetStore(): void {

FILE: frontend/src/stores/__tests__/slicerStore.test.ts
  constant MOCK_SLICERS (line 23) | const MOCK_SLICERS: SlicerInfo[] = [
  function resetStore (line 28) | function resetStore(): void {

FILE: frontend/src/stores/aboutStore.ts
  type AboutState (line 6) | interface AboutState {
  type AboutActions (line 13) | interface AboutActions {
  constant DEFAULT_STATE (line 20) | const DEFAULT_STATE: AboutState = {

FILE: frontend/src/stores/calibrationStore.ts
  type CalibrationState (line 12) | interface CalibrationState {
  type CalibrationActions (line 27) | interface CalibrationActions {
  constant DEFAULT_STATE (line 39) | const DEFAULT_STATE: CalibrationState = {

FILE: frontend/src/stores/converterStore.ts
  function clampValue (line 37) | function clampValue(value: number, min: number, max: number): number {
  constant VALID_IMAGE_TYPES (line 41) | const VALID_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/svg...
  function isValidImageType (line 43) | function isValidImageType(mimeType: string): boolean {
  type ConverterState (line 49) | interface ConverterState {
  type ConverterActions (line 178) | interface ConverterActions {
  function loadEnableCrop (line 276) | function loadEnableCrop(): boolean {
  function loadLutName (line 286) | function loadLutName(): string {
  constant DEFAULT_STATE (line 296) | const DEFAULT_STATE: ConverterState = {

FILE: frontend/src/stores/extractorStore.ts
  type ExtractorState (line 12) | interface ExtractorState {
  type ExtractorActions (line 61) | interface ExtractorActions {
  constant DEFAULT_STATE (line 82) | const DEFAULT_STATE: ExtractorState = {

FILE: frontend/src/stores/fiveColorStore.ts
  type FiveColorState (line 7) | interface FiveColorState {
  type FiveColorActions (line 18) | interface FiveColorActions {
  constant DEFAULT_STATE (line 30) | const DEFAULT_STATE: FiveColorState = {

FILE: frontend/src/stores/lutManagerStore.ts
  constant ALLOWED_SECONDARY_MODES (line 19) | const ALLOWED_SECONDARY_MODES: Record<string, string[]> = {
  function matchesMode (line 25) | function matchesMode(listColorMode: string, shortMode: string): boolean {
  function filterSecondaryOptions (line 33) | function filterSecondaryOptions(
  type LutManagerState (line 57) | interface LutManagerState {
  type LutManagerActions (line 72) | interface LutManagerActions {
  constant DEFAULT_STATE (line 84) | const DEFAULT_STATE: LutManagerState = {

FILE: frontend/src/stores/settingsStore.ts
  type SettingsState (line 7) | interface SettingsState {
  type SettingsActions (line 21) | interface SettingsActions {
  constant DEFAULT_SETTINGS (line 36) | const DEFAULT_SETTINGS: SettingsState = {

FILE: frontend/src/stores/slicerStore.ts
  type SlicerState (line 11) | interface SlicerState {
  type SlicerActions (line 22) | interface SlicerActions {
  constant DEFAULT_STATE (line 31) | const DEFAULT_STATE: SlicerState = {

FILE: frontend/src/stores/widgetStore.ts
  constant TAB_WIDGET_MAP (line 24) | const TAB_WIDGET_MAP: Record<TabId, WidgetId[]> = {
  constant DEFAULT_LAYOUT (line 43) | const DEFAULT_LAYOUT: Record<WidgetId, WidgetLayoutState> = {
  constant WIDGET_REGISTRY (line 158) | const WIDGET_REGISTRY: Omit<WidgetConfig, "component">[] = [

FILE: frontend/src/types/widget.ts
  type TabId (line 7) | type TabId = 'converter' | 'calibration' | 'extractor' | 'lut-manager' |...
  type WidgetId (line 10) | type WidgetId =
  type WidgetLayoutState (line 25) | interface WidgetLayoutState {
  type WidgetStore (line 36) | interface WidgetStore {
  type SnapResult (line 65) | interface SnapResult {
  type WidgetConfig (line 72) | interface WidgetConfig {
  type PersistedWidgetState (line 82) | interface PersistedWidgetState {

FILE: frontend/src/utils/colorUtils.ts
  function hexToLuminance (line 8) | function hexToLuminance(hex: string): number {
  function computeAutoHeightMap (line 23) | function computeAutoHeightMap(
  function colorRemapToReplacementRegions (line 54) | function colorRemapToReplacementRegions(
  function hexToRgb (line 84) | function hexToRgb(hex: string): [number, number, number] {
  function rgbEuclideanDistance (line 101) | function rgbEuclideanDistance(
  function sortByColorDistance (line 119) | function sortByColorDistance(

FILE: frontend/src/utils/scaleUtils.ts
  type ScaleFactor (line 6) | interface ScaleFactor {
  function computeScaleFactor (line 24) | function computeScaleFactor(
  function computeThicknessScale (line 56) | function computeThicknessScale(

FILE: frontend/src/utils/widgetUtils.ts
  constant WIDGET_WIDTH (line 10) | const WIDGET_WIDTH = 350;
  constant COLLAPSED_HEIGHT (line 13) | const COLLAPSED_HEIGHT = 40;
  constant EXPANDED_HEIGHT (line 16) | const EXPANDED_HEIGHT = 400;
  constant SNAP_THRESHOLD (line 19) | const SNAP_THRESHOLD = 48;
  constant STACK_GAP (line 22) | const STACK_GAP = 8;
  function clampPosition (line 38) | function clampPosition(
  function computeSnap (line 71) | function computeSnap(
  function computeStackPositions (line 110) | function computeStackPositions(

FILE: main.py
  function patch_asscalar (line 24) | def patch_asscalar(a):
  class _Tee (line 33) | class _Tee:
    method __init__ (line 34) | def __init__(self, log_path, console_stream=None, lock=None):
    method write (line 41) | def write(self, msg):
    method flush (line 61) | def flush(self):
    method __getattr__ (line 69) | def __getattr__(self, name):
  class _TeeStderr (line 75) | class _TeeStderr:
    method __init__ (line 76) | def __init__(self, log_file, lock):
    method write (line 83) | def write(self, msg):
    method flush (line 103) | def flush(self):
    method __getattr__ (line 111) | def __getattr__(self, name):
  function init_runtime_log (line 117) | def init_runtime_log():
  function find_available_port (line 167) | def find_available_port(start_port=7860, max_attempts=1000):
  function start_browser (line 177) | def start_browser(port):
  function get_platform_head_js (line 182) | def get_platform_head_js() -> str:
  function get_server_host (line 222) | def get_server_host() -> str:
  function _graceful_shutdown (line 231) | def _graceful_shutdown(signum, frame):

FILE: tests/test_5color_merge_properties.py
  function _make_join_redirector (line 29) | def _make_join_redirector(assets_dir: str):
  function lut_array_strategy (line 44) | def lut_array_strategy(
  class TestLUTMergeShapeInvariant (line 64) | class TestLUTMergeShapeInvariant:
    method test_merge_shape_is_sum (line 78) | def test_merge_shape_is_sum(self, n: int, m: int) -> None:
    method test_merge_preserves_all_values (line 92) | def test_merge_preserves_all_values(self, n: int, m: int) -> None:
    method test_merge_via_endpoint_shape (line 107) | def test_merge_via_endpoint_shape(self, n: int, m: int) -> None:
  class TestExtractTempFilePath (line 139) | class TestExtractTempFilePath:
    method test_temp_file_path_matches_page (line 151) | def test_temp_file_path_matches_page(self, page: str) -> None:
    method test_page1_and_page2_produce_distinct_files (line 167) | def test_page1_and_page2_produce_distinct_files(self, page: str) -> None:
    method test_extract_endpoint_saves_temp_file (line 182) | def test_extract_endpoint_saves_temp_file(self, page: str, n: int) -> ...

FILE: tests/test_5color_merge_unit.py
  function _make_join_redirector (line 34) | def _make_join_redirector(assets_dir: str):
  class TestMerge5ColorExtendedSuccess (line 50) | class TestMerge5ColorExtendedSuccess:
    method test_merge_success_returns_200_with_extract_response (line 53) | def test_merge_success_returns_200_with_extract_response(self) -> None:
    method test_merge_produces_correct_shape (line 76) | def test_merge_produces_correct_shape(self) -> None:
  class TestMerge5ColorExtendedMissingFiles (line 105) | class TestMerge5ColorExtendedMissingFiles:
    method test_missing_both_pages_returns_400 (line 108) | def test_missing_both_pages_returns_400(self) -> None:
    method test_missing_page2_returns_400 (line 120) | def test_missing_page2_returns_400(self) -> None:
    method test_missing_page1_returns_400 (line 136) | def test_missing_page1_returns_400(self) -> None:
  class TestCalibration5ColorExtended (line 159) | class TestCalibration5ColorExtended:
    method test_5color_extended_routes_to_generate_5color_extended_batch_zip (line 162) | def test_5color_extended_routes_to_generate_5color_extended_batch_zip(...
    method test_5color_extended_response_contains_download_and_preview_urls (line 181) | def test_5color_extended_response_contains_download_and_preview_urls(s...
    method test_5color_extended_core_error_returns_500 (line 203) | def test_5color_extended_core_error_returns_500(self) -> None:

FILE: tests/test_api_app_unit.py
  function client (line 39) | def client() -> TestClient:
  class TestSwaggerUI (line 51) | class TestSwaggerUI:
    method test_docs_returns_200 (line 56) | def test_docs_returns_200(self, client: TestClient) -> None:
  class TestOpenAPIJSON (line 71) | class TestOpenAPIJSON:
    method test_openapi_json_returns_200 (line 76) | def test_openapi_json_returns_200(self, client: TestClient) -> None:
    method test_openapi_json_contains_all_endpoints (line 85) | def test_openapi_json_contains_all_endpoints(self, client: TestClient)...
    method test_openapi_json_has_correct_title (line 101) | def test_openapi_json_has_correct_title(self, client: TestClient) -> N...
    method test_openapi_json_has_correct_version (line 112) | def test_openapi_json_has_correct_version(self, client: TestClient) ->...
  class TestCORSHeaders (line 129) | class TestCORSHeaders:
    method test_cors_allows_any_origin (line 136) | def test_cors_allows_any_origin(self, client: TestClient) -> None:
    method test_cors_allows_all_methods (line 158) | def test_cors_allows_all_methods(self, client: TestClient) -> None:
    method test_cors_allows_all_headers (line 172) | def test_cors_allows_all_headers(self, client: TestClient) -> None:
    method test_cors_allows_credentials (line 187) | def test_cors_allows_credentials(self, client: TestClient) -> None:

FILE: tests/test_api_routers_unit.py
  function client (line 52) | def client() -> TestClient:
  class TestConverterEndpoints (line 64) | class TestConverterEndpoints:
    method test_post_preview_missing_lut_returns_404 (line 69) | def test_post_preview_missing_lut_returns_404(self, client: TestClient...
    method test_post_preview_missing_image_returns_422 (line 85) | def test_post_preview_missing_image_returns_422(self, client: TestClie...
    method test_post_generate_missing_session_returns_422 (line 93) | def test_post_generate_missing_session_returns_422(self, client: TestC...
    method test_post_generate_unknown_session_returns_404 (line 99) | def test_post_generate_unknown_session_returns_404(self, client: TestC...
    method test_post_batch (line 108) | def test_post_batch(self, client: TestClient) -> None:
    method test_post_replace_color (line 115) | def test_post_replace_color(self, client: TestClient) -> None:
    method test_post_merge_colors (line 125) | def test_post_merge_colors(self, client: TestClient) -> None:
  class TestExtractorEndpoints (line 132) | class TestExtractorEndpoints:
    method test_post_extract (line 137) | def test_post_extract(self, client: TestClient) -> None:
    method test_post_manual_fix (line 147) | def test_post_manual_fix(self, client: TestClient) -> None:
  class TestCalibrationEndpoints (line 159) | class TestCalibrationEndpoints:
    method test_post_generate (line 164) | def test_post_generate(self, client: TestClient) -> None:
  function st_convert_preview_request (line 192) | def st_convert_preview_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_convert_generate_request (line 207) | def st_convert_generate_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_convert_batch_request (line 238) | def st_convert_batch_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_color_replace_request (line 245) | def st_color_replace_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_color_merge_preview_request (line 255) | def st_color_merge_preview_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_extractor_extract_request (line 266) | def st_extractor_extract_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_extractor_manual_fix_request (line 287) | def st_extractor_manual_fix_request(draw: st.DrawFn) -> Dict[str, Any]:
  function st_calibration_generate_request (line 300) | def st_calibration_generate_request(draw: st.DrawFn) -> Dict[str, Any]:
  function test_all_endpoints_return_stub_response (line 337) | def test_all_endpoints_return_stub_response(data: st.DataObject) -> None:

FILE: tests/test_api_schemas_properties.py
  function st_convert_preview_request (line 64) | def st_convert_preview_request(draw: st.DrawFn) -> ConvertPreviewRequest:
  function st_color_replacement_item (line 79) | def st_color_replacement_item(draw: st.DrawFn) -> ColorReplacementItem:
  function st_convert_generate_request (line 89) | def st_convert_generate_request(draw: st.DrawFn) -> ConvertGenerateRequest:
  function st_convert_batch_request (line 133) | def st_convert_batch_request(draw: st.DrawFn) -> ConvertBatchRequest:
  function st_color_replace_request (line 139) | def st_color_replace_request(draw: st.DrawFn) -> ColorReplaceRequest:
  function st_color_merge_preview_request (line 149) | def st_color_merge_preview_request(draw: st.DrawFn) -> ColorMergePreview...
  function st_extractor_extract_request (line 160) | def st_extractor_extract_request(draw: st.DrawFn) -> ExtractorExtractReq...
  function st_extractor_manual_fix_request (line 181) | def st_extractor_manual_fix_request(draw: st.DrawFn) -> ExtractorManualF...
  function st_calibration_generate_request (line 194) | def st_calibration_generate_request(draw: st.DrawFn) -> CalibrationGener...
  function test_schema_serialization_round_trip (line 230) | def test_schema_serialization_round_trip(instance: BaseModel) -> None:
  function test_valid_data_passes_validation (line 254) | def test_valid_data_passes_validation(instance: BaseModel) -> None:
  function _build_minimal_valid_data (line 303) | def _build_minimal_valid_data(model_cls: Type[BaseModel]) -> Dict[str, A...
  function test_out_of_range_values_rejected (line 323) | def test_out_of_range_values_rejected(data: st.DataObject) -> None:
  function test_enum_field_string_serialization (line 387) | def test_enum_field_string_serialization(instance: BaseModel) -> None:
  function test_optional_field_default_values (line 463) | def test_optional_field_default_values(data: st.DataObject) -> None:

FILE: tests/test_bed_size_properties.py
  function test_compute_scale_equals_target_over_max (line 22) | def test_compute_scale_equals_target_over_max(width_mm: int, height_mm: ...
  function test_invalid_bed_label_returns_fallback (line 46) | def test_invalid_bed_label_returns_fallback(label: str) -> None:

FILE: tests/test_calibration_integration_unit.py
  class TestColorModeRouting (line 31) | class TestColorModeRouting:
    method test_bw_mode_routes_to_generate_bw (line 34) | def test_bw_mode_routes_to_generate_bw(self) -> None:
    method test_four_color_mode_routes_to_generate_calibration_board (line 55) | def test_four_color_mode_routes_to_generate_calibration_board(self) ->...
    method test_six_color_mode_routes_to_generate_smart_board (line 77) | def test_six_color_mode_routes_to_generate_smart_board(self) -> None:
    method test_eight_color_mode_routes_to_generate_8color_batch_zip (line 97) | def test_eight_color_mode_routes_to_generate_8color_batch_zip(self) ->...
  class TestParameterMapping (line 120) | class TestParameterMapping:
    method test_block_size_and_gap_mapped_correctly (line 123) | def test_block_size_and_gap_mapped_correctly(self) -> None:
  class TestErrorHandling (line 153) | class TestErrorHandling:
    method test_core_exception_returns_500 (line 156) | def test_core_exception_returns_500(self) -> None:

FILE: tests/test_calibration_routing_properties.py
  function test_all_color_modes_route_to_valid_core_function (line 70) | def test_all_color_modes_route_to_valid_core_function(
  function test_block_size_and_gap_do_not_cause_parameter_errors (line 110) | def test_block_size_and_gap_do_not_cause_parameter_errors(
  function test_enum_coverage_is_exhaustive (line 164) | def test_enum_coverage_is_exhaustive() -> None:

FILE: tests/test_cleanup_output_dir_properties.py
  function _filename_with_ext (line 42) | def _filename_with_ext(ext_strategy: st.SearchStrategy[str]) -> st.Searc...
  function test_cleanup_output_dir_only_deletes_cleanable_extensions (line 67) | def test_cleanup_output_dir_only_deletes_cleanable_extensions(

FILE: tests/test_clear_cache_response_properties.py
  function test_clear_cache_response_json_round_trip (line 46) | def test_clear_cache_response_json_round_trip(

FILE: tests/test_color_merge_map_properties.py
  function _rgb_to_lab (line 29) | def _rgb_to_lab(rgb_array: np.ndarray) -> np.ndarray:
  function st_palette (line 41) | def st_palette(draw: st.DrawFn) -> List[dict]:
  class TestMergeMapConsistency (line 82) | class TestMergeMapConsistency:
    method test_merge_map_invariants (line 93) | def test_merge_map_invariants(

FILE: tests/test_color_merge_unit.py
  function setup_module (line 25) | def setup_module(module):
  function teardown_module (line 33) | def teardown_module(module):
  function _create_session_with_preview (line 47) | def _create_session_with_preview() -> str:
  class TestMergeSessionNotFound (line 68) | class TestMergeSessionNotFound:
    method test_merge_colors_unknown_session_returns_404 (line 71) | def test_merge_colors_unknown_session_returns_404(self) -> None:
  class TestMergeNoPreviewCacheReturns409 (line 89) | class TestMergeNoPreviewCacheReturns409:
    method test_merge_colors_no_cache_returns_409 (line 92) | def test_merge_colors_no_cache_returns_409(self) -> None:
  class TestMergeDisabled (line 113) | class TestMergeDisabled:
    method test_merge_disabled_returns_empty_map_and_perfect_quality (line 116) | def test_merge_disabled_returns_empty_map_and_perfect_quality(self) ->...
  class TestSuccessfulMerge (line 140) | class TestSuccessfulMerge:
    method _create_session_with_mergeable_colors (line 143) | def _create_session_with_mergeable_colors(self) -> str:
    method test_merge_response_has_required_fields (line 159) | def test_merge_response_has_required_fields(self) -> None:
  class TestMergeMapStoredInSession (line 188) | class TestMergeMapStoredInSession:
    method test_merge_map_saved_to_session (line 191) | def test_merge_map_saved_to_session(self) -> None:
    method test_merge_disabled_stores_empty_map (line 207) | def test_merge_disabled_stores_empty_map(self) -> None:

FILE: tests/test_color_replace_unit.py
  function setup_module (line 24) | def setup_module(module):
  function teardown_module (line 32) | def teardown_module(module):
  function _create_session_with_preview (line 46) | def _create_session_with_preview() -> str:
  class TestSessionNotFound (line 65) | class TestSessionNotFound:
    method test_replace_color_unknown_session_returns_404 (line 68) | def test_replace_color_unknown_session_returns_404(self) -> None:
  class TestNoPreviewCacheReturns409 (line 85) | class TestNoPreviewCacheReturns409:
    method test_replace_color_no_cache_returns_409 (line 88) | def test_replace_color_no_cache_returns_409(self) -> None:
  class TestSuccessfulReplacement (line 108) | class TestSuccessfulReplacement:
    method test_replace_color_returns_ok_response (line 111) | def test_replace_color_returns_ok_response(self) -> None:
    method test_replacement_regions_updated_in_session (line 127) | def test_replacement_regions_updated_in_session(self) -> None:
  class TestReplacementHistory (line 149) | class TestReplacementHistory:
    method test_history_snapshot_saved_before_change (line 152) | def test_history_snapshot_saved_before_change(self) -> None:

FILE: tests/test_converter_batch_unit.py
  class _MockWorkerPool (line 34) | class _MockWorkerPool:
    method __init__ (line 39) | def __init__(self) -> None:
    method is_alive (line 43) | def is_alive(self) -> bool:
    method max_workers (line 47) | def max_workers(self) -> int:
  function setup_module (line 53) | def setup_module(module):
  function teardown_module (line 62) | def teardown_module(module):
  function _make_test_image_buf (line 77) | def _make_test_image_buf(name: str = "test.png") -> tuple[str, io.BytesI...
  function _make_fake_3mf (line 86) | def _make_fake_3mf(suffix: str = ".3mf") -> str:
  class TestBatchLutNotFound (line 99) | class TestBatchLutNotFound:
    method test_batch_unknown_lut_returns_404 (line 102) | def test_batch_unknown_lut_returns_404(self) -> None:
  class TestBatchPartialFailure (line 121) | class TestBatchPartialFailure:
    method test_batch_partial_failure_continues (line 124) | def test_batch_partial_failure_continues(self) -> None:
  class TestBatchAllSuccess (line 176) | class TestBatchAllSuccess:
    method test_batch_all_success (line 179) | def test_batch_all_success(self) -> None:
  class TestBatchAllFailed (line 216) | class TestBatchAllFailed:
    method test_batch_all_fail (line 219) | def test_batch_all_fail(self) -> None:
  class TestBatchTimeout (line 250) | class TestBatchTimeout:
    method test_batch_timeout_per_item (line 253) | def test_batch_timeout_per_item(self) -> None:
  class TestBatchWorkerPoolSubmit (line 283) | class TestBatchWorkerPoolSubmit:
    method test_batch_submits_to_pool (line 286) | def test_batch_submits_to_pool(self) -> None:

FILE: tests/test_converter_generate_unit.py
  function setup_module (line 32) | def setup_module(module):
  function teardown_module (line 41) | def teardown_module(module):
  function _create_session_with_preview_and_files (line 57) | def _create_session_with_preview_and_files(store: SessionStore) -> str:
  class TestGenerateSessionNotFound (line 73) | class TestGenerateSessionNotFound:
    method test_generate_unknown_session_returns_404 (line 76) | def test_generate_unknown_session_returns_404(self) -> None:
  class TestGenerateNoPreviewCache (line 91) | class TestGenerateNoPreviewCache:
    method test_generate_no_preview_cache_returns_409 (line 94) | def test_generate_no_preview_cache_returns_409(self) -> None:
  class TestGenerateMissingImagePath (line 110) | class TestGenerateMissingImagePath:
    method test_generate_missing_image_path_returns_409 (line 113) | def test_generate_missing_image_path_returns_409(self) -> None:
    method test_generate_nonexistent_image_file_returns_409 (line 124) | def test_generate_nonexistent_image_file_returns_409(self) -> None:
  class TestGenerateParameterCompleteness (line 142) | class TestGenerateParameterCompleteness:
    method test_generate_passes_all_advanced_params (line 145) | def test_generate_passes_all_advanced_params(self) -> None:
    method test_generate_passes_replacement_regions_from_request (line 258) | def test_generate_passes_replacement_regions_from_request(self) -> None:
  class TestGenerateTimeout (line 300) | class TestGenerateTimeout:
    method test_generate_timeout_returns_504 (line 303) | def test_generate_timeout_returns_504(self) -> None:
  class TestGenerateWorkerException (line 325) | class TestGenerateWorkerException:
    method test_generate_worker_exception_returns_500 (line 328) | def test_generate_worker_exception_returns_500(self) -> None:

FILE: tests/test_converter_preview_unit.py
  function setup_module (line 37) | def setup_module(module):
  function teardown_module (line 46) | def teardown_module(module):
  function _create_worker_result_files (line 85) | def _create_worker_result_files() -> dict:
  function _make_test_image_buf (line 110) | def _make_test_image_buf() -> io.BytesIO:
  class TestLutNotFoundReturns404 (line 124) | class TestLutNotFoundReturns404:
    method test_preview_unknown_lut_returns_404 (line 127) | def test_preview_unknown_lut_returns_404(self) -> None:
  class TestSessionContainsPreviewCache (line 151) | class TestSessionContainsPreviewCache:
    method test_preview_stores_cache_in_session (line 154) | def test_preview_stores_cache_in_session(self) -> None:
  class TestResponseContainsPaletteAndDimensions (line 198) | class TestResponseContainsPaletteAndDimensions:
    method test_preview_response_has_palette_and_dimensions (line 201) | def test_preview_response_has_palette_and_dimensions(self) -> None:
  class TestTimeoutReturns504 (line 257) | class TestTimeoutReturns504:
    method test_preview_timeout_returns_504 (line 260) | def test_preview_timeout_returns_504(self) -> None:
  class TestGeneralExceptionReturns500 (line 291) | class TestGeneralExceptionReturns500:
    method test_preview_exception_returns_500 (line 294) | def test_preview_exception_returns_500(self) -> None:

FILE: tests/test_converter_vector_export_unit.py
  function _build_fake_processor (line 34) | def _build_fake_processor(scene):
  class TestVectorBranchExport (line 47) | class TestVectorBranchExport:
    method test_bambu_export_called (line 53) | def test_bambu_export_called(self, MockVP, mock_bambu_export, tmp_path):
    method test_slot_names_match_scene_geometry (line 95) | def test_slot_names_match_scene_geometry(self, MockVP, mock_bambu_expo...
    method test_empty_scene_returns_error (line 138) | def test_empty_scene_returns_error(self, MockVP, mock_bambu_export, tm...
  class TestBambuExportGuardrails (line 178) | class TestBambuExportGuardrails:
    method test_export_scene_raises_on_missing_slot_geometry (line 180) | def test_export_scene_raises_on_missing_slot_geometry(self, tmp_path):
    method test_writer_add_mesh_rejects_empty_mesh (line 195) | def test_writer_add_mesh_rejects_empty_mesh(self, tmp_path):

FILE: tests/test_converter_workers_unit.py
  class TestWorkerImportability (line 33) | class TestWorkerImportability:
    method test_worker_generate_preview_is_importable (line 36) | def test_worker_generate_preview_is_importable(self) -> None:
    method test_worker_generate_model_is_importable (line 40) | def test_worker_generate_model_is_importable(self) -> None:
    method test_worker_generate_preview_is_function (line 44) | def test_worker_generate_preview_is_function(self) -> None:
    method test_worker_generate_model_is_function (line 48) | def test_worker_generate_model_is_function(self) -> None:
  class TestWorkerPicklability (line 57) | class TestWorkerPicklability:
    method test_worker_generate_preview_is_picklable (line 60) | def test_worker_generate_preview_is_picklable(self) -> None:
    method test_worker_generate_model_is_picklable (line 65) | def test_worker_generate_model_is_picklable(self) -> None:
  class TestParameterAnnotations (line 75) | class TestParameterAnnotations:
    method _get_param_annotations (line 79) | def _get_param_annotations(fn) -> dict[str, type]:
    method test_preview_params_are_serializable (line 88) | def test_preview_params_are_serializable(self) -> None:
    method test_model_params_are_serializable (line 97) | def test_model_params_are_serializable(self) -> None:
    method test_preview_has_expected_param_count (line 106) | def test_preview_has_expected_param_count(self) -> None:
    method test_model_has_expected_param_count (line 111) | def test_model_has_expected_param_count(self) -> None:
    method test_preview_param_names (line 116) | def test_preview_param_names(self) -> None:
    method test_model_param_names (line 126) | def test_model_param_names(self) -> None:
  class TestReturnAnnotations (line 137) | class TestReturnAnnotations:
    method test_preview_returns_dict (line 140) | def test_preview_returns_dict(self) -> None:
    method test_model_returns_dict (line 145) | def test_model_returns_dict(self) -> None:
  class TestTopLevelFunctions (line 155) | class TestTopLevelFunctions:
    method test_preview_has_no_self_param (line 158) | def test_preview_has_no_self_param(self) -> None:
    method test_model_has_no_self_param (line 164) | def test_model_has_no_self_param(self) -> None:
    method test_preview_qualname_is_module_level (line 170) | def test_preview_qualname_is_module_level(self) -> None:
    method test_model_qualname_is_module_level (line 174) | def test_model_qualname_is_module_level(self) -> None:

FILE: tests/test_crop_properties.py
  function test_clamp_boundary_invariant (line 51) | def test_clamp_boundary_invariant(
  function _make_test_png (line 102) | def _make_test_png(width: int = 100, height: int = 80) -> io.BytesIO:
  function test_crop_endpoint_response_completeness (line 119) | def test_crop_endpoint_response_completeness(

FILE: tests/test_crop_unit.py
  function _make_rgb_png (line 29) | def _make_rgb_png(width: int = 200, height: int = 150) -> io.BytesIO:
  function _make_text_file (line 41) | def _make_text_file() -> io.BytesIO:
  class TestNormalCropFlow (line 53) | class TestNormalCropFlow:
    method test_crop_valid_image_returns_200 (line 56) | def test_crop_valid_image_returns_200(self) -> None:
  class TestCoordinateClamping (line 79) | class TestCoordinateClamping:
    method test_large_xy_clamped_to_valid_range (line 82) | def test_large_xy_clamped_to_valid_range(self) -> None:
  class TestInvalidFileUpload (line 105) | class TestInvalidFileUpload:
    method test_text_file_returns_422 (line 108) | def test_text_file_returns_422(self) -> None:
  class TestZeroSizeCrop (line 127) | class TestZeroSizeCrop:
    method test_zero_width_returns_422 (line 135) | def test_zero_width_returns_422(self) -> None:

FILE: tests/test_extractor_integration_unit.py
  function _make_test_image_buf (line 30) | def _make_test_image_buf() -> io.BytesIO:
  class TestCornerPointsValidation (line 44) | class TestCornerPointsValidation:
    method test_extract_invalid_corner_points_count_returns_422 (line 47) | def test_extract_invalid_corner_points_count_returns_422(self) -> None:
  class TestSessionStatePersistence (line 70) | class TestSessionStatePersistence:
    method test_extract_stores_session_state (line 73) | def test_extract_stores_session_state(self) -> None:
  class TestFieldNameMapping (line 110) | class TestFieldNameMapping:
    method test_extract_field_name_mapping (line 113) | def test_extract_field_name_mapping(self) -> None:

FILE: tests/test_file_bridge_properties.py
  function test_ndarray_png_roundtrip (line 42) | def test_ndarray_png_roundtrip(arr: np.ndarray) -> None:
  function test_pil_png_roundtrip (line 63) | def test_pil_png_roundtrip(arr: np.ndarray) -> None:

FILE: tests/test_file_registry_clear_properties.py
  function _create_temp_file (line 49) | def _create_temp_file() -> str:
  function test_clear_all_empties_registry_and_returns_entry_count (line 71) | def test_clear_all_empties_registry_and_returns_entry_count(

FILE: tests/test_file_registry_properties.py
  function _create_temp_file (line 49) | def _create_temp_file() -> str:
  function test_register_path_returns_valid_uuid4 (line 67) | def test_register_path_returns_valid_uuid4(sid: str, filename: str) -> N...
  function test_register_resolve_consistency (line 84) | def test_register_resolve_consistency(sid: str, filename: str) -> None:
  function test_resolve_unknown_id_returns_none (line 104) | def test_resolve_unknown_id_returns_none(unknown_id: str) -> None:
  function test_cleanup_session_invalidates_file_ids (line 118) | def test_cleanup_session_invalidates_file_ids(

FILE: tests/test_five_color_api_unit.py
  function _make_npz_data (line 26) | def _make_npz_data():
  function test_get_base_colors_npz (line 43) | def test_get_base_colors_npz():
  function test_get_base_colors_npy (line 71) | def test_get_base_colors_npy():
  function test_get_base_colors_not_found (line 104) | def test_get_base_colors_not_found():
  function test_query_success (line 120) | def test_query_success():
  function test_query_no_match (line 146) | def test_query_no_match():

FILE: tests/test_five_color_query_properties.py
  function test_base_color_hex_rgb_consistency (line 34) | def test_base_color_hex_rgb_consistency(rgb: tuple[int, int, int]) -> None:
  function synthetic_engine_and_row (line 59) | def synthetic_engine_and_row(draw):
  function test_query_round_trip_known_stack_entry (line 87) | def test_query_round_trip_known_stack_entry(data) -> None:
  function engine_with_out_of_range_indices (line 129) | def engine_with_out_of_range_indices(draw):
  function test_out_of_range_index_rejection (line 161) | def test_out_of_range_index_rejection(data) -> None:
  function test_five_color_query_request_rejects_non_5_length (line 208) | def test_five_color_query_request_rejects_non_5_length(length: int, valu...

FILE: tests/test_health_lut_unit.py
  class TestHealthEndpoint (line 23) | class TestHealthEndpoint:
    method test_health_returns_200 (line 26) | def test_health_returns_200(self) -> None:
    method test_health_contains_required_fields (line 30) | def test_health_contains_required_fields(self) -> None:
    method test_health_status_is_ok (line 37) | def test_health_status_is_ok(self) -> None:
    method test_health_version_is_2_0 (line 41) | def test_health_version_is_2_0(self) -> None:
    method test_health_uptime_is_non_negative_float (line 45) | def test_health_uptime_is_non_negative_float(self) -> None:
    method test_health_worker_pool_contains_required_fields (line 51) | def test_health_worker_pool_contains_required_fields(self) -> None:
    method test_health_worker_pool_healthy_is_bool (line 57) | def test_health_worker_pool_healthy_is_bool(self) -> None:
    method test_health_worker_pool_max_workers_is_positive_int (line 61) | def test_health_worker_pool_max_workers_is_positive_int(self) -> None:
  class TestHealthWorkerPoolState (line 73) | class TestHealthWorkerPoolState:
    method _make_client_with_pool (line 78) | def _make_client_with_pool(self, pool: WorkerPoolManager) -> TestClient:
    method teardown_method (line 86) | def teardown_method(self) -> None:
    method test_pool_alive_reports_healthy_true (line 92) | def test_pool_alive_reports_healthy_true(self) -> None:
    method test_pool_not_started_reports_healthy_false (line 105) | def test_pool_not_started_reports_healthy_false(self) -> None:
    method test_pool_shutdown_reports_healthy_false (line 115) | def test_pool_shutdown_reports_healthy_false(self) -> None:
    method test_pool_alive_max_workers_matches (line 126) | def test_pool_alive_max_workers_matches(self) -> None:
    method test_pool_not_started_max_workers_still_positive (line 139) | def test_pool_not_started_max_workers_still_positive(self) -> None:
  class TestLUTListEndpoint (line 157) | class TestLUTListEndpoint:
    method test_lut_list_returns_200 (line 160) | def test_lut_list_returns_200(self) -> None:
    method test_lut_list_contains_required_fields (line 164) | def test_lut_list_contains_required_fields(self) -> None:
    method test_lut_list_luts_is_list (line 168) | def test_lut_list_luts_is_list(self) -> None:
    method test_lut_list_items_have_required_fields (line 172) | def test_lut_list_items_have_required_fields(self) -> None:
    method test_lut_list_names_sorted_alphabetically (line 179) | def test_lut_list_names_sorted_alphabetically(self) -> None:

FILE: tests/test_heic_support_properties.py
  function test_required_format_present (line 28) | def test_required_format_present(ext: str) -> None:
  function test_extension_format_valid (line 42) | def test_extension_format_valid(ext: str) -> None:
  function test_no_duplicate_extensions (line 59) | def test_no_duplicate_extensions() -> None:

FILE: tests/test_heightmap_color_height_properties.py
  function compute_color_height_map (line 25) | def compute_color_height_map(
  function heightmap_color_strategy (line 77) | def heightmap_color_strategy(draw: st.DrawFn):
  function test_heightmap_color_height_bounded (line 146) | def test_heightmap_color_height_bounded(data):

FILE: tests/test_heightmap_properties.py
  function test_grayscale_mapping_formula (line 43) | def test_grayscale_mapping_formula(grayscale_val, max_relief_height, bas...
  function test_height_matrix_shape_and_type (line 94) | def test_height_matrix_shape_and_type(
  function test_voxel_matrix_structure (line 154) | def test_voxel_matrix_structure(size, max_height, backing_color_id):
  function _simulate_branch_selection (line 243) | def _simulate_branch_selection(
  function _expected_branch (line 279) | def _expected_branch(
  function test_decision_matrix_correctness (line 322) | def test_decision_matrix_correctness(
  function test_clamping_flat_output (line 378) | def test_clamping_flat_output(
  function test_range_invariant_after_clamping (line 449) | def test_range_invariant_after_clamping(
  function test_aspect_ratio_warning (line 516) | def test_aspect_ratio_warning(hm_w, hm_h, tgt_w, tgt_h):
  function test_contrast_warning (line 544) | def test_contrast_warning(size, fill_value):
  function test_contrast_no_warning_for_varied_image (line 563) | def test_contrast_no_warning_for_varied_image(size):
  function test_invalid_file_error_handling (line 592) | def test_invalid_file_error_handling(random_bytes):

FILE: tests/test_heightmap_unit.py
  class TestGrayscaleMapping (line 25) | class TestGrayscaleMapping:
    method test_pure_black_maps_to_max_height (line 28) | def test_pure_black_maps_to_max_height(self):
    method test_pure_white_maps_to_base_thickness (line 39) | def test_pure_white_maps_to_base_thickness(self):
    method test_mid_gray_maps_to_middle_value (line 49) | def test_mid_gray_maps_to_middle_value(self):
  class TestColorToGrayscale (line 64) | class TestColorToGrayscale:
    method test_rgb_image_converts_to_grayscale (line 67) | def test_rgb_image_converts_to_grayscale(self):
    method test_rgba_image_converts_to_grayscale (line 81) | def test_rgba_image_converts_to_grayscale(self):
    method test_grayscale_image_passthrough (line 96) | def test_grayscale_image_passthrough(self):
  class TestResizeToTarget (line 108) | class TestResizeToTarget:
    method test_resize_different_sizes (line 111) | def test_resize_different_sizes(self):
    method test_resize_upscale (line 121) | def test_resize_upscale(self):
    method test_resize_preserves_shape (line 130) | def test_resize_preserves_shape(self):
  class TestHeightClamping (line 144) | class TestHeightClamping:
    method test_height_below_optical_thickness_is_clamped (line 147) | def test_height_below_optical_thickness_is_clamped(self):
  class TestErrorHandling (line 188) | class TestErrorHandling:
    method test_invalid_file_returns_error (line 191) | def test_invalid_file_returns_error(self):
    method test_nonexistent_file_returns_error (line 205) | def test_nonexistent_file_returns_error(self):
    method test_aspect_ratio_deviation_warning (line 211) | def test_aspect_ratio_deviation_warning(self):
    method test_aspect_ratio_no_warning_when_close (line 217) | def test_aspect_ratio_no_warning_when_close(self):
    method test_low_contrast_warning (line 222) | def test_low_contrast_warning(self):
    method test_no_contrast_warning_for_normal_image (line 229) | def test_no_contrast_warning_for_normal_image(self):

FILE: tests/test_heightmap_upload_unit.py
  function _make_grayscale_png (line 35) | def _make_grayscale_png(width: int = 100, height: int = 100) -> io.BytesIO:
  function _make_rgb_png (line 45) | def _make_rgb_png(width: int = 100, height: int = 100) -> io.BytesIO:
  function _make_text_file (line 56) | def _make_text_file() -> io.BytesIO:
  function _setup_session_with_preview (line 63) | def _setup_session_with_preview(
  class TestValidHeightmapUpload (line 114) | class TestValidHeightmapUpload:
    method test_valid_grayscale_png_returns_200 (line 117) | def test_valid_grayscale_png_returns_200(self) -> None:
    method test_valid_rgb_png_returns_200 (line 144) | def test_valid_rgb_png_returns_200(self) -> None:
  class TestInvalidFormatReturns422 (line 166) | class TestInvalidFormatReturns422:
    method test_text_file_returns_422 (line 169) | def test_text_file_returns_422(self) -> None:
  class TestAspectRatioWarning (line 189) | class TestAspectRatioWarning:
    method test_mismatched_aspect_ratio_returns_warning (line 192) | def test_mismatched_aspect_ratio_returns_warning(self) -> None:
    method test_matching_aspect_ratio_no_warning (line 210) | def test_matching_aspect_ratio_no_warning(self) -> None:
  class TestMissingSessionReturns404 (line 231) | class TestMissingSessionReturns404:
    method test_unknown_session_returns_404 (line 234) | def test_unknown_session_returns_404(self) -> None:
  class TestMissingPreviewCacheReturns409 (line 252) | class TestMissingPreviewCacheReturns409:
    method test_no_preview_cache_returns_409 (line 255) | def test_no_preview_cache_returns_409(self) -> None:

FILE: tests/test_layout_new_tabs_unit.py
  function test_header_css_shows_converter_by_default_and_hides_other_tabs (line 6) | def test_header_css_shows_converter_by_default_and_hides_other_tabs():
  function test_custom_tab_js_explicitly_switches_active_content_display (line 19) | def test_custom_tab_js_explicitly_switches_active_content_display():
  function test_create_app_registers_all_custom_tab_ids (line 25) | def test_create_app_registers_all_custom_tab_ids():
  function test_create_app_marks_converter_tab_selected_by_default (line 55) | def test_create_app_marks_converter_tab_selected_by_default():

FILE: tests/test_lut_api_properties.py
  function rgb_array_st (line 34) | def rgb_array_st(draw: st.DrawFn, min_size: int = 2, max_size: int = 30)...
  function stacks_for_rgb (line 52) | def stacks_for_rgb(draw: st.DrawFn, rgb: np.ndarray, max_id: int = 7) ->...
  function lut_entry (line 66) | def lut_entry(draw: st.DrawFn, mode: str | None = None, min_size: int = ...
  function merge_input (line 77) | def merge_input(draw: st.DrawFn):
  function merge_input_small (line 93) | def merge_input_small(draw: st.DrawFn):
  class TestLutInfoApiRoundTrip (line 111) | class TestLutInfoApiRoundTrip:
    method test_info_consistent_with_list (line 129) | def test_info_consistent_with_list(self, names_and_modes):
  class TestMergeStatsConsistencyBackend (line 201) | class TestMergeStatsConsistencyBackend:
    method test_stats_invariants_no_dedup (line 212) | def test_stats_invariants_no_dedup(self, entries):
    method test_stats_invariants_with_small_threshold (line 254) | def test_stats_invariants_with_small_threshold(self, entries):

FILE: tests/test_lut_api_unit.py
  class TestLutInfoSuccess (line 24) | class TestLutInfoSuccess:
    method test_info_returns_200_with_correct_fields (line 29) | def test_info_returns_200_with_correct_fields(
  class TestLutInfoNotFound (line 45) | class TestLutInfoNotFound:
    method test_info_returns_404_when_lut_not_found (line 49) | def test_info_returns_404_when_lut_not_found(self, mock_path) -> None:
  class TestMergeEmptySecondary (line 60) | class TestMergeEmptySecondary:
    method test_merge_empty_secondary_returns_400 (line 63) | def test_merge_empty_secondary_returns_400(self) -> None:
  class TestMergePrimaryModeInvalid (line 79) | class TestMergePrimaryModeInvalid:
    method test_merge_4color_primary_returns_400 (line 84) | def test_merge_4color_primary_returns_400(self, mock_path, mock_detect...
  class TestMergeCompatibilityFailure (line 100) | class TestMergeCompatibilityFailure:
    method test_merge_incompatible_modes_returns_400 (line 110) | def test_merge_incompatible_modes_returns_400(

FILE: tests/test_lut_list_properties.py
  function test_lut_list_keys_always_sorted (line 64) | def test_lut_list_keys_always_sorted() -> None:
  function test_lut_list_via_api_keys_sorted (line 82) | def test_lut_list_via_api_keys_sorted() -> None:
  function test_sorting_preserved_for_random_lut_entries (line 104) | def test_sorting_preserved_for_random_lut_entries(
  function test_api_returns_sorted_keys_with_mocked_luts (line 123) | def test_api_returns_sorted_keys_with_mocked_luts(
  function _find_unsorted_pair (line 146) | def _find_unsorted_pair(keys: list) -> str:

FILE: tests/test_lut_merger_properties.py
  function rgb_array (line 31) | def rgb_array(size):
  function stack_array (line 34) | def stack_array(size, max_id=7):
  class TestColorModeDetection (line 42) | class TestColorModeDetection:
    method test_standard_size_detection (line 50) | def test_standard_size_detection(self, size):
    method test_non_standard_size_returns_merged (line 63) | def test_non_standard_size_returns_merged(self, size):
    method test_npz_detection (line 74) | def test_npz_detection(self):
  class TestCompatibilityValidation (line 90) | class TestCompatibilityValidation:
    method test_valid_with_high_mode (line 101) | def test_valid_with_high_mode(self, low_modes, high_mode):
    method test_invalid_without_high_mode (line 119) | def test_invalid_without_high_mode(self, modes):
    method test_single_lut_invalid (line 126) | def test_single_lut_invalid(self):
    method test_8color_allows_all (line 131) | def test_8color_allows_all(self):
  class TestMergeConcatenation (line 142) | class TestMergeConcatenation:
    method test_no_dedup_preserves_all (line 153) | def test_no_dedup_preserves_all(self, n1, n2):
  class TestMaterialIDRange (line 198) | class TestMaterialIDRange:
    method test_material_ids_in_range (line 208) | def test_material_ids_in_range(self, n):
  class TestDedupNoSimilarColors (line 233) | class TestDedupNoSimilarColors:
    method test_exact_dedup_removes_duplicates (line 239) | def test_exact_dedup_removes_duplicates(self):
    method test_threshold_dedup_removes_similar (line 256) | def test_threshold_dedup_removes_similar(self):
  class TestDedupPriority (line 281) | class TestDedupPriority:
    method test_higher_mode_preserved (line 287) | def test_higher_mode_preserved(self):
  class TestMergeStatsConsistency (line 311) | class TestMergeStatsConsistency:
    method test_stats_add_up (line 322) | def test_stats_add_up(self, n1, n2):
  class TestSaveLoadRoundTrip (line 345) | class TestSaveLoadRoundTrip:
    method test_roundtrip (line 353) | def test_roundtrip(self, n):
    method test_npz_extension_enforced (line 373) | def test_npz_extension_enforced(self):
  class TestNonStandardSizeDetection (line 388) | class TestNonStandardSizeDetection:
    method test_npz_detected_as_merged (line 394) | def test_npz_detected_as_merged(self):
    method test_non_standard_npy_detected_as_merged (line 408) | def test_non_standard_npy_detected_as_merged(self, size):

FILE: tests/test_mime_type_properties.py
  function test_known_extensions_return_correct_mime (line 64) | def test_known_extensions_return_correct_mime(ext: str, base: str) -> None:
  function test_unknown_extensions_return_octet_stream (line 79) | def test_unknown_extensions_return_octet_stream(ext: str, base: str) -> ...
  function test_case_insensitive (line 91) | def test_case_insensitive(ext: str, base: str) -> None:

FILE: tests/test_naming_properties.py
  function test_model_filename_format_correctness (line 46) | def test_model_filename_format_correctness(base_name, modeling_mode, col...
  function test_no_forbidden_characters_in_filenames (line 74) | def test_no_forbidden_characters_in_filenames(
  function test_generate_parse_round_trip (line 109) | def test_generate_parse_round_trip(base_name, modeling_mode, color_mode):
  function _is_standard_filename (line 152) | def _is_standard_filename(s: str) -> bool:
  function test_non_standard_filename_parse_safety (line 169) | def test_non_standard_filename_parse_safety(arbitrary):
  function test_parse_filename_never_raises (line 185) | def test_parse_filename_never_raises(arbitrary):

FILE: tests/test_naming_unit.py
  class TestModelingModeTags (line 32) | class TestModelingModeTags:
    method test_high_fidelity_maps_to_hifi (line 35) | def test_high_fidelity_maps_to_hifi(self):
    method test_pixel_maps_to_pixel (line 38) | def test_pixel_maps_to_pixel(self):
    method test_vector_maps_to_vector (line 41) | def test_vector_maps_to_vector(self):
    method test_all_enum_members_have_mapping (line 44) | def test_all_enum_members_have_mapping(self):
  class TestColorModeTags (line 53) | class TestColorModeTags:
    method test_4color_variants_map_to_4c (line 57) | def test_4color_variants_map_to_4c(self, color_mode):
    method test_6color_maps_to_6c (line 60) | def test_6color_maps_to_6c(self):
    method test_8color_variants_map_to_8c (line 64) | def test_8color_variants_map_to_8c(self, color_mode):
    method test_bw_variants_map_to_bw (line 68) | def test_bw_variants_map_to_bw(self, color_mode):
  class TestEdgeCases (line 76) | class TestEdgeCases:
    method test_empty_base_name_uses_untitled (line 79) | def test_empty_base_name_uses_untitled(self):
    method test_whitespace_only_base_name_uses_untitled (line 83) | def test_whitespace_only_base_name_uses_untitled(self):
    method test_special_characters_sanitized (line 87) | def test_special_characters_sanitized(self):
    method test_all_forbidden_chars_sanitized (line 96) | def test_all_forbidden_chars_sanitized(self):
    method test_unicode_base_name (line 104) | def test_unicode_base_name(self):
    method test_unicode_emoji_base_name (line 109) | def test_unicode_emoji_base_name(self):
    method test_unknown_modeling_mode_uses_unknown (line 113) | def test_unknown_modeling_mode_uses_unknown(self):
    method test_unknown_color_mode_uses_unknown (line 119) | def test_unknown_color_mode_uses_unknown(self):
    method test_sanitize_preserves_normal_chars (line 123) | def test_sanitize_preserves_normal_chars(self):
    method test_sanitize_replaces_forbidden (line 126) | def test_sanitize_replaces_forbidden(self):
  class TestGeneratedFilenameFormat (line 138) | class TestGeneratedFilenameFormat:
    method test_model_filename_structure (line 142) | def test_model_filename_structure(self, _mock_ts):
    method test_model_filename_pixel_6c (line 149) | def test_model_filename_pixel_6c(self, _mock_ts):
    method test_model_filename_vector_8c (line 154) | def test_model_filename_vector_8c(self, _mock_ts):
    method test_model_filename_bw (line 161) | def test_model_filename_bw(self, _mock_ts):
    method test_preview_filename_structure (line 166) | def test_preview_filename_structure(self, _mock_ts):
    method test_calibration_filename_structure (line 171) | def test_calibration_filename_structure(self, _mock_ts):
    method test_batch_filename_structure (line 176) | def test_batch_filename_structure(self, _mock_ts):
    method test_custom_extension (line 181) | def test_custom_extension(self, _mock_ts):
    method test_model_filename_matches_regex (line 187) | def test_model_filename_matches_regex(self):
  class TestParseFilename (line 201) | class TestParseFilename:
    method test_parse_returns_none_for_empty_string (line 204) | def test_parse_returns_none_for_empty_string(self):
    method test_parse_returns_none_for_random_string (line 207) | def test_parse_returns_none_for_random_string(self):
    method test_parse_returns_none_for_none_input (line 210) | def test_parse_returns_none_for_none_input(self):
    method test_parse_returns_none_for_non_string (line 213) | def test_parse_returns_none_for_non_string(self):
    method test_parse_model_filename (line 217) | def test_parse_model_filename(self, _mock_ts):
    method test_parse_preview_filename (line 228) | def test_parse_preview_filename(self, _mock_ts):
    method test_parse_calibration_filename (line 236) | def test_parse_calibration_filename(self, _mock_ts):
    method test_parse_batch_filename (line 244) | def test_parse_batch_filename(self, _mock_ts):

FILE: tests/test_palette_connected_selection_unit.py
  function test_connected_region_4n_splits_diagonal_islands (line 13) | def test_connected_region_4n_splits_diagonal_islands():
  function test_recommend_lut_colors_returns_top_k_sorted (line 27) | def test_recommend_lut_colors_returns_top_k_sorted():
  function test_generate_preview_cache_contract_requires_quantized_image_key (line 40) | def test_generate_preview_cache_contract_requires_quantized_image_key():
  function test_preview_click_records_region_and_dual_hex (line 50) | def test_preview_click_records_region_and_dual_hex():
  function test_highlight_uses_region_mask_when_present (line 62) | def test_highlight_uses_region_mask_when_present():
  function test_apply_region_replacement_only_changes_masked_pixels (line 74) | def test_apply_region_replacement_only_changes_masked_pixels():
  function test_dual_recommendation_returns_two_groups_of_ten_or_less (line 89) | def test_dual_recommendation_returns_two_groups_of_ten_or_less():
  function test_resolve_click_selection_hexes_prefers_matched_for_display (line 100) | def test_resolve_click_selection_hexes_prefers_matched_for_display():

FILE: tests/test_preview_click_selection_unit.py
  class _EvtNoneIndex (line 20) | class _EvtNoneIndex:
  function test_invalid_click_returns_none_hex (line 24) | def test_invalid_click_returns_none_hex():
  function test_resolve_click_selection_hexes_rejects_non_string_default (line 32) | def test_resolve_click_selection_hexes_rejects_non_string_default():
  function test_resolve_click_selection_hexes_prefers_cached_strings (line 39) | def test_resolve_click_selection_hexes_prefers_cached_strings():
  function test_selected_dual_color_html_accepts_non_string_inputs (line 46) | def test_selected_dual_color_html_accepts_non_string_inputs():

FILE: tests/test_relief_mode_fix_properties.py
  function extract_unique_colors (line 25) | def extract_unique_colors(
  function test_black_solid_pixels_included_in_color_set (line 76) | def test_black_solid_pixels_included_in_color_set(
  function test_black_non_solid_pixels_excluded (line 132) | def test_black_non_solid_pixels_excluded(
  function test_fallback_without_mask_includes_black (line 176) | def test_fallback_without_mask_includes_black(

FILE: tests/test_relief_mode_fix_unit.py
  function _real_gr (line 17) | def _real_gr():
  function on_height_mode_change (line 27) | def on_height_mode_change(mode: str) -> tuple:
  function on_relief_mode_toggle (line 46) | def on_relief_mode_toggle(enable_relief, selected_color, height_map, bas...
  class TestOnHeightModeChange (line 94) | class TestOnHeightModeChange:
    method test_switch_to_dark_raised_clears_heightmap (line 97) | def test_switch_to_dark_raised_clears_heightmap(self) -> None:
    method test_switch_to_light_raised_clears_heightmap (line 104) | def test_switch_to_light_raised_clears_heightmap(self) -> None:
    method test_switch_to_heightmap_mode_preserves (line 111) | def test_switch_to_heightmap_mode_preserves(self) -> None:
    method test_return_tuple_length (line 117) | def test_return_tuple_length(self) -> None:
  class TestOnReliefModeToggle (line 125) | class TestOnReliefModeToggle:
    method test_disable_relief_clears_heightmap (line 128) | def test_disable_relief_clears_heightmap(self) -> None:
    method test_enable_relief_preserves_heightmap (line 135) | def test_enable_relief_preserves_heightmap(self) -> None:
    method test_disable_relief_return_length (line 141) | def test_disable_relief_return_length(self) -> None:
    method test_enable_relief_no_selected_color_preserves_heightmap (line 146) | def test_enable_relief_no_selected_color_preserves_heightmap(self) -> ...

FILE: tests/test_segmented_glb_properties.py
  function _hex_to_rgb (line 27) | def _hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
  function _make_cache (line 32) | def _make_cache(
  function matched_rgb_strategy (line 51) | def matched_rgb_strategy(draw: st.DrawFn):
  function test_glb_segmentation_correctness (line 106) | def test_glb_segmentation_correctness(data):
  function many_colors_strategy (line 193) | def many_colors_strategy(draw: st.DrawFn):
  function test_glb_mesh_count_limit (line 259) | def test_glb_mesh_count_limit(data):

FILE: tests/test_segmented_glb_unit.py
  function _make_cache (line 23) | def _make_cache(
  function _get_color_nodes (line 39) | def _get_color_nodes(scene: trimesh.Scene) -> dict[str, trimesh.Trimesh]:
  class TestEmptyAndNoneInputs (line 54) | class TestEmptyAndNoneInputs:
    method test_none_cache_returns_none (line 57) | def test_none_cache_returns_none(self) -> None:
    method test_empty_image_all_transparent_returns_none (line 62) | def test_empty_image_all_transparent_returns_none(self) -> None:
    method test_missing_matched_rgb_returns_none (line 72) | def test_missing_matched_rgb_returns_none(self) -> None:
    method test_missing_mask_solid_returns_none (line 83) | def test_missing_mask_solid_returns_none(self) -> None:
  class TestSingleColorImage (line 99) | class TestSingleColorImage:
    method test_single_color_generates_one_mesh (line 102) | def test_single_color_generates_one_mesh(self) -> None:
    method test_single_color_with_partial_mask (line 130) | def test_single_color_with_partial_mask(self) -> None:
  class TestExactly64Colors (line 157) | class TestExactly64Colors:
    method test_64_colors_no_merge (line 160) | def test_64_colors_no_merge(self) -> None:

FILE: tests/test_session_store_clear_properties.py
  function _create_temp_file (line 35) | def _create_temp_file() -> str:
  function test_clear_all_empties_store_and_returns_session_count (line 56) | def test_clear_all_empties_store_and_returns_session_count(

FILE: tests/test_session_store_properties.py
  function test_create_returns_valid_uuid4 (line 47) | def test_create_returns_valid_uuid4(data: st.DataObject) -> None:
  function test_create_then_get_returns_empty_dict (line 61) | def test_create_then_get_returns_empty_dict(data: st.DataObject) -> None:
  function test_put_then_get_consistency (line 74) | def test_put_then_get_consistency(key: str, value: object) -> None:
  function test_multiple_puts_all_retrievable (line 90) | def test_multiple_puts_all_retrievable(kv: dict) -> None:
  function test_exists_after_create (line 111) | def test_exists_after_create(data: st.DataObject) -> None:

FILE: tests/test_session_thread_safety_properties.py
  function test_concurrent_create_put_get_no_exceptions (line 30) | def test_concurrent_create_put_get_no_exceptions(
  function test_concurrent_sessions_data_isolation (line 73) | def test_concurrent_sessions_data_isolation(n_threads: int) -> None:

FILE: tests/test_session_ttl_properties.py
  function _create_temp_files (line 50) | def _create_temp_files(n: int) -> List[Tuple[str, int]]:
  function test_ttl_zero_cleanup_removes_session (line 69) | def test_ttl_zero_cleanup_removes_session(

FILE: tests/test_settings_api_properties.py
  function test_settings_round_trip (line 50) | def test_settings_round_trip(
  function test_settings_rejects_invalid_enable_crop_modal (line 113) | def test_settings_rejects_invalid_enable_crop_modal(bad_value) -> None:

FILE: tests/test_settings_api_unit.py
  class TestGetSettings (line 27) | class TestGetSettings:
    method test_get_settings_file_not_exists (line 30) | def test_get_settings_file_not_exists(self) -> None:
    method test_get_settings_file_exists (line 47) | def test_get_settings_file_exists(self) -> None:
  class TestPostSettings (line 81) | class TestPostSettings:
    method test_post_settings_success (line 84) | def test_post_settings_success(self) -> None:
    method test_post_settings_io_error (line 104) | def test_post_settings_io_error(self) -> None:
  class TestGetStats (line 129) | class TestGetStats:
    method test_get_stats_success (line 132) | def test_get_stats_success(self) -> None:
    method test_get_stats_no_file (line 144) | def test_get_stats_no_file(self) -> None:

FILE: tests/test_slicer_properties.py
  function _make_app (line 30) | def _make_app():
  function _make_client (line 39) | def _make_client() -> TestClient:
  function test_nonexistent_exe_paths_filtered (line 81) | def test_nonexistent_exe_paths_filtered(num_valid: int, num_invalid: int...
  function test_unknown_slicer_id_returns_error (line 143) | def test_unknown_slicer_id_returns_error(slicer_id: str) -> None:
  function test_nonexistent_file_path_returns_error (line 174) | def test_nonexistent_file_path_returns_error(file_path: str) -> None:
  function test_invalid_request_body_returns_422 (line 239) | def test_invalid_request_body_returns_422(body: dict) -> None:
  function test_registry_match_produces_correct_display_name (line 282) | def test_registry_match_produces_correct_display_name(
  function test_generate_response_contains_threemf_disk_path (line 331) | def test_generate_response_contains_threemf_disk_path(path: str) -> None:
  function test_generate_response_threemf_disk_path_none_when_omitted (line 357) | def test_generate_response_threemf_disk_path_none_when_omitted() -> None:
  function test_generate_response_threemf_disk_path_explicit_none (line 375) | def test_generate_response_threemf_disk_path_explicit_none(path) -> None:

FILE: tests/test_slicer_unit.py
  function _make_app (line 35) | def _make_app():
  function _make_client (line 44) | def _make_client() -> TestClient:
  class TestScanRegistry (line 52) | class TestScanRegistry:
    method test_scan_registry_non_windows_linux (line 56) | def test_scan_registry_non_windows_linux(self, _mock_sys):
    method test_scan_registry_non_windows_darwin (line 60) | def test_scan_registry_non_windows_darwin(self, _mock_sys):
  class TestDetectInstalledSlicers (line 68) | class TestDetectInstalledSlicers:
    method test_filters_invalid_paths (line 71) | def test_filters_invalid_paths(self, tmp_path):
    method test_returns_empty_when_all_invalid (line 86) | def test_returns_empty_when_all_invalid(self):
    method test_returns_empty_when_scan_empty (line 94) | def test_returns_empty_when_scan_empty(self):
  class TestLaunchSlicer (line 104) | class TestLaunchSlicer:
    method test_success (line 107) | def test_success(self, tmp_path):
    method test_unknown_slicer_id (line 120) | def test_unknown_slicer_id(self, tmp_path):
    method test_file_not_found (line 130) | def test_file_not_found(self):
    method test_process_error (line 137) | def test_process_error(self, tmp_path):
  class TestPydanticModels (line 154) | class TestPydanticModels:
    method test_slicer_info_valid (line 157) | def test_slicer_info_valid(self):
    method test_slicer_info_empty_id_rejected (line 162) | def test_slicer_info_empty_id_rejected(self):
    method test_launch_request_valid (line 167) | def test_launch_request_valid(self):
    method test_launch_request_empty_path_rejected (line 171) | def test_launch_request_empty_path_rejected(self):
    method test_detect_response_defaults_to_empty_list (line 176) | def test_detect_response_defaults_to_empty_list(self):
    method test_launch_response_valid (line 180) | def test_launch_response_valid(self):
  class TestRouterEndpoints (line 189) | class TestRouterEndpoints:
    method test_detect_endpoint_returns_200 (line 192) | def test_detect_endpoint_returns_200(self):
    method test_detect_endpoint_empty (line 207) | def test_detect_endpoint_empty(self):
    method test_launch_success (line 216) | def test_launch_success(self, tmp_path):
    method test_launch_file_not_found (line 235) | def test_launch_file_not_found(self):
    method test_launch_invalid_body_returns_422 (line 245) | def test_launch_invalid_body_returns_422(self):
    method test_launch_empty_body_returns_422 (line 251) | def test_launch_empty_body_returns_422(self):
    method test_launch_slicer_not_found_returns_404 (line 257) | def test_launch_slicer_not_found_returns_404(self, tmp_path):

FILE: tests/test_stack_order_properties.py
  class TestStackReversalEquivalence (line 25) | class TestStackReversalEquivalence:
    method test_reversal_equivalence_6color (line 33) | def test_reversal_equivalence_6color(self, stack: tuple):
    method test_reversal_equivalence_8color (line 49) | def test_reversal_equivalence_8color(self, stack: tuple):
    method test_reversal_equivalence_generic (line 62) | def test_reversal_equivalence_generic(self, stack: list):
    method test_old_vs_new_calibration_write_6color (line 70) | def test_old_vs_new_calibration_write_6color(self, stack: tuple):
    method test_old_vs_new_calibration_write_8color (line 97) | def test_old_vs_new_calibration_write_8color(self, stack: tuple):
  class TestEndToEndOutputEquivalence (line 121) | class TestEndToEndOutputEquivalence:
    method test_calibration_board_6color_pipeline (line 135) | def test_calibration_board_6color_pipeline(self, stack: tuple):
    method test_calibration_board_8color_pipeline (line 169) | def test_calibration_board_8color_pipeline(self, stack: tuple):
    method test_lut_loading_6color_ref_stacks (line 201) | def test_lut_loading_6color_ref_stacks(self, stack: tuple):
    method test_lut_loading_8color_ref_stacks (line 225) | def test_lut_loading_8color_ref_stacks(self, stack: tuple):
    method test_full_pipeline_6color (line 249) | def test_full_pipeline_6color(self, stack: tuple):
    method test_full_pipeline_8color (line 283) | def test_full_pipeline_8color(self, stack: tuple):
  class TestLUTRefStacksInvariance (line 312) | class TestLUTRefStacksInvariance:
    method test_ref_stacks_invariance_single_6color (line 335) | def test_ref_stacks_invariance_single_6color(self, stack: tuple):
    method test_ref_stacks_invariance_single_8color (line 365) | def test_ref_stacks_invariance_single_8color(self, stack: tuple):
    method test_ref_stacks_invariance_batch_6color (line 396) | def test_ref_stacks_invariance_batch_6color(self, batch: list):
    method test_ref_stacks_invariance_batch_8color (line 424) | def test_ref_stacks_invariance_batch_8color(self, batch: list):
    method test_convention_correctness_6color (line 454) | def test_convention_correctness_6color(self, stack: tuple):
    method test_convention_correctness_8color (line 472) | def test_convention_correctness_8color(self, stack: tuple):

FILE: tests/test_user_replacement_list_ui_unit.py
  function test_palette_list_i18n_keys_exist (line 17) | def test_palette_list_i18n_keys_exist():
  function test_generate_palette_html_uses_list_cards_and_selected_classes (line 31) | def test_generate_palette_html_uses_list_cards_and_selected_classes():
  function test_delete_selected_user_replacement_removes_target_row (line 48) | def test_delete_selected_user_replacement_removes_target_row(monkeypatch):
  function test_apply_replacement_global_scope_writes_region_not_map (line 66) | def test_apply_replacement_global_scope_writes_region_not_map(monkeypatch):
  function test_normalize_color_replacements_accepts_region_list_without_crash (line 114) | def test_normalize_color_replacements_accepts_region_list_without_crash():
  function test_normalize_color_replacements_prefers_matched_when_present_in_region_item (line 128) | def test_normalize_color_replacements_prefers_matched_when_present_in_re...
  function test_process_batch_generation_single_accepts_replacement_regions_list (line 140) | def test_process_batch_generation_single_accepts_replacement_regions_lis...
  function test_process_batch_generation_full_pipeline_replacement_regions_affect_preview_and_model (line 199) | def test_process_batch_generation_full_pipeline_replacement_regions_affe...
  function test_update_preview_with_replacement_regions_applies_hex_without_nameerror (line 281) | def test_update_preview_with_replacement_regions_applies_hex_without_nam...
  function test_undo_uses_regions_only_history (line 319) | def test_undo_uses_regions_only_history(monkeypatch):
  function test_update_preview_applies_regions_in_order_without_map (line 344) | def test_update_preview_applies_regions_in_order_without_map(monkeypatch):
  function test_create_converter_tab_content_initializes_without_replacement_map_nameerror (line 365) | def test_create_converter_tab_content_initializes_without_replacement_ma...
  function test_regions_override_updates_material_matrix_in_order (line 380) | def test_regions_override_updates_material_matrix_in_order():

FILE: tests/test_vector_engine_unit.py
  function _rect (line 43) | def _rect(x0, y0, x1, y1, color=(255, 0, 0)):
  function _make_parse_only_processor (line 48) | def _make_parse_only_processor(sampling_precision=0.05):
  class TestClipOcclusion (line 59) | class TestClipOcclusion:
    method test_no_overlap_preserves_all (line 62) | def test_no_overlap_preserves_all(self):
    method test_later_shape_covers_earlier (line 73) | def test_later_shape_covers_earlier(self):
    method test_partial_overlap (line 86) | def test_partial_overlap(self):
    method test_draw_order_preserved (line 106) | def test_draw_order_preserved(self):
    method test_empty_input (line 117) | def test_empty_input(self):
    method test_return_silhouette_when_requested (line 121) | def test_return_silhouette_when_requested(self):
    method test_three_stacked_shapes (line 134) | def test_three_stacked_shapes(self):
    method test_no_small_feature_exemption (line 145) | def test_no_small_feature_exemption(self):
  class TestRunLengthExtrude (line 160) | class TestRunLengthExtrude:
    method _make_matched (line 166) | def _make_matched(self, geometry, recipe):
    method test_single_channel_all_layers (line 169) | def test_single_channel_all_layers(self):
    method test_alternating_channels_no_merge (line 182) | def test_alternating_channels_no_merge(self):
    method test_run_merges_consecutive (line 197) | def test_run_merges_consecutive(self):
    method test_material_id_in_result (line 211) | def test_material_id_in_result(self):
    method test_empty_geometry_skipped (line 223) | def test_empty_geometry_skipped(self):
  class TestOutputOrdering (line 240) | class TestOutputOrdering:
    method test_sorted_by_mat_id (line 243) | def test_sorted_by_mat_id(self):
  class TestExtrudeGeometry (line 259) | class TestExtrudeGeometry:
    method test_polygon_produces_mesh (line 261) | def test_polygon_produces_mesh(self):
    method test_multipolygon (line 267) | def test_multipolygon(self):
    method test_empty_returns_empty (line 273) | def test_empty_returns_empty(self):
    method test_none_returns_empty (line 277) | def test_none_returns_empty(self):
    method test_extrude_cache_reuses_base_mesh (line 281) | def test_extrude_cache_reuses_base_mesh(self):
  class TestParseSvgSubpaths (line 311) | class TestParseSvgSubpaths:
    method test_split_multi_subpath_path_into_multiple_polygons (line 313) | def test_split_multi_subpath_path_into_multiple_polygons(self, tmp_path):
    method test_parse_falls_back_when_subpath_split_unavailable (line 338) | def test_parse_falls_back_when_subpath_split_unavailable(self, tmp_path):
    method test_occlusion_keeps_uncovered_large_block (line 357) | def test_occlusion_keeps_uncovered_large_block(self, tmp_path):

FILE: tests/test_worker_pool_properties.py
  function _worker_raise_value_error (line 32) | def _worker_raise_value_error(msg: str) -> None:
  function _worker_raise_runtime_error (line 37) | def _worker_raise_runtime_error(msg: str) -> None:
  function _worker_raise_os_error (line 42) | def _worker_raise_os_error(msg: str) -> None:
  function _worker_raise_type_error (line 47) | def _worker_raise_type_error(msg: str) -> None:
  function _worker_raise_io_error (line 52) | def _worker_raise_io_error(msg: str) -> None:
  function _worker_sleep (line 66) | def _worker_sleep(seconds: float) -> str:
  class TestWorkerPoolMaxWorkersProperty (line 78) | class TestWorkerPoolMaxWorkersProperty:
    method test_max_workers_equals_min_cpu_count_4 (line 86) | def test_max_workers_equals_min_cpu_count_4(self, cpu_count: int) -> N...
  class TestWorkerPoolConfigEnvOverrideProperty (line 106) | class TestWorkerPoolConfigEnvOverrideProperty:
    method test_env_vars_override_config (line 117) | def test_env_vars_override_config(self, n: int, f: float) -> None:
  class TestWorkerExceptionPropagationProperty (line 143) | class TestWorkerExceptionPropagationProperty:
    method test_worker_exception_propagates (line 158) | def test_worker_exception_propagates(self, exc_index: int, msg: str) -...
  class TestTaskTimeoutCancellationProperty (line 184) | class TestTaskTimeoutCancellationProperty:
    method test_task_timeout_raises_timeout_error (line 194) | def test_task_timeout_raises_timeout_error(self, timeout: float) -> None:

FILE: tests/test_worker_pool_unit.py
  function _add (line 21) | def _add(a: int, b: int) -> int:
  function _raise_value_error (line 26) | def _raise_value_error(msg: str) -> None:
  function _slow_task (line 31) | def _slow_task(seconds: float) -> str:
  class TestWorkerPoolLifecycle (line 42) | class TestWorkerPoolLifecycle:
    method test_not_alive_before_start (line 45) | def test_not_alive_before_start(self) -> None:
    method test_alive_after_start (line 49) | def test_alive_after_start(self) -> None:
    method test_not_alive_after_shutdown (line 57) | def test_not_alive_after_shutdown(self) -> None:
    method test_shutdown_without_start_is_noop (line 63) | def test_shutdown_without_start_is_noop(self) -> None:
    method test_double_shutdown_is_safe (line 68) | def test_double_shutdown_is_safe(self) -> None:
  class TestMaxWorkers (line 80) | class TestMaxWorkers:
    method test_explicit_max_workers (line 83) | def test_explicit_max_workers(self) -> None:
    method test_default_max_workers (line 87) | def test_default_max_workers(self) -> None:
    method test_none_max_workers_uses_default (line 92) | def test_none_max_workers_uses_default(self) -> None:
  class TestSubmit (line 102) | class TestSubmit:
    method test_submit_returns_result (line 106) | async def test_submit_returns_result(self) -> None:
    method test_submit_raises_runtime_error_when_not_started (line 116) | async def test_submit_raises_runtime_error_when_not_started(self) -> N...
    method test_submit_propagates_worker_exception (line 122) | async def test_submit_propagates_worker_exception(self) -> None:
    method test_submit_timeout_raises_timeout_error (line 133) | async def test_submit_timeout_raises_timeout_error(self) -> None:

FILE: tests/verify_merge_remap.py
  function verify_remap_table (line 42) | def verify_remap_table(remap_key, src_slots, expected_mapping):
  function verify_subtype_detection (line 70) | def verify_subtype_detection():
  function verify_remap_with_real_stacks (line 114) | def verify_remap_with_real_stacks():
  function verify_real_lut_files (line 197) | def verify_real_lut_files():
  function verify_merged_npz (line 274) | def verify_merged_npz():
  function verify_image_conversion (line 310) | def verify_image_conversion():

FILE: ui/callbacks.py
  function _hex_to_rgb_tuple (line 16) | def _hex_to_rgb_tuple(hex_color: str):
  function _build_full_color_region_mask (line 23) | def _build_full_color_region_mask(cache, selected_color: str):
  function _resolve_mode_key (line 51) | def _resolve_mode_key(mode: str) -> str:
  function _color_mode_html (line 62) | def _color_mode_html(mode: str) -> str:
  function on_lut_select (line 83) | def on_lut_select(display_name):
  function on_lut_upload_save (line 104) | def on_lut_upload_save(uploaded_file):
  function _get_corner_labels (line 120) | def _get_corner_labels(mode, page_choice=None):
  function get_first_hint (line 127) | def get_first_hint(mode, page_choice=None):
  function get_next_hint (line 134) | def get_next_hint(mode, pts_count, page_choice=None):
  function on_extractor_upload (line 143) | def on_extractor_upload(i, mode, page_choice=None):
  function on_extractor_mode_change (line 149) | def on_extractor_mode_change(img, mode, page_choice=None):
  function on_extractor_rotate (line 157) | def on_extractor_rotate(i, mode, page_choice=None):
  function on_extractor_click (line 166) | def on_extractor_click(img, pts, mode, page_choice, evt: gr.SelectData):
  function on_extractor_clear (line 177) | def on_extractor_clear(img, mode, page_choice=None):
  function on_extractor_page_change (line 183) | def on_extractor_page_change(img, mode, page_choice):
  function on_palette_color_select (line 192) | def on_palette_color_select(palette_html, evt: gr.SelectData, lang: str ...
  function on_apply_color_replacement (line 212) | def on_apply_color_replacement(cache, selected_color, replacement_color,
  function on_clear_color_replacements (line 265) | def on_clear_color_replacements(cache, replacement_regions, replacement_...
  function on_preview_generated_update_palette (line 296) | def on_preview_generated_update_palette(cache, lang: str = "zh"):
  function on_color_swatch_click (line 346) | def on_color_swatch_click(selected_hex):
  function on_color_dropdown_select (line 365) | def on_color_dropdown_select(selected_value):
  function on_lut_change_update_colors (line 381) | def on_lut_change_update_colors(lut_path, cache=None):
  function on_preview_update_lut_colors (line 411) | def on_preview_update_lut_colors(cache, lut_path):
  function on_lut_color_swatch_click (line 440) | def on_lut_color_swatch_click(selected_hex):
  function on_replacement_color_select (line 459) | def on_replacement_color_select(selected_value):
  function on_highlight_color_change (line 479) | def on_highlight_color_change(highlight_hex, cache, loop_pos, add_loop,
  function on_clear_highlight (line 517) | def on_clear_highlight(cache, loop_pos, add_loop,
  function on_delete_selected_user_replacement (line 552) | def on_delete_selected_user_replacement(
  function on_undo_color_replacement (line 621) | def on_undo_color_replacement(cache, replacement_regions, replacement_hi...
  function run_extraction_wrapper (line 647) | def run_extraction_wrapper(img, points, offset_x, offset_y, zoom, barrel...
  function merge_8color_data (line 702) | def merge_8color_data():
  function merge_5color_extended_data (line 742) | def merge_5color_extended_data():
  function on_merge_lut_select (line 788) | def on_merge_lut_select(display_name, lang="zh"):
  function on_merge_primary_select (line 813) | def on_merge_primary_select(display_name, lang="zh"):
  function on_merge_secondary_change (line 885) | def on_merge_secondary_change(selected_names, lang="zh"):
  function on_merge_execute (line 915) | def on_merge_execute(primary_name, secondary_names, dedup_threshold, lan...
  function on_merge_preview (line 1004) | def on_merge_preview(cache, merge_enable, merge_threshold, merge_max_dis...
  function on_merge_apply (line 1110) | def on_merge_apply(cache, merge_map, merge_stats, loop_pos, add_loop,
  function on_merge_revert (line 1176) | def on_merge_revert(cache, loop_pos, add_loop, loop_width, loop_length, ...

FILE: ui/crop_extension.py
  function get_crop_modal_html (line 9) | def get_crop_modal_html(lang: str) -> str:
  function get_crop_head_js (line 698) | def get_crop_head_js():

FILE: ui/fivecolor_tab_v2.py
  function create_5color_tab_v2 (line 11) | def create_5color_tab_v2(lang="zh"):
  function _get_8color_luts (line 223) | def _get_8color_luts():
  function _format_seq (line 259) | def _format_seq(selected, color_names=None):
  function _empty_colors_html (line 275) | def _empty_colors_html():
  function _generate_colors_html_v2 (line 279) | def _generate_colors_html_v2(base_colors, color_count=None, color_names=...
  function _empty_result (line 375) | def _empty_result():
  function _error_result (line 379) | def _error_result(msg):
  function _result_html (line 383) | def _result_html(result):

FILE: ui/layout_new.py
  function load_last_lut_setting (line 173) | def load_last_lut_setting():
  function save_last_lut_setting (line 189) | def save_last_lut_setting(lut_name):
  function _load_user_settings (line 212) | def _load_user_settings():
  function _save_user_setting (line 223) | def _save_user_setting(key, value):
  function save_color_mode (line 234) | def save_color_mode(color_mode):
  function save_modeling_mode (line 239) | def save_modeling_mode(modeling_mode):
  function resolve_height_mode (line 245) | def resolve_height_mode(radio_value: str) -> str:
  function _scan_registry_for_slicers (line 279) | def _scan_registry_for_slicers():
  function detect_installed_slicers (line 375) | def detect_installed_slicers():
  function open_in_slicer (line 402) | def open_in_slicer(file_path, slicer_id):
  function _get_slicer_choices (line 430) | def _get_slicer_choices(lang="zh"):
  function _get_default_slicer (line 443) | def _get_default_slicer():
  function _slicer_css_class (line 455) | def _slicer_css_class(slicer_id):
  function _get_image_size (line 956) | def _get_image_size(img):
  function calc_height_from_width (line 994) | def calc_height_from_width(width, img):
  function calc_width_from_height (line 1016) | def calc_width_from_height(height, img):
  function init_dims (line 1038) | def init_dims(img):
  function _scale_preview_image (line 1057) | def _scale_preview_image(img, max_w: int = 1200, max_h: int = 750):
  function _preview_update (line 1085) | def _preview_update(img):
  function process_batch_generation (line 1092) | def process_batch_generation(batch_files, is_batch, single_image, lut_pa...
  function _update_lut_grid (line 1228) | def _update_lut_grid(lut_path, lang, palette_mode="swatch"):
  function _detect_and_enforce_structure (line 1242) | def _detect_and_enforce_structure(lut_path):
  function create_app (line 1259) | def create_app():
  function _get_header_html (line 1675) | def _get_header_html(lang: str) -> str:
  function _get_stats_html (line 1680) | def _get_stats_html(lang: str, stats: dict) -> str:
  function _get_footer_html (line 1692) | def _get_footer_html(lang: str) -> str:
  function _get_all_component_updates (line 1701) | def _get_all_component_updates(lang: str, components: dict) -> list:
  function _get_component_list (line 1879) | def _get_component_list(components: dict) -> list:
  function get_extractor_reference_image (line 1892) | def get_extractor_reference_image(mode_str, page_choice="Page 1"):
  function create_converter_tab_content (line 2009) | def create_converter_tab_content(lang: str, lang_state=None, theme_state...
  function create_calibration_tab_content (line 4699) | def create_calibration_tab_content(lang: str) -> dict:
  function create_extractor_tab_content (line 4804) | def create_extractor_tab_content(lang: str) -> dict:
  function create_merge_tab_content (line 5057) | def create_merge_tab_content(lang: str) -> dict:
  function create_advanced_tab_content (line 5105) | def create_advanced_tab_content(lang: str) -> dict:
  function create_about_tab_content (line 5142) | def create_about_tab_content(lang: str) -> dict:
  function _format_bytes (line 5184) | def _format_bytes(size_bytes: int) -> str:

FILE: ui/palette_extension.py
  function build_hue_filter_bar_html (line 15) | def build_hue_filter_bar_html(lang: str = "zh") -> str:
  function build_search_bar_html (line 53) | def build_search_bar_html(lang: str = "zh") -> str:
  function dedupe_auto_pairs (line 69) | def dedupe_auto_pairs(pairs):
  function generate_palette_html (line 84) | def generate_palette_html(
  function build_selected_dual_color_html (line 196) | def build_selected_dual_color_html(quantized_hex: str = None, matched_he...
  function generate_lut_color_grid_html (line 221) | def generate_lut_color_grid_html(colors: List[dict], selected_color: str...
  function generate_dual_recommendations_html (line 332) | def generate_dual_recommendations_html(recommendations: dict, lang: str ...

FILE: utils/bambu_3mf_writer.py
  class BambuStudio3MFWriter (line 21) | class BambuStudio3MFWriter:
    method __init__ (line 51) | def __init__(self, output_path: str, settings: Optional[Dict] = None, ...
    method add_mesh (line 66) | def add_mesh(self, mesh: trimesh.Trimesh, name: str, color_rgb: tuple):
    method export (line 89) | def export(self):
    method _write_content_types (line 133) | def _write_content_types(self, tmpdir: str):
    method _write_root_rels (line 149) | def _write_root_rels(self, tmpdir: str):
    method _write_main_model (line 159) | def _write_main_model(self, tmpdir: str):
    method _write_model_rels (line 195) | def _write_model_rels(self, tmpdir: str):
    method _write_object_files (line 205) | def _write_object_files(self, tmpdir: str):
    method _write_vertices_stream (line 231) | def _write_vertices_stream(stream, vertices):
    method _write_triangles_stream (line 246) | def _write_triangles_stream(stream, faces):
    method _write_vertices_bytes (line 261) | def _write_vertices_bytes(raw, vertices):
    method _write_triangles_bytes (line 279) | def _write_triangles_bytes(raw, faces):
    method _format_vertices (line 295) | def _format_vertices(vertices):
    method _format_triangles (line 314) | def _format_triangles(faces):
    method _write_single_object (line 332) | def _write_single_object(self, tmpdir: str, obj_id: int, mesh: trimesh...
    method _write_metadata_files (line 379) | def _write_metadata_files(self, tmpdir: str):
    method _write_model_settings (line 396) | def _write_model_settings(self, tmpdir: str):
    method _get_base_config_template (line 438) | def _get_base_config_template(self):
    method _get_minimal_config_template (line 463) | def _get_minimal_config_template(self):
    method _build_filament_arrays (line 477) | def _build_filament_arrays(self, num_colors: int, color_conf: dict):
    method _write_project_settings (line 530) | def _write_project_settings(self, tmpdir: str):
    method _write_slice_info (line 588) | def _write_slice_info(self, tmpdir: str):
    method _write_filament_sequence (line 603) | def _write_filament_sequence(self, tmpdir: str):
    method _write_cut_information (line 612) | def _write_cut_information(self, tmpdir: str):
    method _write_object_file_to_zip (line 631) | def _write_object_file_to_zip(self, zf: zipfile.ZipFile):
    method _create_zip (line 655) | def _create_zip(self, tmpdir: str, include_object_model: bool = False):
    method _generate_uuid (line 670) | def _generate_uuid() -> str:
  function export_scene_with_bambu_metadata (line 676) | def export_scene_with_bambu_metadata(scene: trimesh.Scene, output_path: ...
  function inject_bambu_metadata (line 766) | def inject_bambu_metadata(filepath: str, settings: Optional[Dict],

FILE: utils/color_recipe_logger.py
  class ColorRecipeLogger (line 18) | class ColorRecipeLogger:
    method __init__ (line 23) | def __init__(self, lut_path: str, lut_rgb: np.ndarray, ref_stacks: np....
    method _extract_color_names_from_filename (line 45) | def _extract_color_names_from_filename(self) -> Optional[List[str]]:
    method _get_color_name (line 102) | def _get_color_name(self, material_id: int) -> str:
    method add_mapping (line 128) | def add_mapping(self, original_rgb: Tuple[int, int, int],
    method generate_report (line 165) | def generate_report(self, output_path: str, model_filename: str):
    method create_from_processor (line 257) | def create_from_processor(processor, output_dir: str, model_filename: ...

FILE: utils/helpers.py
  function safe_fix_3mf_names (line 11) | def safe_fix_3mf_names(filepath: str, slot_names: List[str], create_asse...

FILE: utils/lut_manager.py
  class LUTManager (line 14) | class LUTManager:
    method get_all_lut_files (line 41) | def get_all_lut_files(cls) -> dict[str, str]:
    method get_lut_choices (line 85) | def get_lut_choices(cls):
    method infer_color_mode (line 97) | def infer_color_mode(display_name: str, file_path: str) -> str:
    method get_lut_path (line 158) | def get_lut_path(cls, display_name: str) -> str | None:
    method save_uploaded_lut (line 172) | def save_uploaded_lut(cls, uploaded_file, custom_name=None):
    method delete_lut (line 237) | def delete_lut(cls, display_name):

FILE: utils/stats.py
  class Stats (line 11) | class Stats:
    method increment (line 22) | def increment(key: str) -> int:
    method get_all (line 29) | def get_all() -> dict:
    method reset_all (line 33) | def reset_all() -> dict:
    method clear_cache (line 40) | def clear_cache() -> tuple:
    method get_cache_size (line 69) | def get_cache_size() -> int:
    method _load (line 90) | def _load() -> dict:
    method clear_output (line 99) | def clear_output() -> tuple:
    method get_output_size (line 131) | def get_output_size() -> int:
    method _save (line 154) | def _save(data: dict):
Condensed preview — 410 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,742K chars).
[
  {
    "path": ".dockerignore",
    "chars": 217,
    "preview": "__pycache__\n*.pyc\n*.pyo\n*.pyd\n.Python\nenv/\nvenv/\n.venv/\npip-log.txt\npip-delete-this-directory.txt\n.tox/\n.coverage\n.cover"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 6439,
    "preview": "name: Build and Package Lumina Layers\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  workflow_"
  },
  {
    "path": ".gitignore",
    "chars": 5833,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[codz]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packag"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 16885,
    "preview": "# Changelog\n\nAll notable changes to Lumina Studio are documented in this file.\n\n[📖 中文更新日志 / Chinese Changelog](CHANGELOG"
  },
  {
    "path": "CHANGELOG_CN.md",
    "chars": 7995,
    "preview": "# 更新日志\n\nLumina Studio 所有重要变更记录。\n\n[📖 English Changelog / 英文更新日志](CHANGELOG.md)\n\n---\n\n## v1.6.8 (2026-04-30)\n\n### Bug 修复\n-"
  },
  {
    "path": "Dockerfile",
    "chars": 891,
    "preview": "# Use an official Python runtime as a parent image\nFROM python:3.13-slim\n\n# Set the working directory in the container\nW"
  },
  {
    "path": "LICENSE",
    "chars": 35150,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 9280,
    "preview": "<p align=\"center\">\n  <img src=\"logo.png\" width=\"128\" alt=\"Lumina Studio Logo\">\n</p>\n\n<h1 align=\"center\">Lumina Studio</h"
  },
  {
    "path": "README_CN.md",
    "chars": 5847,
    "preview": "<p align=\"center\">\n  <img src=\"logo.png\" width=\"128\" alt=\"Lumina Studio Logo\">\n</p>\n\n<h1 align=\"center\">Lumina Studio</h"
  },
  {
    "path": "api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "api/app.py",
    "chars": 3420,
    "preview": "\"\"\"Lumina Studio API — Application Factory.\nLumina Studio API — 应用工厂模块。\n\nProvides a ``create_app()`` factory function th"
  },
  {
    "path": "api/dependencies.py",
    "chars": 1226,
    "preview": "\"\"\"Lumina Studio API — Dependency Injection.\nLumina Studio API — 依赖注入模块。\n\nGlobal singletons and FastAPI dependency funct"
  },
  {
    "path": "api/file_bridge.py",
    "chars": 2698,
    "preview": "import io\nimport os\nimport tempfile\nfrom typing import Optional\n\nimport numpy as np\nfrom fastapi import UploadFile\nfrom "
  },
  {
    "path": "api/file_registry.py",
    "chars": 2622,
    "preview": "import os\nimport threading\nimport uuid\nfrom typing import Optional, Tuple\n\n\nclass FileRegistry:\n    \"\"\"文件注册表,管理生成文件的 UUI"
  },
  {
    "path": "api/routers/__init__.py",
    "chars": 949,
    "preview": "\"\"\"Lumina Studio API — Router re-exports.\nLumina Studio API — 路由模块的统一导出。\n\nThis package re-exports all domain routers so "
  },
  {
    "path": "api/routers/calibration.py",
    "chars": 3060,
    "preview": "\"\"\"Calibration domain API router.\nCalibration 领域 API 路由模块。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import "
  },
  {
    "path": "api/routers/converter.py",
    "chars": 35049,
    "preview": "\"\"\"Converter domain API router.\nConverter 领域 API 路由模块。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport os"
  },
  {
    "path": "api/routers/extractor.py",
    "chars": 10531,
    "preview": "\"\"\"Extractor domain API router.\nExtractor 领域 API 路由模块。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nfr"
  },
  {
    "path": "api/routers/five_color.py",
    "chars": 4261,
    "preview": "\"\"\"Lumina Studio API — Five-Color Query Router.\nLumina Studio API — 五色组合查询路由。\n\nProvides endpoints for querying base colo"
  },
  {
    "path": "api/routers/health.py",
    "chars": 1050,
    "preview": "\"\"\"Lumina Studio API — Health Check Router.\nLumina Studio API — 健康检查路由。\n\nProvides a ``GET /api/health`` endpoint that re"
  },
  {
    "path": "api/routers/lut.py",
    "chars": 6303,
    "preview": "\"\"\"Lumina Studio API — LUT Management Router.\nLumina Studio API — LUT 管理路由。\n\nProvides endpoints for LUT preset listing, "
  },
  {
    "path": "api/routers/slicer.py",
    "chars": 2356,
    "preview": "\"\"\"Slicer domain API router.\nSlicer 领域 API 路由模块 — 切片软件检测与启动端点。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\n\nfrom "
  },
  {
    "path": "api/routers/system.py",
    "chars": 5228,
    "preview": "\"\"\"Lumina Studio API — System Management Router.\nLumina Studio API — 系统管理路由。\n\nProvides cache cleanup utilities and the `"
  },
  {
    "path": "api/schemas/__init__.py",
    "chars": 2995,
    "preview": "\"\"\"Lumina Studio API — Pydantic schemas and enums re-exports.\nLumina Studio API — Pydantic 数据模型与枚举的统一导出。\n\nThis package r"
  },
  {
    "path": "api/schemas/calibration.py",
    "chars": 2045,
    "preview": "\"\"\"Calibration domain Pydantic schemas and enums.\nCalibration 领域的 Pydantic 数据模型与枚举定义。\n\nThis module defines the request m"
  },
  {
    "path": "api/schemas/converter.py",
    "chars": 13372,
    "preview": "\"\"\"Converter domain Pydantic schemas and enums.\nConverter 领域的 Pydantic 数据模型与枚举定义。\n\nThis module defines all request model"
  },
  {
    "path": "api/schemas/extractor.py",
    "chars": 4320,
    "preview": "\"\"\"Extractor domain Pydantic schemas and enums.\nExtractor 领域的 Pydantic 数据模型与枚举定义。\n\nThis module defines all request model"
  },
  {
    "path": "api/schemas/five_color.py",
    "chars": 1312,
    "preview": "\"\"\"Five-Color Query — Pydantic 数据模型。\n五色组合查询的请求与响应模型定义。\n\"\"\"\n\nfrom typing import Optional\n\nfrom pydantic import BaseModel,"
  },
  {
    "path": "api/schemas/lut.py",
    "chars": 3605,
    "preview": "\"\"\"LUT Manager domain Pydantic schemas.\nLUT 管理领域的 Pydantic 数据模型。\n\nThis module defines request and response models for th"
  },
  {
    "path": "api/schemas/responses.py",
    "chars": 2817,
    "preview": "\"\"\"Lumina Studio API — Response Pydantic models.\nLumina Studio API — 响应 Pydantic 数据模型。\n\nAll API endpoint response schema"
  },
  {
    "path": "api/schemas/slicer.py",
    "chars": 2124,
    "preview": "\"\"\"Slicer domain Pydantic schemas.\nSlicer 领域的 Pydantic 数据模型定义。\n\nThis module defines request and response models for the "
  },
  {
    "path": "api/schemas/system.py",
    "chars": 1401,
    "preview": "\"\"\"Lumina Studio API — System Pydantic models.\nLumina Studio API — 系统管理 Pydantic 数据模型。\n\nCache cleanup response schemas a"
  },
  {
    "path": "api/session_store.py",
    "chars": 3078,
    "preview": "import threading\nimport time\nimport uuid\nimport os\nfrom typing import Any, Dict, Optional\n\n\nclass SessionStore:\n    \"\"\"服"
  },
  {
    "path": "api/worker_pool.py",
    "chars": 3215,
    "preview": "\"\"\"WorkerPoolManager — ProcessPoolExecutor lifecycle manager.\nWorkerPoolManager — ProcessPoolExecutor 生命周期管理器。\n\nProvides"
  },
  {
    "path": "api/workers/__init__.py",
    "chars": 285,
    "preview": "\"\"\"Lumina Studio API — Worker functions for ProcessPoolExecutor.\nLumina Studio API — 用于 ProcessPoolExecutor 的工作函数模块。\n\nAl"
  },
  {
    "path": "api/workers/converter_workers.py",
    "chars": 8033,
    "preview": "\"\"\"Top-level worker functions for converter CPU tasks.\nConverter CPU 任务的顶层工作函数。\n\nThese functions run in separate process"
  },
  {
    "path": "api_server.py",
    "chars": 500,
    "preview": "\"\"\"Lumina Studio API — Server Entry Point.\nLumina Studio API — 服务启动入口。\n\nMinimal entry script that imports the FastAPI ap"
  },
  {
    "path": "bambu_config_template.json",
    "chars": 70179,
    "preview": "{\n    \"accel_to_decel_enable\": \"0\",\n    \"accel_to_decel_factor\": \"50%\",\n    \"activate_air_filtration\": [\n        \"0\",\n  "
  },
  {
    "path": "benchmark.py",
    "chars": 11894,
    "preview": "\"\"\"\nLumina Studio — headless benchmark script.\n\nRuns both SVG mode (Lanz1.svg) and High-Fidelity mode (Lanz2.jpg) withou"
  },
  {
    "path": "config.py",
    "chars": 13214,
    "preview": "\"\"\"Lumina Studio configuration: paths, printer/smart config, and legacy i18n data.\"\"\"\n\nimport os\nimport sys\nimport platf"
  },
  {
    "path": "core/__init__.py",
    "chars": 1611,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nLumina Studio - Core Module (Refactored)\n核心算法模块 - 重构版本\n\"\"\"\n\n# Calibration module\nfrom .calib"
  },
  {
    "path": "core/calibration.py",
    "chars": 51689,
    "preview": "\"\"\"\nLumina Studio - Calibration Generator Module\n\nGenerates calibration boards for physical color testing.\n\"\"\"\n\nimport o"
  },
  {
    "path": "core/color_analyzer.py",
    "chars": 12886,
    "preview": "\"\"\"\nLumina Studio - Color Analyzer\n\n分析图片复杂度,推荐最佳量化颜色数。\n独立模块,可单独测试和调用。\n\"\"\"\n\nimport os\nimport time\nfrom collections import"
  },
  {
    "path": "core/color_matching_hue_aware.py",
    "chars": 6566,
    "preview": "\"\"\"\n色相感知颜色匹配器 (Hue-Aware Color Matcher)\n\n核心思想:在 LCH 色彩空间中使用加权距离进行颜色匹配。\n\nCIELAB 的 L*a*b* 用欧氏距离时,亮度差异容易压过色相差异,\n导致浅粉色匹配到白色而"
  },
  {
    "path": "core/color_merger.py",
    "chars": 15389,
    "preview": "\"\"\"\nLumina Studio - Color Merger\n\nIntelligent color merger for simplifying color palettes.\nIdentifies low-usage colors a"
  },
  {
    "path": "core/color_replacement.py",
    "chars": 6797,
    "preview": "\"\"\"\nLumina Studio - Color Replacement Manager\n\nManages color replacement mappings for preview and final model generation"
  },
  {
    "path": "core/converter.py",
    "chars": 176283,
    "preview": "\"\"\"\nLumina Studio - Image Converter Coordinator (Refactored)\n\nCoordinates modules to complete image-to-3D model conversi"
  },
  {
    "path": "core/extractor.py",
    "chars": 14925,
    "preview": "\"\"\"\nLumina Studio - Color Extractor Module\n\nExtracts color data from printed calibration boards.\n\"\"\"\n\nimport os\nimport n"
  },
  {
    "path": "core/five_color_combination.py",
    "chars": 13597,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\"\"\"\n5色组合查询功能 - 核心模块\n\n从 8 个基础颜色中选择 5 次(可重复),查询对应的结果颜色。\n\"\"\"\n\nimport numpy as"
  },
  {
    "path": "core/geometry_utils.py",
    "chars": 6786,
    "preview": "\"\"\"\nLumina Studio - Geometry Utilities\nGeometry utilities module - Pure functional geometry calculation tools\n\"\"\"\n\nimpor"
  },
  {
    "path": "core/heightmap_loader.py",
    "chars": 9085,
    "preview": "\"\"\"\nLumina Studio - 高度图加载与处理模块 (Heightmap Loader)\n\n负责高度图的加载、验证、灰度转换、缩放和高度映射。\n灰度映射约定:纯黑(0) = 最大高度,纯白(255) = 最小高度(底板厚度)。\n\""
  },
  {
    "path": "core/i18n.py",
    "chars": 38128,
    "preview": "\"\"\"\nLumina Studio - Internationalization Module\nInternationalization module - Complete Chinese-English translation dicti"
  },
  {
    "path": "core/image_preprocessor.py",
    "chars": 10291,
    "preview": "\"\"\"\nLumina Studio - Image Preprocessor\n\nHandles image cropping and format conversion before main processing.\nIndependent"
  },
  {
    "path": "core/image_processing.py",
    "chars": 42476,
    "preview": "\"\"\"\nLumina Studio - Image Processing Core\n\nHandles image loading, preprocessing, color quantization and matching.\n\"\"\"\n\ni"
  },
  {
    "path": "core/isolated_pixel_cleanup.py",
    "chars": 5885,
    "preview": "\"\"\"\n孤立像素清理模块(Isolated Pixel Cleanup)\n\n在 LUT 颜色匹配之后、voxel matrix 构建之前,对 material_matrix 执行孤立像素检测与替换。\n孤立像素是指其 5 层材料堆叠编码与所有"
  },
  {
    "path": "core/lut_merger.py",
    "chars": 15583,
    "preview": "\"\"\"\nLumina Studio - LUT Merger Engine\n\nCore module for merging LUT color cards from different color modes.\nSupports BW(2"
  },
  {
    "path": "core/mesh_generators.py",
    "chars": 16475,
    "preview": "\"\"\"\nLumina Studio - Mesh Generation Strategies (Refactored v2.2)\nMesh generation strategy module - Refactored version\n\nA"
  },
  {
    "path": "core/naming.py",
    "chars": 5630,
    "preview": "\"\"\"Naming_Service — 统一的文件命名服务模块。\n\n负责生成 Lumina Studio 中所有输出文件的标准化文件名,\n包含时间戳和模式信息,便于用户识别和管理生成的文件。\n\"\"\"\n\nimport re\nfrom date"
  },
  {
    "path": "core/slicer.py",
    "chars": 7913,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nSlicer detection and launch module.\n\nExtracted from ui/layout_new.py — pure business logic, "
  },
  {
    "path": "core/tray.py",
    "chars": 5309,
    "preview": "\"\"\"\n╔═══════════════════════════════════════════════════════════════════════════════╗\n║                          LUMINA "
  },
  {
    "path": "core/vector_engine.py",
    "chars": 31996,
    "preview": "\"\"\"\nLumina Studio - Native Vector Engine (v2 - Chroma-aligned)\n\nSVG to 3D mesh conversion using vector geometry operatio"
  },
  {
    "path": "docs/api_mapping_blueprint.md",
    "chars": 36180,
    "preview": "# Lumina Studio — Gradio → FastAPI API Mapping Blueprint\n# Lumina Studio — Gradio → FastAPI API 映射蓝图\n\n> 本文档是从 Gradio UI "
  },
  {
    "path": "frontend/README.md",
    "chars": 2555,
    "preview": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLin"
  },
  {
    "path": "frontend/eslint.config.js",
    "chars": 734,
    "preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
  },
  {
    "path": "frontend/index.html",
    "chars": 364,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"/f"
  },
  {
    "path": "frontend/package.json",
    "chars": 1242,
    "preview": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n "
  },
  {
    "path": "frontend/postcss.config.js",
    "chars": 92,
    "preview": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "frontend/scripts/generate-test-glb.mjs",
    "chars": 3756,
    "preview": "/**\n * Generate a minimal valid GLB file (a colored cube) by constructing\n * the binary format directly. No browser APIs"
  },
  {
    "path": "frontend/src/App.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "frontend/src/App.tsx",
    "chars": 7384,
    "preview": "import { useState, useEffect, Suspense, Component } from \"react\";\nimport type { ReactNode } from \"react\";\nimport apiClie"
  },
  {
    "path": "frontend/src/__tests__/App.test.tsx",
    "chars": 1478,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library"
  },
  {
    "path": "frontend/src/__tests__/InteractiveModelViewer.test.ts",
    "chars": 1586,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport {\n  extractHexFromMeshName,\n  toggleColorSelection,\n} from \"../com"
  },
  {
    "path": "frontend/src/__tests__/KeychainRing3D.test.tsx",
    "chars": 3449,
    "preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { render } from \"@testing-library/react\";\nimport { createKeych"
  },
  {
    "path": "frontend/src/__tests__/LutColorGrid.test.ts",
    "chars": 2906,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport { matchesSearch } from \"../components/sections/LutColorGrid\";\nimpo"
  },
  {
    "path": "frontend/src/__tests__/Scene3D.test.tsx",
    "chars": 6711,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, act } from \"@testing-library/rea"
  },
  {
    "path": "frontend/src/__tests__/action-bar-always-visible.property.test.ts",
    "chars": 2883,
    "preview": "/**\n * Feature: action-bar-always-visible\n * Property-Based Tests for SlicerSelector disabled state computation\n *\n * Us"
  },
  {
    "path": "frontend/src/__tests__/action-bar-always-visible.test.ts",
    "chars": 3573,
    "preview": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { useConverterStore } from '../stores/converterSto"
  },
  {
    "path": "frontend/src/__tests__/api-client.property.test.ts",
    "chars": 1548,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport fc from \"fast-check\";\nimport apiClient from \"../api/client\";\n\ndesc"
  },
  {
    "path": "frontend/src/__tests__/app-tabs.test.tsx",
    "chars": 3655,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-libra"
  },
  {
    "path": "frontend/src/__tests__/autoPreview.test.ts",
    "chars": 4443,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { renderHook, act } from \"@testing-libr"
  },
  {
    "path": "frontend/src/__tests__/bed-size-selector.property.test.ts",
    "chars": 1785,
    "preview": "import { describe, it, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useConverterStore } from \""
  },
  {
    "path": "frontend/src/__tests__/calibration-hooks.property.test.ts",
    "chars": 6602,
    "preview": "import { describe, it, afterEach } from \"vitest\";\nimport { renderHook, act } from \"@testing-library/react\";\nimport * as "
  },
  {
    "path": "frontend/src/__tests__/calibration-panel.test.tsx",
    "chars": 5179,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "frontend/src/__tests__/calibration-store.property.test.ts",
    "chars": 11520,
    "preview": "import { describe, it } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { clampValue } from \"../stores/converter"
  },
  {
    "path": "frontend/src/__tests__/colorRemap.property.test.ts",
    "chars": 6897,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useConverterStore "
  },
  {
    "path": "frontend/src/__tests__/colorSelect.property.test.ts",
    "chars": 1603,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport {\n  extractHexFromMeshName,\n  to"
  },
  {
    "path": "frontend/src/__tests__/converter-api.test.ts",
    "chars": 7309,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport apiClient from \"../api/client\";\nimport {\n  convert"
  },
  {
    "path": "frontend/src/__tests__/converter-store.property.test.ts",
    "chars": 10843,
    "preview": "import { describe, it, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport {\n  useConverterStore,\n  cla"
  },
  {
    "path": "frontend/src/__tests__/extractor-5color.property.test.ts",
    "chars": 6731,
    "preview": "import { describe, it, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useExtractorStore } fr"
  },
  {
    "path": "frontend/src/__tests__/extractor-api.property.test.ts",
    "chars": 7157,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useExtractorSt"
  },
  {
    "path": "frontend/src/__tests__/extractor-canvas.test.tsx",
    "chars": 8957,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, cleanup } from \"@testing-library"
  },
  {
    "path": "frontend/src/__tests__/extractor-panel.test.tsx",
    "chars": 6828,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, cleanup } from \"@testing-library"
  },
  {
    "path": "frontend/src/__tests__/extractor-store.property.test.ts",
    "chars": 18215,
    "preview": "import { describe, it, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useExtractorStore } fr"
  },
  {
    "path": "frontend/src/__tests__/five-color-store.property.test.ts",
    "chars": 7754,
    "preview": "import { describe, it, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useFiveColorStore } fr"
  },
  {
    "path": "frontend/src/__tests__/health-status.property.test.tsx",
    "chars": 2483,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, waitFor, cleanup } from \"@testin"
  },
  {
    "path": "frontend/src/__tests__/keychainRing.property.test.ts",
    "chars": 2236,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { createKeychainRingGeometry } f"
  },
  {
    "path": "frontend/src/__tests__/layout.test.tsx",
    "chars": 2062,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "frontend/src/__tests__/loading-spinner.test.tsx",
    "chars": 632,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport LoadingSp"
  },
  {
    "path": "frontend/src/__tests__/lut-manager-panel.test.tsx",
    "chars": 4062,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "frontend/src/__tests__/lut-manager-refresh.property.test.ts",
    "chars": 4480,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\n\n// Mock API modules BE"
  },
  {
    "path": "frontend/src/__tests__/lut-manager-store.property.test.ts",
    "chars": 5384,
    "preview": "import { describe, it } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { filterSecondaryOptions } from \"../stor"
  },
  {
    "path": "frontend/src/__tests__/lutColorFilter.property.test.ts",
    "chars": 7695,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport {\n  matchesSearch,\n  classifyHue"
  },
  {
    "path": "frontend/src/__tests__/model-centering.property.test.ts",
    "chars": 2107,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport fc from \"fast-check\";\nimport { computeCenterOffset } from \"../comp"
  },
  {
    "path": "frontend/src/__tests__/paletteLutMerge.property.test.ts",
    "chars": 11032,
    "preview": "/**\n * Property-Based Tests: ColorWorkstation (Palette + LUT Merge)\n *\n * Feature: palette-lut-merge\n * Tests the correc"
  },
  {
    "path": "frontend/src/__tests__/paletteLutMerge.test.tsx",
    "chars": 4911,
    "preview": "/**\n * Unit tests for Palette-LUT Merge (ColorWorkstation).\n * 调色板-LUT 合并(ColorWorkstation)单元测试。\n *\n * Validates registr"
  },
  {
    "path": "frontend/src/__tests__/paletteOptimization.property.test.ts",
    "chars": 13202,
    "preview": "import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';\nimport * as fc from 'fast-check';\nim"
  },
  {
    "path": "frontend/src/__tests__/paletteOptimization.test.tsx",
    "chars": 8599,
    "preview": "import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { render, screen, fireEvent } from '@testing-libra"
  },
  {
    "path": "frontend/src/__tests__/realtimePreview.property.test.ts",
    "chars": 8268,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { computeThicknessScale } from \""
  },
  {
    "path": "frontend/src/__tests__/realtimePreview.test.ts",
    "chars": 1592,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport { computeThicknessScale } from \"../utils/scaleUtils\";\n"
  },
  {
    "path": "frontend/src/__tests__/reliefHeight.property.test.ts",
    "chars": 7909,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { computeAutoHeightM"
  },
  {
    "path": "frontend/src/__tests__/replace-preview.property.test.ts",
    "chars": 5379,
    "preview": "import { describe, it, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useConverterStore } fr"
  },
  {
    "path": "frontend/src/__tests__/scaleUtils.property.test.ts",
    "chars": 5444,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { computeScaleFactor } from \"../"
  },
  {
    "path": "frontend/src/__tests__/settings-store.property.test.ts",
    "chars": 6289,
    "preview": "import { describe, it, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { useSettingsStore, DEFAULT_"
  },
  {
    "path": "frontend/src/__tests__/slicer.property.test.ts",
    "chars": 19673,
    "preview": "/**\n * Feature: slicer-launch-integration\n * Property-Based Tests for slicer launch integration\n *\n * Uses Vitest + fast"
  },
  {
    "path": "frontend/src/__tests__/stack-positions-nonoverlap.property.test.ts",
    "chars": 5068,
    "preview": "/**\n * Property-Based Test: computeStackPositions 非重叠性\n * Stack positions computed by computeStackPositions never overla"
  },
  {
    "path": "frontend/src/__tests__/tab-filter.property.test.ts",
    "chars": 3359,
    "preview": "/**\n * Property-Based Test: TAB 页面过滤正确性\n * TAB page filtering correctness.\n *\n * Feature: granular-floating-widgets, Pro"
  },
  {
    "path": "frontend/src/__tests__/tab-switch-layout.property.test.ts",
    "chars": 5632,
    "preview": "/**\n * Property-Based Test: TAB 切换保持 Widget 布局不变性\n * TAB switching preserves widget layout invariance.\n *\n * Feature: gr"
  },
  {
    "path": "frontend/src/__tests__/theme.property.test.ts",
    "chars": 3944,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { THEME_CONFIG } from \"../compon"
  },
  {
    "path": "frontend/src/__tests__/theme.test.tsx",
    "chars": 2655,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/r"
  },
  {
    "path": "frontend/src/__tests__/ui-components.test.tsx",
    "chars": 2230,
    "preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\ni"
  },
  {
    "path": "frontend/src/__tests__/widget-drag-perf.property.test.ts",
    "chars": 17131,
    "preview": "/**\n * Property-Based Tests for Widget drag performance optimizations.\n * Widget 拖拽性能优化 Property-Based 测试。\n *\n * Tests p"
  },
  {
    "path": "frontend/src/__tests__/widget-registry-i18n.property.test.ts",
    "chars": 1904,
    "preview": "/**\n * Property-Based Test: WIDGET_REGISTRY titleKey 与 i18n 一致性\n * WIDGET_REGISTRY titleKey consistency with i18n transl"
  },
  {
    "path": "frontend/src/__tests__/widget-workspace.property.test.ts",
    "chars": 15469,
    "preview": "/**\n * Property-Based Tests for the floating widget workspace.\n * 浮动 Widget 工作区 Property-Based 测试。\n *\n * This file conta"
  },
  {
    "path": "frontend/src/__tests__/widget-workspace.test.tsx",
    "chars": 4776,
    "preview": "/**\n * Unit tests for Widget workspace components.\n * Widget 工作区组件单元测试。\n */\n\nimport { describe, it, expect, vi, beforeEa"
  },
  {
    "path": "frontend/src/__tests__/zoomable-image.property.test.ts",
    "chars": 3639,
    "preview": "import { describe, it } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { clampScale } from \"../components/ui/Zo"
  },
  {
    "path": "frontend/src/api/__tests__/batchApi.property.test.ts",
    "chars": 2899,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport type { BatchConv"
  },
  {
    "path": "frontend/src/api/calibration.ts",
    "chars": 418,
    "preview": "import apiClient from \"./client\";\nimport type { CalibrationGenerateRequest, CalibrationResponse } from \"./types\";\n\n/** 提"
  },
  {
    "path": "frontend/src/api/client.ts",
    "chars": 409,
    "preview": "import axios from \"axios\";\n\n/**\n * Axios 实例,统一管理 API 请求基础配置。\n *\n * 注意:不设置默认 Content-Type header。\n * - 发送 JSON 时 axios 自动"
  },
  {
    "path": "frontend/src/api/converter.ts",
    "chars": 4215,
    "preview": "import apiClient from \"./client\";\nimport type {\n  ConvertPreviewRequest,\n  ConvertGenerateRequest,\n  PreviewResponse,\n  "
  },
  {
    "path": "frontend/src/api/extractor.ts",
    "chars": 2030,
    "preview": "import apiClient from \"./client\";\nimport type { ExtractResponse, ManualFixResponse } from \"./types\";\n\n/** 提取颜色 - multipa"
  },
  {
    "path": "frontend/src/api/fiveColor.ts",
    "chars": 659,
    "preview": "import apiClient from \"./client\";\nimport type { BaseColorsResponse, FiveColorQueryResponse, FiveColorQueryRequest } from"
  },
  {
    "path": "frontend/src/api/lut.ts",
    "chars": 606,
    "preview": "import apiClient from \"./client\";\nimport type { LutInfoResponse, MergeRequest, MergeResponse } from \"./types\";\n\n/** 获取指定"
  },
  {
    "path": "frontend/src/api/slicer.ts",
    "chars": 591,
    "preview": "import apiClient from \"./client\";\nimport type {\n  SlicerDetectResponse,\n  SlicerLaunchRequest,\n  SlicerLaunchResponse,\n}"
  },
  {
    "path": "frontend/src/api/system.ts",
    "chars": 985,
    "preview": "import apiClient from \"./client\";\nimport type {\n  ClearCacheResponse,\n  UserSettings,\n  UserSettingsResponse,\n  SaveSett"
  },
  {
    "path": "frontend/src/api/types.ts",
    "chars": 7880,
    "preview": "export interface HealthResponse {\n  status: string;\n  version: string;\n  uptime_seconds: number;\n}\n\n// ========== Enums "
  },
  {
    "path": "frontend/src/components/AboutView.tsx",
    "chars": 1782,
    "preview": "import { useAboutStore } from \"../stores/aboutStore\";\nimport { useI18n } from \"../i18n/context\";\nimport Button from \"./u"
  },
  {
    "path": "frontend/src/components/BedPlatform.tsx",
    "chars": 5457,
    "preview": "import { useMemo, useEffect } from \"react\";\nimport { useThree } from \"@react-three/fiber\";\nimport * as THREE from \"three"
  },
  {
    "path": "frontend/src/components/CalibrationPanel.tsx",
    "chars": 3214,
    "preview": "import { useCalibrationStore } from \"../stores/calibrationStore\";\nimport { useI18n } from \"../i18n/context\";\nimport { Ca"
  },
  {
    "path": "frontend/src/components/ExtractorCanvas.tsx",
    "chars": 11651,
    "preview": "import { useRef, useEffect, useCallback, useState } from \"react\";\nimport { useExtractorStore } from \"../stores/extractor"
  },
  {
    "path": "frontend/src/components/ExtractorPanel.tsx",
    "chars": 5948,
    "preview": "import { useExtractorStore } from \"../stores/extractorStore\";\nimport { useI18n } from \"../i18n/context\";\nimport { Extrac"
  },
  {
    "path": "frontend/src/components/FiveColorCanvas.tsx",
    "chars": 8490,
    "preview": "/**\n * FiveColorCanvas - 五色配方 3D 薄片叠加动画。\n * 用 Canvas 2D 绘制 5 层半透明薄片,选满后播放叠加动画,融合成结果颜色。\n */\n\nimport { useRef, useEffect, "
  },
  {
    "path": "frontend/src/components/FiveColorQueryPanel.tsx",
    "chars": 7687,
    "preview": "import { useEffect, useMemo } from \"react\";\nimport { useFiveColorStore } from \"../stores/fiveColorStore\";\nimport { useCo"
  },
  {
    "path": "frontend/src/components/InteractiveModelViewer.tsx",
    "chars": 18097,
    "preview": "import { useMemo, useEffect, useRef, useCallback } from \"react\";\nimport { useThree, useFrame } from \"@react-three/fiber\""
  },
  {
    "path": "frontend/src/components/KeychainRing3D.tsx",
    "chars": 3268,
    "preview": "/**\n * KeychainRing3D — 3D keychain ring preview component using Three.js ExtrudeGeometry.\n * KeychainRing3D — 使用 Three."
  },
  {
    "path": "frontend/src/components/LanguageToggle.tsx",
    "chars": 652,
    "preview": "import { useSettingsStore } from \"../stores/settingsStore\";\nimport { useI18n } from \"../i18n/context\";\n\nexport function "
  },
  {
    "path": "frontend/src/components/LoadingSpinner.tsx",
    "chars": 320,
    "preview": "function LoadingSpinner() {\n  return (\n    <div\n      data-testid=\"loading-spinner\"\n      className=\"absolute inset-0 fl"
  },
  {
    "path": "frontend/src/components/LutManagerPanel.tsx",
    "chars": 6137,
    "preview": "import { useEffect } from \"react\";\nimport { useLutManagerStore } from \"../stores/lutManagerStore\";\nimport { useI18n } fr"
  },
  {
    "path": "frontend/src/components/ModelViewer.tsx",
    "chars": 3694,
    "preview": "import { useMemo, useEffect } from \"react\";\nimport { useThree } from \"@react-three/fiber\";\nimport { useGLTF } from \"@rea"
  },
  {
    "path": "frontend/src/components/Scene3D.tsx",
    "chars": 9760,
    "preview": "import { Suspense, useRef, useState, useEffect, useCallback } from \"react\";\nimport { Canvas, useThree } from \"@react-thr"
  },
  {
    "path": "frontend/src/components/ThemeToggle.tsx",
    "chars": 983,
    "preview": "import { useEffect } from \"react\";\nimport { useSettingsStore } from \"../stores/settingsStore\";\nimport { useI18n } from \""
  },
  {
    "path": "frontend/src/components/__tests__/ActionBar.batch.test.tsx",
    "chars": 4292,
    "preview": "import { describe, it, expect, beforeEach, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "frontend/src/components/__tests__/BasicSettings.batch.test.tsx",
    "chars": 3207,
    "preview": "import { describe, it, expect, beforeEach, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "frontend/src/components/__tests__/BatchFileUploader.test.tsx",
    "chars": 2864,
    "preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\ni"
  },
  {
    "path": "frontend/src/components/__tests__/BatchResultSummary.property.test.tsx",
    "chars": 5706,
    "preview": "import { describe, it, expect, afterEach } from \"vitest\";\nimport * as fc from \"fast-check\";\nimport { render, screen, cle"
  },
  {
    "path": "frontend/src/components/__tests__/BatchResultSummary.test.tsx",
    "chars": 4878,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport BatchResu"
  },
  {
    "path": "frontend/src/components/lightingConfig.ts",
    "chars": 851,
    "preview": "/** Lighting configuration for the 3D preview scene.\n *  3D 预览场景的光照配置。\n */\nexport const LIGHTING_CONFIG = {\n  /** Enviro"
  },
  {
    "path": "frontend/src/components/sections/ActionBar.tsx",
    "chars": 3516,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport Button from \"../ui/Button\";\nimport BatchResultSu"
  },
  {
    "path": "frontend/src/components/sections/AdvancedSettings.tsx",
    "chars": 1774,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport Sl"
  },
  {
    "path": "frontend/src/components/sections/BasicSettings.tsx",
    "chars": 5799,
    "preview": "import { useShallow } from \"zustand/react/shallow\";\nimport { useConverterStore, isValidImageType } from \"../../stores/co"
  },
  {
    "path": "frontend/src/components/sections/BedSizeSelector.tsx",
    "chars": 678,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport Dr"
  },
  {
    "path": "frontend/src/components/sections/CloisonneSettings.tsx",
    "chars": 1449,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport { "
  },
  {
    "path": "frontend/src/components/sections/CoatingSettings.tsx",
    "chars": 1014,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport Ch"
  },
  {
    "path": "frontend/src/components/sections/KeychainLoopSettings.tsx",
    "chars": 1355,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport Ch"
  },
  {
    "path": "frontend/src/components/sections/LutColorGrid.tsx",
    "chars": 13008,
    "preview": "import { useState, useMemo, useCallback, useEffect } from \"react\";\nimport { useConverterStore } from \"../../stores/conve"
  },
  {
    "path": "frontend/src/components/sections/OutlineSettings.tsx",
    "chars": 1192,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport { useI18n } from \"../../i18n/context\";\nimport { "
  },
  {
    "path": "frontend/src/components/sections/PalettePanel.tsx",
    "chars": 8057,
    "preview": "import { useConverterStore } from \"../../stores/converterStore\";\nimport type { PaletteEntry } from \"../../api/types\";\nim"
  },
  {
    "path": "frontend/src/components/sections/ReliefSettings.tsx",
    "chars": 3726,
    "preview": "import { useCallback } from \"react\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport { useConverterStore } fr"
  },
  {
    "path": "frontend/src/components/sections/SlicerSelector.tsx",
    "chars": 12489,
    "preview": "import { useEffect, useRef, useState } from \"react\";\nimport { useSlicerStore } from \"../../stores/slicerStore\";\nimport {"
  },
  {
    "path": "frontend/src/components/themeConfig.ts",
    "chars": 1573,
    "preview": "/**\n * Theme configuration for light and dark modes.\n * 日间/夜间模式的主题配置。\n *\n * Centralizes all visual parameters for 3D sce"
  },
  {
    "path": "frontend/src/components/ui/Accordion.tsx",
    "chars": 1068,
    "preview": "import { useState, type ReactNode } from \"react\";\n\ninterface AccordionProps {\n  title: string;\n  defaultOpen?: boolean;\n"
  },
  {
    "path": "frontend/src/components/ui/BatchFileUploader.tsx",
    "chars": 3657,
    "preview": "import { useState, useRef, useCallback } from \"react\";\nimport { useI18n } from \"../../i18n/context\";\n\ninterface BatchFil"
  },
  {
    "path": "frontend/src/components/ui/BatchResultSummary.tsx",
    "chars": 2832,
    "preview": "import type { BatchResponse } from \"../../api/types\";\nimport { useI18n } from \"../../i18n/context\";\n\ninterface BatchResu"
  },
  {
    "path": "frontend/src/components/ui/Button.tsx",
    "chars": 1388,
    "preview": "interface ButtonProps {\n  label: string;\n  onClick: () => void;\n  disabled?: boolean;\n  loading?: boolean;\n  variant?: \""
  },
  {
    "path": "frontend/src/components/ui/Checkbox.tsx",
    "chars": 782,
    "preview": "interface CheckboxProps {\n  label: string;\n  checked: boolean;\n  onChange: (checked: boolean) => void;\n  disabled?: bool"
  },
  {
    "path": "frontend/src/components/ui/ColorModeBadge.tsx",
    "chars": 2764,
    "preview": "interface ColorModeBadgeProps {\n  mode: string;\n}\n\n// Filament dot colors per mode  (hex strings for inline style)\nconst"
  },
  {
    "path": "frontend/src/components/ui/CropModal.tsx",
    "chars": 11516,
    "preview": "import \"cropperjs/dist/cropper.css\";\n\nimport { useState, useRef, useEffect, useCallback } from \"react\";\nimport { createP"
  },
  {
    "path": "frontend/src/components/ui/Dropdown.tsx",
    "chars": 1237,
    "preview": "import { useId } from \"react\";\n\ninterface DropdownProps {\n  label: string;\n  value: string;\n  options: { label: string; "
  },
  {
    "path": "frontend/src/components/ui/FullScreenModal.tsx",
    "chars": 2306,
    "preview": "/**\n * FullScreenModal - 接近全屏的弹窗容器。\n * 用于承载校准、提取器、LUT管理、配方查询等独立操作面板。\n */\n\nimport { useEffect, useRef, useCallback } from"
  },
  {
    "path": "frontend/src/components/ui/ImageUpload.tsx",
    "chars": 2440,
    "preview": "import { useState, useRef, useCallback } from \"react\";\nimport { useI18n } from \"../../i18n/context\";\n\ninterface ImageUpl"
  },
  {
    "path": "frontend/src/components/ui/RadioGroup.tsx",
    "chars": 1340,
    "preview": "import { useId } from \"react\";\n\ninterface RadioGroupProps {\n  label: string;\n  value: string;\n  options: { label: string"
  },
  {
    "path": "frontend/src/components/ui/Slider.tsx",
    "chars": 1569,
    "preview": "import { useId } from \"react\";\n\ninterface SliderProps {\n  label: string;\n  value: number;\n  min: number;\n  max: number;\n"
  },
  {
    "path": "frontend/src/components/ui/ZoomableImage.tsx",
    "chars": 3689,
    "preview": "import { useState, useRef, useCallback, type WheelEvent, type MouseEvent } from \"react\";\nimport { useI18n } from \"../../"
  },
  {
    "path": "frontend/src/components/widget/ActionBarWidgetContent.tsx",
    "chars": 272,
    "preview": "/**\n * Action bar widget content wrapper.\n * 操作栏 Widget 内容包装组件。\n */\n\nimport ActionBar from '../sections/ActionBar';\n\nexp"
  },
  {
    "path": "frontend/src/components/widget/AdvancedSettingsWidgetContent.tsx",
    "chars": 308,
    "preview": "/**\n * Advanced settings widget content wrapper.\n * 高级设置 Widget 内容包装组件。\n */\n\nimport AdvancedSettings from '../sections/A"
  },
  {
    "path": "frontend/src/components/widget/BasicSettingsWidgetContent.tsx",
    "chars": 293,
    "preview": "/**\n * Basic settings widget content wrapper.\n * 基础设置 Widget 内容包装组件。\n */\n\nimport BasicSettings from '../sections/BasicSe"
  },
  {
    "path": "frontend/src/components/widget/CalibrationWidgetContent.tsx",
    "chars": 282,
    "preview": "/**\n * Calibration widget content wrapper.\n * 校准 Widget 内容包装组件。\n */\n\nimport CalibrationPanel from '../CalibrationPanel';"
  },
  {
    "path": "frontend/src/components/widget/CloisonneSettingsWidgetContent.tsx",
    "chars": 315,
    "preview": "/**\n * Cloisonné settings widget content wrapper.\n * 掐丝珐琅设置 Widget 内容包装组件。\n */\n\nimport CloisonneSettings from '../sectio"
  },
  {
    "path": "frontend/src/components/widget/CoatingSettingsWidgetContent.tsx",
    "chars": 303,
    "preview": "/**\n * Coating settings widget content wrapper.\n * 涂层设置 Widget 内容包装组件。\n */\n\nimport CoatingSettings from '../sections/Coa"
  },
  {
    "path": "frontend/src/components/widget/ColorWorkstation.tsx",
    "chars": 3919,
    "preview": "/**\n * ColorWorkstation — fixed bottom-center composite panel for PalettePanel + LutColorGrid.\n * ColorWorkstation — 固定在"
  },
  {
    "path": "frontend/src/components/widget/ExtractorWidgetContent.tsx",
    "chars": 273,
    "preview": "/**\n * Extractor widget content wrapper.\n * 提取器 Widget 内容包装组件。\n */\n\nimport ExtractorPanel from '../ExtractorPanel';\n\nexp"
  },
  {
    "path": "frontend/src/components/widget/FiveColorWidgetContent.tsx",
    "chars": 296,
    "preview": "/**\n * Five-Color Query widget content wrapper.\n * 配方查询 Widget 内容包装组件。\n */\n\nimport FiveColorQueryPanel from '../FiveColo"
  },
  {
    "path": "frontend/src/components/widget/KeychainLoopWidgetContent.tsx",
    "chars": 322,
    "preview": "/**\n * Keychain loop settings widget content wrapper.\n * 挂件环设置 Widget 内容包装组件。\n */\n\nimport KeychainLoopSettings from '../"
  },
  {
    "path": "frontend/src/components/widget/LutManagerWidgetContent.tsx",
    "chars": 283,
    "preview": "/**\n * LUT Manager widget content wrapper.\n * LUT 管理器 Widget 内容包装组件。\n */\n\nimport LutManagerPanel from '../LutManagerPane"
  },
  {
    "path": "frontend/src/components/widget/OutlineSettingsWidgetContent.tsx",
    "chars": 303,
    "preview": "/**\n * Outline settings widget content wrapper.\n * 轮廓设置 Widget 内容包装组件。\n */\n\nimport OutlineSettings from '../sections/Out"
  },
  {
    "path": "frontend/src/components/widget/ReliefSettingsWidgetContent.tsx",
    "chars": 298,
    "preview": "/**\n * Relief settings widget content wrapper.\n * 浮雕设置 Widget 内容包装组件。\n */\n\nimport ReliefSettings from '../sections/Relie"
  },
  {
    "path": "frontend/src/components/widget/SnapGuides.tsx",
    "chars": 2252,
    "preview": "/**\n * Snap guide lines rendered during widget drag near screen edges.\n * Uses RAF polling of refs to avoid re-renders f"
  },
  {
    "path": "frontend/src/components/widget/TabNavBar.tsx",
    "chars": 1460,
    "preview": "/**\n * TabNavBar - Top navigation bar for switching between TAB pages.\n * 顶部导航栏组件,用于在大 TAB 页面之间切换。\n */\nimport { useI18n "
  },
  {
    "path": "frontend/src/components/widget/WidgetHeader.tsx",
    "chars": 1896,
    "preview": "/**\n * Widget header component with drag handle, collapse toggle, and ARIA support.\n * Widget 标题栏组件,支持拖拽手柄、折叠切换和 ARIA 无障"
  },
  {
    "path": "frontend/src/components/widget/WidgetPanel.tsx",
    "chars": 7315,
    "preview": "/**\n * Widget panel component with drag, collapse animation, and frosted glass effect.\n * Widget 面板组件,支持拖拽、折叠动画和毛玻璃效果。\n "
  },
  {
    "path": "frontend/src/components/widget/WidgetWorkspace.tsx",
    "chars": 14511,
    "preview": "/**\n * Widget workspace container with DnD context and snap guides.\n * Widget 工作区容器,包含拖拽上下文和吸附引导线。\n *\n * Wraps all widge"
  }
]

// ... and 210 more files (download for full content)

About this extraction

This page contains the full source code of the MOVIBALE/Lumina-Layers GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 410 files (2.4 MB), approximately 647.9k tokens, and a symbol index with 1617 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!