diff --git a/.gitattributes b/.gitattributes index 94430f8a..6418a28b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ tests/assets/performance/**/*.3mf filter=lfs diff=lfs merge=lfs -text +src-tauri/linux/*.sh text eol=lf diff --git a/.gitignore b/.gitignore index a8a2423e..87369bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dist dist-ssr playwright-report/ test-results/ +tmp/ *.local # Editor directories and files diff --git a/CHANGELOG.md b/CHANGELOG.md index bbce21df..987c4b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to Kromacut are documented in this file. ## Unreleased +## v4.2.0 - 2026-10-19 + +### Upgrade notes + +- **Saved smoothing settings carry over.** Smooth Meshing that was on becomes **Medium**, which builds the same geometry as before; Smooth Meshing that was off becomes **None**. Choose another strength and rebuild to change the result. +- **Rebuild Height dithering and Flat Paint models before printing.** Height dithering now produces neighboring-layer patterns when enabled, and Flat Paint resolves diagonal color contacts differently, so rebuilt previews and exports can differ from 4.1.0 results. Check the new model in your slicer. +- **Opening files from the desktop needs an installed app.** Desktop packages register `.kfil`, legacy `.kapp`, and `.kpal` files with Kromacut; portable Linux AppImages open them only after desktop integration. Existing profiles and palettes remain compatible. + +### Added + +- **Legal pages in Settings** - Privacy & local data and Terms & conditions are accessible from Settings in both the web and desktop apps. Links follow the selected language and open in a new browser tab or the desktop system browser, keeping the workspace open. +- **Smooth Meshing with Flat Paint** - All smoothing strengths now work with face-down and face-up Flat Paint. Shared color boundaries, carrier, and backing stay joined while the slab remains flat. Small shared joins resolve diagonal corner contacts without overlapping color regions. Boundary preparation and cap generation yield to keep dense images and long slabs responsive. Ordinary smooth meshing retains its previous geometry. Updated localized usage and export guides. Print instructions and 3MF metadata record the built strength; added topology, responsiveness, export, and workflow regressions. +- **Smooth Meshing strengths** - Choose None, Minimal, Medium, or Aggressive for preview and exported geometry. Medium preserves the previous enabled result; saved on/off settings migrate to Medium/None. Aggressive smooths remaining diagonal stair steps with a stable finishing pass inside the existing movement limit, while avoiding redundant boundary-map copies between passes. Print instructions and 3MF metadata record the built strength. Added topology and export regression coverage for every preset, strength-ordering checks across diagonal orientations, and a repeatable comparison benchmark. +- **Desktop file associations** - Desktop packages register `.kfil` and legacy `.kapp` filament profiles and `.kpal` palettes with Kromacut file icons. Opening a file launches Kromacut or delivers it to the existing window, imports through the normal validation/migration and calibration-preservation rules, and selects the resulting saved item, including existing content duplicates. External imports wait until an open palette editor, calibration dialog, profile Rename/Save New form, or filament-row edit is finished or cancelled, preserving drafts and the profile being edited. Name/HD inputs, color and TD-conversion popovers, and pending debounced color updates hold both profile and palette imports; committed changes then use the normal unsaved-edit confirmation. Overlapping manual and desktop imports merge with the latest saved list, preserving files imported while a manual read is pending. Late Palette Proof saves merge into the original profile's latest data without removing newer imports, and cannot recreate a deleted profile or attach old evidence after its filaments change. Multi-profile batches load the final saved version after duplicate handling and later updates. Profile opening protects unsaved filament edits, including newly applied calibration measurements, with a confirmation dialog; unreadable, invalid, oversized, or unpersistable imports report an error. Linux packages include MIME definitions, file-forwarding launchers, document icons, and cache refresh scripts; portable AppImages require desktop integration. Added native queue, import, packaging, and browser regression coverage, including overlapping manual/desktop imports, late proof saves after closing or changing their owner, batch selection, imports queued during editing, saving/cancelling profile forms, and retrying after a profile save failure. +- **README video guides** - Linked the Kromacut tutorial and project introduction in side-by-side video cards with thumbnail previews. + +### Changed + +- **README organization** - Reworked the README into a concise product overview with a two-column grid of six finished prints, shorter feature descriptions, and direct documentation and download links. Kept community badges in the header, removed the obsolete TD test-image example, and separated desktop downloads from development setup. + +### Fixed + +- **Flat Paint preview and layer inspection** - Unsmoothed color regions now share diagonal-contact repairs instead of expanding independently and overlapping on the flat artwork face. Precision-sensitive corner repairs now triangulate as smaller pieces with the same footprint, fixing aborted face-up builds that left only dark backing and no layer slider. Failed builds clear incomplete geometry and show an error. Layer Preview cuts through merged backing and color parts one physical layer at a time, including keyboard stepping and wireframe inspection, while exports retain the complete model. Explicit builds regenerate unchanged inputs and restore the full preview and camera instead of leaving a cached mesh or cut on screen. Build progress explicitly counts mesh parts; the Model badge shows physical print layers, including the clear carrier when present. Added shared-boundary, precision, layer-cut, export-preservation, and browser recovery regressions. +- **Desktop development startup on Windows** - Vite no longer watches native sources or Cargo build output, preventing intermittent `EBUSY` crashes when F5 starts the frontend while Rust executables are being linked. Dependency scanning starts from the app entry page instead of generated HTML. Tauri continues to watch native source changes. +- **Height dithering in 3D Auto-paint** - Restored fractional height targets before error diffusion, so enabling Height Dithering can produce neighboring-layer patterns in both preview and exports. Dithering stays local to the selected printable layer, preserves exact calibrated matches and protected boundaries, and leaves ordinary matching unchanged when disabled. Added regression coverage for fractional targets, calibration safeguards, effective line width, and rebuilt STL/3MF geometry. +- **Landing showcase artwork credits** - Original artwork credits such as NASA/JPL link to their sources again in every language; the translated credit used a tag name that rendered an empty link beside plain text. A translation check now rejects tags that cannot hold linked text; landing browser tests now cover every linked credit and the footer language selector before the sticky mobile launch button. + ## v4.1.0 - 2026-09-26 ### Upgrade notes diff --git a/README.md b/README.md index 507a2825..e7cfe85a 100644 --- a/README.md +++ b/README.md @@ -1,372 +1,143 @@ - +# Kromacut logo Kromacut -# Kromacut +**Turn images into multicolor 3D prints.** Prepare artwork, plan filament layers, and export STL or 3MF models with a free, open-source app for your browser, Windows, macOS, and Linux. -[![Patreon](https://img.shields.io/badge/Patreon-Support-orange?logo=patreon&logoColor=white)](https://www.patreon.com/cw/vycdev) [![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/nU63sFMcnX) [![Reddit](https://img.shields.io/badge/Reddit-r%2Fkromacut-FF4500?logo=reddit&logoColor=white)](https://www.reddit.com/r/kromacut/) [![YouTube](https://img.shields.io/badge/YouTube-@vycdev-red?logo=youtube&logoColor=white)](https://www.youtube.com/@vycdev) [![Release](https://img.shields.io/github/v/release/vycdev/kromacut?cacheSeconds=3600)](https://github.com/vycdev/Kromacut/releases/latest) [![Repo size](https://img.shields.io/github/repo-size/vycdev/kromacut?cacheSeconds=3600)](https://github.com/vycdev/Kromacut) [![Total downloads](https://img.shields.io/github/downloads/vycdev/Kromacut/total?label=total%20downloads&cacheSeconds=3600)](https://github.com/vycdev/Kromacut/releases) [![Latest downloads](https://img.shields.io/github/downloads/vycdev/Kromacut/latest/total?cacheSeconds=3600)](https://github.com/vycdev/Kromacut/releases/latest) +**[Open the app](https://kromacut.com/app)** | **[Download for desktop](https://github.com/vycdev/Kromacut/releases)** | [Documentation](https://kromacut.com/docs/overview) | [Website](https://kromacut.com/) -Open-source HueForge-style tool for converting images into stacked, color-layered 3D prints. - -**Try Kromacut in your browser:** [landing page](https://kromacut.com/) · [open the app](https://kromacut.com/app) · [read the docs](https://kromacut.com/docs/overview) · [download desktop releases](https://github.com/vycdev/Kromacut/releases) - -Kromacut is a browser-first app that converts images into multi-color lithophane 3D prints. It offers two powerful workflows: - -**Auto-paint mode** — Define your actual filaments (color + hiding distance), and Kromacut automatically computes optimal layer stacks using physically accurate Beer-Lambert optical blending. Features include a calibration wizard, five deterministic optimizer effort tiers, and region weighting for spatial priority. - -**Manual mode** — Reduce images to a small palette, manually tweak per-color layer heights and ordering, and fine-tune every aspect of the stack with complete control. - -Both modes include live 2D/3D previews, layer-by-layer visualization, and export to STL/3MF with detailed print instructions. +[![Release](https://img.shields.io/github/v/release/vycdev/kromacut?cacheSeconds=3600)](https://github.com/vycdev/Kromacut/releases/latest) [![Downloads](https://img.shields.io/github/downloads/vycdev/Kromacut/total?label=downloads&cacheSeconds=3600)](https://github.com/vycdev/Kromacut/releases) [![Patreon](https://img.shields.io/badge/Patreon-Support-orange?logo=patreon&logoColor=white)](https://www.patreon.com/cw/vycdev) [![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/nU63sFMcnX) [![Reddit](https://img.shields.io/badge/Reddit-r%2Fkromacut-FF4500?logo=reddit&logoColor=white)](https://www.reddit.com/r/kromacut/) [![YouTube](https://img.shields.io/badge/YouTube-@vycdev-red?logo=youtube&logoColor=white)](https://www.youtube.com/@vycdev) ## Examples -A quick look at what Kromacut produces from a source image to a printable model: - -- Fuji test image (2D input) → 3D preview → per-color sliced view → real print - -| 2D Input | 3D Preview | -|---|---| -| Fuji 2D Input | Fuji 3D Preview | -| Sliced by Colors | Real Print | -| Fuji Sliced | Printed Result | - -Another minimal test you can try yourself: the included Transmission Distance (TD) test image (`src/assets/tdTest.png`). - -| TD Test | -|---| -| TD Test Image | - -## Notable Features - -- Image upload and preview (drag & drop or file picker). -- Color reduction / quantization with selectable color count. -- Inline color pickers to tweak or replace generated palette colors. -- Per-color slice heights and a configurable base slice height. -- Reorder colors with drag-and-drop to control stack order (darkest → lightest default ordering). -- Live 2D preview and a 3D stacked preview rendered with three.js. -- **Layer-by-layer preview slider** — Interactively visualize how the print builds up by dragging a slider to show/hide layers at specific heights. -- 3D model export to binary STL or 3MF (Preview) suitable for multi-material slicers. -- Plain-text 3D print instructions that describe layer heights and exact layers where filament swaps are required. -- Copy-to-clipboard button for the print instructions (produces a clean, copyable plain-text plan). -- **Filament calibration** — Accurately measure frontlit hiding distances from a camera-free printed wedge, with per-channel values and confidence scoring. -- **Advanced optimizer** — Five deterministic effort tiers search for optimal filament ordering, from quick previews to exact base-order enumeration. -- **Region weighting** — Prioritize accuracy in specific image areas (center, edges) during auto-paint optimization. - -## Notable implementation details - -- Frontend: React + TypeScript + Vite. -- 3D Rendering: three.js with BufferGeometry. The geometry pipeline produces per-face (non-indexed) triangles so each color slice renders as a solid block (no blended vertex colors) and vertical side walls are preserved. -- Texture sampling: we create a `CanvasTexture` from the preview canvas and use `NearestFilter` with mipmaps disabled so the texture stays crisp when mapped onto the 3D mesh. The texture UVs are adjusted (repeat/offset) so the canvas region aligns with the mesh bounding box. -- Color ordering: palette swatches are derived from image pixels, then ordered by hue/saturation/lightness and presented from darkest to lightest by default (you can reorder manually). -- Async mesh generation: The greedy meshing algorithm yields control back to the browser every 8ms during large mesh builds, keeping the UI responsive. Boundary vertex filtering uses binary search for optimal performance. - - -## How to use - -### Quick start (Auto-paint mode — Recommended) - -1. Upload or drag an image into the preview area. -2. Switch to the **Auto-paint** tab in the 3D controls panel. -3. Click **Add Filament** and configure your filaments: - - Use **Calibrate Filaments** to measure accurate hiding distances from a small printed wedge, or - - Enter hiding distances manually (the convert button accepts conventional backlit TD values). -4. Enable **Enhanced color matching** for optimal results; start with the **Balanced** optimizer tier. -5. (Optional) Set **Region weighting** to Center or Edge to prioritize important areas. -6. Use the **layer-by-layer preview slider** to verify transitions. -7. Export via **Download STL** or **Download 3MF** and follow the print plan. - -See the [Auto-paint section](#auto-paint) for complete details. - -### Manual mode workflow - -1. Upload or drag an image into the preview area. -2. Adjust quantization settings to reduce to the desired number of colors. -3. Tweak or replace swatches using the color pickers in the Swatches panel. -4. Open the 3D panel → **Manual** tab to configure: - - Per-color slice heights - - Base slice height - - Pixel size - - Color order (drag-and-drop) -5. Preview in 3D and use the layer slider to inspect the stack. -6. Click `Download STL` or `Download 3MF` to export your model. -7. Use the `Copy` button to copy the plain-text print plan with layer heights and swap instructions. - -## 3D / printing specifics and tips - -- Base slice height default: `0.20 mm` -- Layer height default: `0.12 mm` -- Layer height used to compute the exact layer numbers at which color swaps happen in the plain-text plan. -- Per-color slice heights are snapped/multiplied to sensible values relative to `layerHeight` when the swatches change or are initialized. - -### Layer-by-layer preview - -The 3D view includes an interactive **height slider** at the bottom that lets you visualize how the print builds up layer by layer: - -- Drag the slider to show/hide layers progressively from base (0mm) to full height -- Current height and layer information displayed in real-time -- Slider step size matches your configured layer height for accurate control -- Helps verify filament swap positions and catch potential issues before printing - -This is especially useful for complex multi-color prints where you need to understand exactly when each color transition occurs. - -## Auto-paint - -Auto-paint is an automated layer-generation mode that replaces the manual palette/swatch workflow. Instead of reducing an image to a fixed number of colors and manually tuning per-color slice heights, you define a set of **filaments** (each with a color and a hiding distance) and let the algorithm compute the optimal layer stack automatically. - -### Core concepts - -- **Filaments**: Each filament has a hex color and a **Hiding Distance (HD)** value (in mm) — the depth of material at which the filament visually hides what's beneath it when viewed front-lit, the way finished prints are actually seen. Dark/opaque filaments hide quickly (e.g. 0.05–0.1 mm); light/translucent filaments need more depth (e.g. 0.5–0.7 mm). When you add a filament without specifying an HD, Kromacut estimates one from the color's luminance and saturation. If you have a conventional (backlit/lithophane) Transmission Distance from a spool sheet or TD test print, the convert button on the filament row turns it into a hiding distance (a conventional TD is roughly 10× the HD). -- **Beer-Lambert optical blending**: The algorithm simulates how light transmits through stacked filament layers using the Beer-Lambert law: `transmission = 10^(-thickness / HD)`. It blends in linear-light sRGB before converting back for display and color matching, which better models the color you see from thin semi-transparent layers. -- **Transition zones**: Each filament in the stack needs enough vertical space to visually transition from the color below it to its own pure color. The algorithm simulates adding layers one at a time until the blended color converges (DeltaE < 2.3 — the "just noticeable difference" threshold) or reaches the selected opacity endpoint. **Compact** uses 80% opacity, **Detailed** uses 90% (one HD), and **Maximum** uses 95% (the same endpoint as the foundation layer). Higher endpoints retain more printable intermediate colors but create taller stacks. -- **Luminance-to-height mapping**: Once the transition zones are computed, each pixel's brightness is mapped to a target height in the model. Dark pixels get the minimum height (base layer only), bright pixels get the full height (all layers), and mid-tones fall proportionally in between. This produces the characteristic lithophane-style relief where image brightness = model thickness. - -### How it works (step by step) - -1. **Define filaments** — Add your filament colors and hiding distances in the Auto-paint tab. Use the color picker and HD input for each filament row (or convert a conventional TD value). -2. **Filament ordering** — By default filaments are sorted by luminance (darkest on the bottom, lightest on top). With **Enhanced color matching** enabled, the algorithm evaluates orderings to find the one that best reproduces the image's color palette. -3. **Transition zone calculation** — For each consecutive pair of filaments, the algorithm simulates Beer-Lambert blending layer-by-layer until the color converges. The first (darkest) filament gets a foundation zone thick enough to be ~95% opaque (`HD × 1.3`). -4. **Height compression** — If the ideal height exceeds a user-set **Max Height**, all zones are uniformly compressed. The UI shows which zones are compressed and by how much. -5. **Virtual swatch generation** — The transition zones are sampled at each layer-height increment to produce a sequence of blended colors (virtual swatches). These drive the 3D preview and the height map, where each pixel maps to the layer whose blended color best matches. - -### Filament Calibration - -Kromacut measures hiding distances with a camera-free printed wedge — no light meter, no photos, no color picking: - -1. **Click Calibrate Filaments** below the filament list and select the filaments to calibrate. -2. **Pick bases** — Quick mode uses one base read per filament; Accurate mode repeats the read over two or three bases so Kromacut can measure per-channel (RGB) hiding distances directly. -3. **Print the wedge** — Download an STL (any printer, one filament swap) or a multi-material 3MF (AMS-ready). Each filament prints as a row of patches from 1 layer up beside a fully opaque reference rail. -4. **Read back one number** — For each print, report the first patch that looks identical to the reference rail. Kromacut converts the reads into measured hiding distances with a confidence score. -5. **Save profile** — Keep calibrated filaments in a reusable profile for future projects. - -Calibrated filaments display a confidence badge next to their HD value. Higher confidence = more accurate optical simulation = better print results. Auto-paint uses calibrated red, green, and blue hiding distances for both preview blending and transition-zone thickness, so calibration can change the generated stack height and swap plan. - -### Advanced Optimizer - -When **Enhanced color matching** is enabled, choose an effort tier for the filament-order search: - -| Tier | Search | Best use | -|---|---|---| -| **Fast** | Narrow deterministic beam search. | A quick preview while tuning settings. | -| **Balanced** | Full deterministic beam search. | Recommended default for most prints. | -| **Thorough** | Full beam plus deeper multi-start refinement. | A stronger search without a long wait. | -| **Deep** | Wider beam plus substantially more deterministic refinement. | Difficult images when Thorough does not improve the stack. | -| **Exact base order** | Enumerates every no-repeat base order. | A guaranteed best base order; the candidate count grows very quickly with each filament. | - -Each higher tier retains the best result from the tier below for the same seed. Exact -base order checks 109,600 sequences at eight filaments and 986,409 at nine, so use it -deliberately on larger profiles. With extra repeated swaps enabled, its base order is exact -but repeated occurrences are still refined heuristically. - -The optimizer displays metadata after generation: -- **Algorithm used** — Which method was selected -- **Quality score** — Lower is better (weighted DeltaE + penalties) -- **Iterations** — How many orderings were evaluated -- **Converged** — Whether the algorithm reached a stable solution -- **Cache hit** — Whether results were retrieved from cache (instant) - -**Optimizer seed** — Leave blank for an automatic stable seed, or enter a number to use a specific repeatable seed for A/B comparisons. - -### Region Weighting - -Prioritize accuracy in specific image regions during optimization: - -| Mode | Description | -|---|---| -| **Uniform** | All pixels weighted equally (default). | -| **Center** | Gaussian falloff from center — faces and subjects in the middle get higher priority. | -| **Edge** | Colors nearer the outer edges of the image get higher priority. | - -Region weighting is most useful when filament budget is limited and you want the optimizer to focus on visually important areas. - -### Options - -| Option | Description | -|---|---| -| **Max Height** | Constrains the total model height (mm). When set below the auto-calculated ideal, zones are uniformly compressed. Leave blank or click `Auto` for the physics-derived default. | -| **Enhanced color matching** | Optimizes the printable filament sequence rather than simply sorting by luminance. It may omit filaments that do not improve the result and evaluates the actual preview color-to-height path. | -| **Extra repeated swaps** | (Requires Enhanced color matching) Lets the optimizer use a filament more than once. Choose Off, 2, 4, 6, 8, or 12 additional occurrences; more repeats may create useful blend paths but substantially expand the search. | -| **Preserve color separation** | (Requires Enhanced color matching) Treats the configurable ΔE value as a hard eligibility limit for distinct printable colors. The optimizer searches repeat tiers progressively and ranks candidates by preserved-color count, source coverage, preserved-match error, repeats, then stack length. Strict mode rejects a result unless every 2D color receives a unique in-limit match; non-strict mode drops unmatched colors by merging their regions into surviving preserved mappings. Mutually exclusive with Height dithering. | -| **Height dithering** | (Requires Enhanced color matching) Applies block-aware Stucki error diffusion to the quantized height map. Instead of sharp stair-steps between layer heights, dithering produces a stippled gradient that simulates intermediate heights, resulting in smoother tonal transitions in the print. Edge pixels between different heights are protected from dithering to avoid staircase artifacts. Mutually exclusive with Preserve color separation. | -| **Flat Paint (flat face-down print)** | Builds a uniform-thickness slab printed image-side down instead of a stepped relief. Each pixel column's layer order is reversed so the artwork sits against the build plate (already mirrored — don't mirror in the slicer) under a transparent carrier layer, and the back is filled with the foundation filament so every layer has the full footprint. The result has a smooth, glass-flat face — great for bookmarks and coasters. Requires a multi-material printer (AMS/toolchanger); export as 3MF, which contains one object per filament plus the clear carrier object. Flat Paint and Smooth Meshing toggle each other off because flat prints always use the full-footprint slab layout. | -| **Dither line width** | (Requires Height dithering) Controls the minimum dot size for the dither pattern in mm. This should roughly match your printer's line/nozzle width so dither dots are actually printable. Default: `0.42 mm`. | -| **Optimizer algorithm** | Choose Fast for a preview, Balanced for the recommended full beam, Thorough or Deep for more search effort, or Exact base order when you want every no-repeat base stack checked. | -| **Transition detail** | Chooses where a color transition stops: Compact at 80% opacity, Detailed at 90%, or Maximum at 95%. Higher detail preserves a longer physical color ramp, increasing model height, swaps, and runtime. | -| **Optimizer seed** | Leave blank for an automatic stable seed, or enter a number for a specific repeatable run. | -| **Region weighting** | Prioritize specific image regions: Uniform (equal), Center (image middle), or Edge (outer image border). Helps focus quality budget on important areas. | - -Enhanced matching uses every color in the palette currently produced by 2D mode; it does not silently reduce that palette again. For detail-critical work, first prepare the image in 2D with K-means (for example, weight 128) and the Auto palette set to the number of colors you want to preserve. More 2D colors improve the target representation but make the optimizer proportionally slower. - -### Filament profiles + + + + + + + + + + + + + +
+ Finished Titan layered print with golden yellow and orange wave reflections on a dark background
+ Titan
+ vycdev | NASA/JPL artwork +
+ A colorful Hobbits and Dragons layered 3D print
+ Hobbits and Dragons
+ u/ominaex25 +
+ Finished Batmanga layered print with a yellow Japanese title, tan background, and black-and-gray Batman figure
+ Batmanga
+ vycdev | Jiro Kuwata / DC artwork +
+ A multicolor layered King of Hearts playing-card print
+ King of Hearts
+ vycdev +
+ Finished Naruto layered print with yellow hair, orange clothing, and a lavender-blue sky
+ Naruto
+ vycdev | Artwork source +
+ Finished layered Hope poster print showing orange and purple-blue color shifts
+ Hope poster
+ vycdev +
+ +## Features + +- **Image editing.** Make non-destructive adjustments, crop and resize, reduce colors, and remove dithering. Retouch artwork with painting, text, and palette tools. +- **Auto-paint.** Plan a filament stack from the colors you own. Hiding Distance measurements help predict how thin layers blend. +- **Manual layers.** Choose the palette, arrange the print order, and set the height of each color. +- **Calibration and profiles.** Measure your filaments, compare printed color samples, and save or share reusable profiles. +- **3D preview.** Inspect each layer, compare simulated and physical colors, and explore shaded, transparent, or wireframe views. +- **STL and 3MF export.** Export STL or color-aware 3MF models, with filament swap instructions for layered prints. +- **Flat Paint.** Create uniform-thickness bookmarks, coasters, and other flat pieces, with 3MF export for multi-material printing. + +## Video guides + + + + + + +
+ Watch the Kromacut tutorial on YouTube
+ Image to 3D print tutorial +
+ Watch the original Kromacut introduction on YouTube
+ Project introduction +
+ +## Documentation + +The [user guide](https://kromacut.com/docs/overview) covers the full app and is also available in the app's documentation panel. + +[Quick start](https://kromacut.com/docs/quick-start) | [Auto-paint](https://kromacut.com/docs/auto-paint) | [Filament calibration](https://kromacut.com/docs/calibration-workflows) | [Exporting and printing](https://kromacut.com/docs/generating-exporting-output) | [Troubleshooting](https://kromacut.com/docs/troubleshooting) + +## Desktop app + +Download the latest version from [GitHub Releases](https://github.com/vycdev/Kromacut/releases/latest). + +| Platform | Package | +| --- | --- | +| Windows | `*-setup.exe` | +| macOS Apple Silicon | `*_aarch64.dmg` | +| macOS Intel | `*_x86_64.dmg` | +| Linux | `*.AppImage` or `*.deb` | + +Windows also has an `*_offline-setup.exe` package with the WebView2 installer included. For platform requirements and installation help, see the [desktop documentation](docs/TAURI.md#distribution-notes). + +## Development + +Built with React, TypeScript, Vite, and Three.js. Desktop builds use Tauri. + +Use **Node.js 22.18+** and npm: -Auto-paint configurations (filament lists) can be saved, loaded, and shared as **profiles**: - -- **Save / Save New** — Persist the current filament set under a name. Overwrite an existing profile or create a new one. -- **Load** — Select a saved profile from the dropdown to restore its filaments. -- **Rename** — Change the selected profile name without changing its filaments. -- **Export** — Download the current filaments as a `.kfil` file (JSON) to share with others. -- **Import** — Load a `.kfil`, legacy `.kapp`, `.json`, or HueForge spool `.csv`/`.tsv` profile file from disk. -- **Delete** — Remove a saved profile. - -Profiles are stored in browser `localStorage` and persist across sessions. - -### Transition zones panel - -When auto-paint is active and filaments are defined, the UI displays a **Transition Zones** panel showing: - -- Each zone's filament color swatch and hex code. -- The start and end height of each zone (in mm) and its thickness (Δ). -- A **compressed** badge on zones that have been reduced below their ideal thickness due to a Max Height constraint. -- Total model height and total number of physical layers. - -### Next-best-color suggestion - -After running auto-paint, a **Suggest next filament** button appears at the bottom of the panel. Click it to run a blend-aware analysis that finds the single filament addition that would most reduce the average color error (ΔE) across the image. - -The result card shows: - -| Field | Description | -|---|---| -| **Hex** | Suggested filament color. | -| **Est. ΔE** | Estimated reduction in blend-aware average ΔE if this filament is added. A rough relative estimate — not a calibration confidence rating. | -| **HD** | Recommended starting hiding distance, borrowed from the nearest existing filament by color distance. Adjust after printing a test patch. | -| **Captures** | Percentage of image pixels whose blend-aware color error would improve with this filament. | -| **Isolation** | How far this color sits from existing filaments in perceptual color space (0–1). Higher means it fills a more distinct gap; lower means it overlaps territory already covered by blending. | - -Click **Add to filaments** to insert the suggestion directly into the filament list (named `Kromacut-Suggestion-01`, `02`, etc.). You can then re-run auto-paint with the expanded set and repeat as many times as needed. - -**How it works (heuristic):** The algorithm estimates how well the current filament set covers the image's color range by checking each image color against each filament and Beer-Lambert blend curve. It identifies underserved colors (those with the largest estimated error) and generates candidates from those colors plus extrapolated positions — colors that, when optically blended with an existing filament, would be closer to the underserved target. The winner is the candidate whose addition most reduces weighted-average estimated error across all image pixels. This is an inventory-planning heuristic: the improvement numbers are relative estimates, not predictions of a specific auto-paint result. - -### Quick start - -1. Load an image into Kromacut. -2. Switch to the **Auto-paint** tab (inside the 3D controls panel). -3. Click **Add Filament** and configure each filament's color and hiding distance to match your real filament stock. - - **Tip:** Use **Calibrate Filaments** to measure accurate hiding distances from a printed wedge. -4. (Optional) Enable **Enhanced color matching** for better results with complex images. Start with the **Balanced** optimizer tier, then use Thorough, Deep, or Exact base order when you want to spend more search effort. -5. (Optional) Set **Region weighting** to Center or Edge to prioritize accuracy in visually important areas. -6. The 3D preview updates automatically. Adjust **Max Height** if the model is too tall. -7. Use the **layer-by-layer preview slider** at the bottom of the 3D view to verify color transitions. -8. Export the model via **Download STL** or **Download 3MF** and follow the printed swap plan. - -## 3MF Export (Preview) - -Kromacut now supports exporting directly to `.3mf` format. This file format preserves color information by splitting the model into separate objects for each color, automatically assigned to different extruders/filaments. - -**Disclaimer:** This feature is currently in **PREVIEW**. While Kromacut preserves colors, the rest of the settings in the slicer profile should be manually adjusted. Please report any issues or weird behaviors you encounter on the GitHub Issues page. - -3MF Export - -## Hiding Distance (HD) and Transmission Distance (TD) - -Stacking thin translucent filament layers produces perceptual intermediate shades — the idea HueForge popularized with Transmission Distance: https://shop.thehueforge.com/blogs/news/what-is-hueforge - -Kromacut models the same physics but stores a **Hiding Distance (HD)**: the depth at which a filament visually hides what's beneath it under front lighting, which is how finished prints are viewed. A conventional TD (measured with a backlit lithophane-style test print) is roughly **10× the hiding distance**; the convert button on each filament row accepts a conventional TD and converts it for you. - -### Two workflow modes in Kromacut: - -**1. Auto-paint mode (Automatic optical blending)** — The recommended approach for layered-color prints: - -- **Automatic Beer-Lambert blending** — Kromacut simulates light transmission through stacked filament layers using physically accurate optical models. -- **Filament-based workflow** — Define your actual filaments (color + hiding distances) and let the algorithm compute optimal layer stacks automatically. -- **Camera-free calibration** — Print a small wedge and read back the layer count where it turns opaque; Kromacut fits measured per-channel hiding distances from the reads. -- **Advanced optimizer** — Deterministic effort tiers search for the best filament ordering for complex images, from quick beam previews to exact base-order enumeration. -- **Transition zones** — Automatically calculated vertical zones where each filament blends from the color below to its own pure color. - -See the [Auto-paint section](#auto-paint) above for full details. - -**2. Manual mode (Traditional workflow)** — For users who want complete control: - -- Reduce your image to a palette with *more colors* than you actually intend to print. The extra colors give you candidate shades to use as intermediate/translucent-looking layers. -- Use the per-color slice heights and ordering controls in the 3D panel to approximate the stacked thicknesses and ordering that would produce the target intermediate shades. -- Small adjustments to `base slice height`, per-color slice heights, and `layerHeight` change the produced layer numbers and perceived blends. -- Iterate with actual filament on small test prints — translucency and perceived mix depend heavily on filament brand, color, and print settings. - -**Recommendation:** Use Auto-paint mode for layered-color prints. It handles the complex Beer-Lambert physics and optimization automatically, producing better results than manual approximation. - -### TD test image (for manual mode experimentation) - -- This repository includes a small test image `tdTest.png` (found in `src/assets/tdTest.png`). -- Load it into the preview with the toolbar upload button or drag-and-drop so you can quickly experiment with manual layer ordering and per-color slice heights. -- This is primarily useful for testing manual mode workflows or understanding how layer heights affect perceived color. - -Quick manual workflow experiment: - -1. Load `tdTest.png` (upload button or drag-and-drop). -2. Reduce colors to your desired number. -3. Switch to Manual mode (3D Controls → Manual tab). -4. Adjust per-color slice heights and ordering. -5. Generate and download your 3D model. -6. Follow the layer color swap instructions and print your 3D model to see results. - -Preview of the included TD test image: - -TD Test Image - -## Native Desktop App (Tauri) +```bash +npm ci +npm run dev +``` -Kromacut can be built as a **native desktop application** for macOS, Windows, and Linux using [Tauri](https://tauri.app/), offering better performance than the web version: +Run the checks and create a production build: -- **Better performance:** Native webview for faster rendering -- **Smaller size:** compact native bundle with low runtime overhead -- **Lower memory usage:** More efficient resource utilization -- **Automatic updates:** Built-in update checker notifies you of new versions +```bash +npm run lint +npm run test:docs +npm test +npm run build +``` -The desktop app includes an automatic update checker. See [UPDATE_CHECKER.md](docs/UPDATE_CHECKER.md) for details. +
+Desktop development -### Building +Install Rust and the [Tauri platform prerequisites](https://tauri.app/start/prerequisites/), then run: ```bash -# Install Rust (one-time setup) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source "$HOME/.cargo/env" - -# Install dependencies -npm install - -# Run in development mode npm run tauri:dev - -# Build production app npm run tauri:build ``` -The production build creates: -- `Kromacut.app` in `src-tauri/target/release/bundle/macos/` -- `Kromacut_0.1.0_aarch64.dmg` installer in `src-tauri/target/release/bundle/dmg/` - -### Releases - -Pre-built native apps are automatically released on GitHub: - -1. Download the latest release from the [Releases page](https://github.com/vycdev/Kromacut/releases) -2. Choose the right file for your platform: - - **macOS Apple Silicon (M1/M2/M3)**: `*_aarch64.dmg` - - **macOS Intel**: `*_x86_64.dmg` - - **Windows**: `*-setup.exe` - - **Windows offline**: `*_offline-setup.exe` - - **Linux**: `*.AppImage` or `*.deb` -3. Install and launch +Build artifacts are written to `src-tauri/target/release/bundle/`. See [TAURI.md](docs/TAURI.md) for build, packaging, and release details. -**macOS "Kromacut is damaged" error?** The app isn't code-signed. Run `sudo xattr -d com.apple.quarantine /Applications/Kromacut.app` in Terminal to fix it. +
-**Windows SmartScreen warning?** The app isn't signed. Click "More info" → "Run anyway". - -**Windows WebView2 requirement?** Kromacut uses Tauri, which runs on Microsoft Edge WebView2 on Windows. The standard Windows installers include the small WebView2 bootstrapper, and the setup installer checks for a recent enough WebView2 runtime. Use the offline setup installer on machines without internet access. - -> **Note:** To eliminate the macOS quarantine requirement entirely, the app would need to be signed with an Apple Developer ID certificate and notarized through Apple's notarization service. This requires an [Apple Developer Program membership](https://developer.apple.com/programs/) ($99/year). See [TAURI.md](docs/TAURI.md#code-signing--notarization-future-enhancement) for details. +## Contributing -To create a new release, see [TAURI.md](docs/TAURI.md#automated-releases-github). +Bug reports, feature suggestions, and pull requests are welcome. Use [GitHub Issues](https://github.com/vycdev/Kromacut/issues), and discuss larger changes before starting work. -See [TAURI.md](docs/TAURI.md) for complete documentation on development, building, and distribution. +Read [AGENTS.md](AGENTS.md) for repository guidance and testing expectations. The [Changelog](CHANGELOG.md) tracks releases and upcoming changes. -## Star History +
+Star history [![Star History Chart](https://api.star-history.com/svg?repos=vycdev/kromacut&type=date&legend=top-left)](https://www.star-history.com/#vycdev/kromacut&type=date&legend=top-left) -## Contributing - -Contributions welcome. Open issues or PRs for bugs, improvements, or feature suggestions. If you plan a larger change (architecture, algorithms), open an issue first describing the approach so we can discuss it. +
## License -Kromacut is free software licensed under the GNU Affero General Public License v3.0 only (`AGPL-3.0-only`). See the `LICENSE` file in the repository. - -Copyright (C) 2025 vycdev. Redistribution and modified versions must preserve the copyright and license notices. Modified desktop distributions must provide the corresponding source under the AGPL, and modified hosted/network versions must also offer source access to their users. +[GNU Affero General Public License v3.0 only](LICENSE) (`AGPL-3.0-only`). Copyright (C) 2025 vycdev. diff --git a/content/tutorial-thumbnail.png b/content/tutorial-thumbnail.png new file mode 100644 index 00000000..da26b6bb Binary files /dev/null and b/content/tutorial-thumbnail.png differ diff --git a/package-lock.json b/package-lock.json index 55267e75..93c805d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kromacut", - "version": "4.1.0", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kromacut", - "version": "4.1.0", + "version": "4.2.0", "license": "AGPL-3.0-only", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 8eba27f3..597c07b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kromacut", "private": true, - "version": "4.1.0", + "version": "4.2.0", "license": "AGPL-3.0-only", "type": "module", "scripts": { @@ -16,6 +16,7 @@ "benchmark:calibrated-exact": "node --expose-gc --no-warnings --experimental-strip-types tests/benchmark/calibratedOptimizer.ts", "benchmark:calibrated-series": "node scripts/benchmark-calibrated-series.mjs", "benchmark:hotpaths": "node --no-warnings --experimental-strip-types tests/benchmark/performanceHotPaths.ts", + "benchmark:meshing": "node --no-warnings --experimental-strip-types tests/benchmark/smoothMeshing.ts", "profile:calibrated": "node scripts/profile-calibrated.mjs", "profile:compare": "node scripts/compare-calibrated-profiles.mjs", "test:e2e": "playwright test --grep @smoke", diff --git a/public/version.json b/public/version.json index 865c28a1..0f011c3d 100644 --- a/public/version.json +++ b/public/version.json @@ -1,19 +1,19 @@ { - "version": "4.1.0", - "download_url": "https://github.com/vycdev/Kromacut/releases/tag/v4.1.0", - "release_notes": "Kromacut 4.1.0 adds 12 languages across the app, guides, and public pages; improves Stack Matrix foundations and layout; preserves more fine detail during optional color cleanup; and restores desktop Hiding Distance STL/3MF downloads. Effective line width is now in 3D Print Settings. Linux AppImages support delta updates through compatible external tools. Existing profiles and calibration records remain compatible; back up important profiles and check new prints in your slicer.", + "version": "4.2.0", + "download_url": "https://github.com/vycdev/Kromacut/releases/tag/v4.2.0", + "release_notes": "Kromacut 4.2.0 adds Smooth Meshing strengths (None, Minimal, Medium, and Aggressive), which now also work with Flat Paint, and lets the desktop app open .kfil, .kapp, and .kpal files directly. Height dithering in Auto-paint works again, Flat Paint and Layer Preview are more reliable, and Settings now links to Privacy and Terms. Saved smoothing settings carry over as Medium or None; existing profiles and palettes remain compatible. Rebuild Height dithering and Flat Paint models before printing, back up important profiles, and check new prints in your slicer.", "release_notes_localized": { - "en": "Kromacut 4.1.0 adds 12 languages across the app, guides, and public pages; improves Stack Matrix foundations and layout; preserves more fine detail during optional color cleanup; and restores desktop Hiding Distance STL/3MF downloads. Effective line width is now in 3D Print Settings. Linux AppImages support delta updates through compatible external tools. Existing profiles and calibration records remain compatible; back up important profiles and check new prints in your slicer.", - "fr": "Kromacut 4.1.0 propose 12 langues dans l’application, les guides et les pages publiques ; améliore les bases et la disposition des matrices Stack Matrix ; préserve davantage de détails fins lors du nettoyage facultatif des couleurs ; et rétablit les téléchargements STL/3MF de calibration Hiding Distance dans l’application de bureau. La largeur de ligne effective se trouve désormais dans les paramètres d’impression 3D. Les AppImages Linux prennent en charge les mises à jour différentielles via des outils externes compatibles. Les profils et les données de calibration existants restent compatibles ; sauvegardez vos profils importants et vérifiez les nouvelles impressions dans votre logiciel de découpage.", - "de": "Kromacut 4.1.0 bietet 12 Sprachen in der App, den Anleitungen und den öffentlichen Seiten, verbessert die Fundamente und Anordnung von Stack Matrix, erhält bei der optionalen Farbbereinigung mehr feine Details und stellt die STL/3MF-Downloads für die Hiding-Distance-Kalibrierung in der Desktop-App wieder her. Die effektive Linienbreite befindet sich jetzt in den 3D-Druckeinstellungen. Linux-AppImages unterstützen Delta-Updates über kompatible externe Programme. Bestehende Profile und Kalibrierungsdaten bleiben kompatibel; sichern Sie wichtige Profile und prüfen Sie neue Drucke im Slicer.", - "it": "Kromacut 4.1.0 offre 12 lingue nell’app, nelle guide e nelle pagine pubbliche; migliora le basi e la disposizione di Stack Matrix; conserva più dettagli fini durante la pulizia facoltativa dei colori; e ripristina i download STL/3MF per la calibrazione Hiding Distance nell’app desktop. La larghezza effettiva della linea si trova ora nelle impostazioni di stampa 3D. Le AppImage Linux supportano gli aggiornamenti differenziali tramite strumenti esterni compatibili. I profili e i dati di calibrazione esistenti restano compatibili; esegui una copia di sicurezza dei profili importanti e verifica le nuove stampe nello slicer.", - "ro": "Kromacut 4.1.0 oferă 12 limbi în aplicație, ghiduri și paginile publice; îmbunătățește fundația și aranjarea Stack Matrix; păstrează mai multe detalii fine la curățarea opțională a culorilor; și restabilește descărcările STL/3MF pentru calibrarea Hiding Distance în aplicația desktop. Lățimea efectivă a liniei se află acum în setările de imprimare 3D. AppImage-urile Linux acceptă actualizări diferențiale prin instrumente externe compatibile. Profilurile și datele de calibrare existente rămân compatibile; fă copii de siguranță ale profilurilor importante și verifică imprimările noi în slicer.", - "es": "Kromacut 4.1.0 incorpora 12 idiomas en la aplicación, las guías y las páginas públicas; mejora las bases y la distribución de Stack Matrix; conserva más detalles finos durante la limpieza opcional de colores; y restablece las descargas STL/3MF de calibración Hiding Distance en la aplicación de escritorio. El ancho de línea efectivo está ahora en los ajustes de impresión 3D. Las AppImage de Linux admiten actualizaciones diferenciales mediante herramientas externas compatibles. Los perfiles y los datos de calibración existentes siguen siendo compatibles; haz copias de seguridad de los perfiles importantes y revisa las nuevas impresiones en tu laminador.", - "ja": "Kromacut 4.1.0は、アプリ、ガイド、公開ページ全体で12言語に対応し、Stack Matrixの土台と配置を改善しました。オプションの色クリーンアップで細かなディテールをより多く保持し、デスクトップアプリでHiding DistanceキャリブレーションのSTL/3MFダウンロードを復旧しました。実効ライン幅は3D印刷設定に移動しました。Linux版AppImageは、対応する外部ツールによる差分更新をサポートします。既存のプロファイルとキャリブレーション記録の互換性は維持されます。重要なプロファイルをバックアップし、新しい印刷データをスライサーで確認してください。", - "zh-CN": "Kromacut 4.1.0 为应用、指南和公开页面提供了 12 种语言;改进了 Stack Matrix 的底座和布局;在可选的颜色清理过程中保留更多精细细节;并恢复了桌面应用中 Hiding Distance 校准的 STL/3MF 下载。有效线宽现已移至 3D 打印设置。Linux AppImage 支持通过兼容的外部工具进行增量更新。现有配置文件和校准记录仍然兼容;请备份重要配置文件,并在切片软件中检查新的打印模型。", - "hi": "Kromacut 4.1.0 में ऐप, गाइड और सार्वजनिक पृष्ठों के लिए 12 भाषाएँ जोड़ी गई हैं; Stack Matrix की आधार परत और लेआउट में सुधार किया गया है; वैकल्पिक रंग सफ़ाई के दौरान अधिक बारीक विवरण सुरक्षित रहते हैं; और डेस्कटॉप ऐप में Hiding Distance कैलिब्रेशन के STL/3MF डाउनलोड फिर से काम करते हैं। प्रभावी लाइन चौड़ाई अब 3D प्रिंट सेटिंग्स में है। Linux AppImage संगत बाहरी टूल के माध्यम से डेल्टा अपडेट का समर्थन करती हैं। मौजूदा प्रोफ़ाइल और कैलिब्रेशन रिकॉर्ड संगत बने रहते हैं; महत्वपूर्ण प्रोफ़ाइल का बैकअप लें और नए प्रिंट को अपने स्लाइसर में जाँचें।", - "pt-PT": "O Kromacut 4.1.0 disponibiliza 12 idiomas na aplicação, nos guias e nas páginas públicas; melhora as bases e a disposição da Stack Matrix; preserva mais detalhes finos durante a limpeza opcional de cores; e repõe as transferências STL/3MF da calibração Hiding Distance na aplicação de computador. A largura efetiva da linha encontra-se agora nas definições de impressão 3D. As AppImages Linux suportam atualizações diferenciais através de ferramentas externas compatíveis. Os perfis e os registos de calibração existentes continuam compatíveis; faça cópias de segurança dos perfis importantes e verifique as novas impressões no seu programa de corte.", - "uk": "Kromacut 4.1.0 додає 12 мов для застосунку, посібників і публічних сторінок; покращує основу та розташування комірок Stack Matrix; зберігає більше дрібних деталей під час необов’язкового очищення кольорів; і відновлює завантаження STL/3MF для калібрування Hiding Distance у настільному застосунку. Ефективна ширина лінії тепер міститься в налаштуваннях 3D-друку. Linux AppImage підтримують дельта-оновлення через сумісні зовнішні інструменти. Наявні профілі та записи калібрування залишаються сумісними; створіть резервні копії важливих профілів і перевіряйте нові моделі для друку у слайсері.", - "bn": "Kromacut 4.1.0-তে অ্যাপ, গাইড এবং সর্বজনীন পৃষ্ঠাগুলোতে ১২টি ভাষা যোগ করা হয়েছে; Stack Matrix-এর ভিত্তি ও বিন্যাস উন্নত করা হয়েছে; ঐচ্ছিক রঙ পরিষ্কারের সময় আরও সূক্ষ্ম বিবরণ অক্ষত থাকে; এবং ডেস্কটপ অ্যাপে Hiding Distance ক্যালিব্রেশনের STL/3MF ডাউনলোড আবার কাজ করে। কার্যকর লাইন প্রস্থ এখন 3D প্রিন্ট সেটিংসে রয়েছে। Linux AppImage সামঞ্জস্যপূর্ণ বাহ্যিক সরঞ্জামের মাধ্যমে ডেল্টা আপডেট সমর্থন করে। বিদ্যমান প্রোফাইল ও ক্যালিব্রেশন রেকর্ড সামঞ্জস্যপূর্ণ থাকে; গুরুত্বপূর্ণ প্রোফাইলের ব্যাকআপ নিন এবং নতুন প্রিন্টগুলো আপনার স্লাইসারে পরীক্ষা করুন।" + "en": "Kromacut 4.2.0 adds Smooth Meshing strengths (None, Minimal, Medium, and Aggressive), which now also work with Flat Paint, and lets the desktop app open .kfil, .kapp, and .kpal files directly. Height dithering in Auto-paint works again, Flat Paint and Layer Preview are more reliable, and Settings now links to Privacy and Terms. Saved smoothing settings carry over as Medium or None; existing profiles and palettes remain compatible. Rebuild Height dithering and Flat Paint models before printing, back up important profiles, and check new prints in your slicer.", + "fr": "Kromacut 4.2.0 ajoute des niveaux de Maillage lissé (Aucun, Minimal, Moyen et Intensif), désormais compatibles avec la Peinture à plat, et permet à l’application de bureau d’ouvrir directement les fichiers .kfil, .kapp et .kpal. Le Tramage en hauteur de la Peinture automatique fonctionne de nouveau, la Peinture à plat et l’Aperçu des couches sont plus fiables, et les Paramètres renvoient désormais vers les pages Confidentialité et Conditions d’utilisation. Les réglages de lissage enregistrés deviennent Moyen ou Aucun ; les profils et palettes existants restent compatibles. Reconstruisez les modèles utilisant le Tramage en hauteur ou la Peinture à plat avant d’imprimer, sauvegardez vos profils importants et vérifiez les nouvelles impressions dans votre logiciel de découpage.", + "de": "Kromacut 4.2.0 bietet Stärkestufen für Geglättetes Netz (Keine, Minimal, Mittel und Stark), die jetzt auch mit Flat Paint funktionieren, und die Desktop-App öffnet .kfil-, .kapp- und .kpal-Dateien direkt. Höhen-Dithering in Auto-paint funktioniert wieder, Flat Paint und die Schichtvorschau arbeiten zuverlässiger, und die Einstellungen verlinken jetzt auf Datenschutz und Nutzungsbedingungen. Gespeicherte Glättungseinstellungen werden als Mittel oder Keine übernommen; bestehende Profile und Paletten bleiben kompatibel. Erstellen Sie Modelle mit Höhen-Dithering oder Flat Paint vor dem Drucken neu, sichern Sie wichtige Profile und prüfen Sie neue Drucke im Slicer.", + "it": "Kromacut 4.2.0 aggiunge i livelli di Mesh levigata (Nessuna, Minima, Media e Intensa), ora compatibili anche con Flat Paint, e consente all’app desktop di aprire direttamente i file .kfil, .kapp e .kpal. Il Dithering dell’altezza in Auto-paint funziona di nuovo, Flat Paint e l’Anteprima degli strati sono più affidabili e le Impostazioni ora rimandano a Privacy e Termini e condizioni. Le impostazioni di levigatura salvate diventano Media o Nessuna; i profili e le palette esistenti restano compatibili. Ricostruisci i modelli con Dithering dell’altezza o Flat Paint prima di stampare, esegui una copia di sicurezza dei profili importanti e verifica le nuove stampe nello slicer.", + "ro": "Kromacut 4.2.0 adaugă niveluri pentru Geometrie netezită (Fără, Minimă, Medie și Intensă), care funcționează acum și cu Flat Paint, iar aplicația desktop poate deschide direct fișierele .kfil, .kapp și .kpal. Opțiunea Dithering al înălțimii din Auto-paint funcționează din nou, Flat Paint și Previzualizarea straturilor sunt mai fiabile, iar Setările includ acum linkuri către Confidențialitate și Termeni și condiții. Setările de netezire salvate devin Medie sau Fără; profilurile și paletele existente rămân compatibile. Reconstruiește modelele cu Dithering al înălțimii sau Flat Paint înainte de imprimare, fă copii de siguranță ale profilurilor importante și verifică imprimările noi în slicer.", + "es": "Kromacut 4.2.0 añade niveles de Malla suavizada (Ninguno, Mínimo, Medio e Intenso), que ahora también funcionan con Pintura plana, y permite que la aplicación de escritorio abra directamente archivos .kfil, .kapp y .kpal. El Tramado de altura de la Pintura automática vuelve a funcionar, la Pintura plana y la Vista previa de capas son más fiables, y los Ajustes ahora enlazan a Privacidad y Términos y condiciones. Los ajustes de suavizado guardados pasan a Medio o Ninguno; los perfiles y paletas existentes siguen siendo compatibles. Reconstruye los modelos con Tramado de altura o Pintura plana antes de imprimir, haz copias de seguridad de los perfiles importantes y revisa las nuevas impresiones en tu laminador.", + "ja": "Kromacut 4.2.0では、滑らかなメッシュの強さ(なし、最小、中、強)を選べるようになり、フラット印刷でも使用できます。デスクトップアプリで.kfil、.kapp、.kpalファイルを直接開けるようになりました。自動ペイントの高さディザリングが再び機能し、フラット印刷とレイヤープレビューの信頼性が向上し、設定からプライバシーと利用規約を開けるようになりました。保存済みのスムージング設定は「中」または「なし」として引き継がれ、既存のプロファイルとパレットの互換性は維持されます。高さディザリングまたはフラット印刷を使うモデルは印刷前に再ビルドし、重要なプロファイルをバックアップして、新しい印刷データをスライサーで確認してください。", + "zh-CN": "Kromacut 4.2.0 新增平滑网格强度(无、轻度、中度、强度),现在也可用于平面打印模式;桌面应用现可直接打开 .kfil、.kapp 和 .kpal 文件。自动配色中的高度抖动已恢复正常,平面打印模式和层预览更加可靠,设置中现已提供隐私与使用条款链接。已保存的平滑设置将转换为“中度”或“无”;现有配置文件和调色板仍然兼容。使用高度抖动或平面打印模式的模型请在打印前重新生成,请备份重要配置文件,并在切片软件中检查新的打印模型。", + "hi": "Kromacut 4.2.0 में चिकनी मेशिंग के स्तर (कोई नहीं, न्यूनतम, मध्यम और तीव्र) जोड़े गए हैं, जो अब समतल प्रिंट मोड के साथ भी काम करते हैं, और डेस्कटॉप ऐप अब .kfil, .kapp और .kpal फ़ाइलें सीधे खोल सकता है। स्वचालित रंगाई में ऊँचाई डिथरिंग फिर से काम करती है, समतल प्रिंट मोड और परत पूर्वावलोकन अधिक भरोसेमंद हैं, और सेटिंग्स में अब गोपनीयता और नियम व शर्तों के लिंक हैं। सहेजी गई स्मूदिंग सेटिंग्स मध्यम या कोई नहीं के रूप में बनी रहती हैं; मौजूदा प्रोफ़ाइल और पैलेट संगत बने रहते हैं। ऊँचाई डिथरिंग या समतल प्रिंट मोड वाले मॉडल प्रिंट से पहले फिर से बनाएँ, महत्वपूर्ण प्रोफ़ाइल का बैकअप लें और नए प्रिंट को अपने स्लाइसर में जाँचें।", + "pt-PT": "O Kromacut 4.2.0 adiciona níveis de Malha suavizada (Nenhuma, Mínima, Média e Intensa), que agora também funcionam com a Pintura plana, e permite que a aplicação de computador abra diretamente ficheiros .kfil, .kapp e .kpal. O Pontilhamento em altura da Pintura automática volta a funcionar, a Pintura plana e a Pré-visualização de camadas estão mais fiáveis, e as Definições incluem agora ligações para Privacidade e Termos e condições. As definições de suavização guardadas passam a Média ou Nenhuma; os perfis e as paletas existentes continuam compatíveis. Reconstrua os modelos com Pontilhamento em altura ou Pintura plana antes de imprimir, faça cópias de segurança dos perfis importantes e verifique as novas impressões no seu programa de corte.", + "uk": "Kromacut 4.2.0 додає рівні параметра «Згладжена сітка» (Без згладжування, Мінімальне, Середнє та Інтенсивне), які тепер працюють і з Flat Paint, а настільний застосунок може безпосередньо відкривати файли .kfil, .kapp і .kpal. Дизеринг висоти в Auto-paint знову працює, Flat Paint і Перегляд шарів стали надійнішими, а в Налаштуваннях з’явилися посилання на Конфіденційність і Умови користування. Збережені налаштування згладжування переходять у Середнє або Без згладжування; наявні профілі та палітри залишаються сумісними. Перед друком перебудуйте моделі з дизерингом висоти або Flat Paint, створіть резервні копії важливих профілів і перевіряйте нові моделі для друку у слайсері.", + "bn": "Kromacut 4.2.0-তে মসৃণ মেশের স্তর (কোনোটিই নয়, ন্যূনতম, মাঝারি ও তীব্র) যোগ করা হয়েছে, যা এখন সমতল প্রিন্ট মোডেও কাজ করে, এবং ডেস্কটপ অ্যাপ এখন সরাসরি .kfil, .kapp ও .kpal ফাইল খুলতে পারে। স্বয়ংক্রিয় রং-এর উচ্চতার ডিথারিং আবার কাজ করে, সমতল প্রিন্ট মোড ও স্তরের প্রিভিউ আরও নির্ভরযোগ্য হয়েছে, এবং সেটিংসে এখন গোপনীয়তা ও নিয়ম ও শর্তাবলির লিংক রয়েছে। সংরক্ষিত মসৃণকরণ সেটিং মাঝারি বা কোনোটিই নয় হিসেবে বহাল থাকে; বিদ্যমান প্রোফাইল ও প্যালেট সামঞ্জস্যপূর্ণ থাকে। উচ্চতার ডিথারিং বা সমতল প্রিন্ট মোড ব্যবহার করা মডেল প্রিন্টের আগে আবার তৈরি করুন, গুরুত্বপূর্ণ প্রোফাইলের ব্যাকআপ নিন এবং নতুন প্রিন্টগুলো আপনার স্লাইসারে পরীক্ষা করুন।" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f836110d..a6214923 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -81,6 +81,137 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "atk" version = "0.18.2" @@ -188,6 +319,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.6.0" @@ -431,6 +575,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -858,6 +1011,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -892,7 +1072,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -1062,6 +1262,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1469,6 +1682,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + [[package]] name = "hex" version = "0.4.3" @@ -1907,7 +2126,7 @@ dependencies = [ [[package]] name = "kromacut" -version = "4.1.0" +version = "4.2.0" dependencies = [ "log", "reqwest 0.12.28", @@ -1918,6 +2137,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-log", + "tauri-plugin-single-instance", "tokio", ] @@ -2489,6 +2709,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "pango" version = "0.18.3" @@ -2514,6 +2744,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2728,6 +2964,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2773,6 +3020,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -3278,7 +3539,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3877,6 +4138,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4188,6 +4460,22 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db817fe9295e19b7d8357e900af31edb93703dd9fb6de524b007b47b6afc63b0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tokio", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -4300,7 +4588,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4655,9 +4943,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -4707,6 +5007,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -5150,7 +5461,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5591,6 +5902,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.55.0" @@ -5792,6 +6112,76 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerocopy" version = "0.8.39" @@ -5877,3 +6267,44 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a1a80ec6..60532f84 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kromacut" -version = "4.1.0" +version = "4.2.0" description = "Kromacut - Multi-color lithophane 3D print generator" authors = ["vycdev"] license = "AGPL-3.0-only" @@ -27,3 +27,4 @@ reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } tauri-plugin-dialog = "2" tauri-plugin-fs = "2" +tauri-plugin-single-instance = "2" diff --git a/src-tauri/linux/kromacut-mime.xml b/src-tauri/linux/kromacut-mime.xml new file mode 100644 index 00000000..f821957c --- /dev/null +++ b/src-tauri/linux/kromacut-mime.xml @@ -0,0 +1,14 @@ + + + + Kromacut filament profile + + + + + + Kromacut color palette + + + + diff --git a/src-tauri/linux/kromacut.desktop b/src-tauri/linux/kromacut.desktop new file mode 100644 index 00000000..a139a83f --- /dev/null +++ b/src-tauri/linux/kromacut.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Categories={{categories}} +{{#if comment}} +Comment={{comment}} +{{/if}} +Exec={{exec}} %F +StartupWMClass={{exec}} +Icon={{icon}} +Name={{name}} +Terminal=false +Type=Application +{{#if mime_type}} +MimeType={{mime_type}}; +{{/if}} diff --git a/src-tauri/linux/refresh-file-associations.sh b/src-tauri/linux/refresh-file-associations.sh new file mode 100644 index 00000000..0e38a0d7 --- /dev/null +++ b/src-tauri/linux/refresh-file-associations.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Package install/removal: refresh MIME globs and application handler metadata. +if command -v update-mime-database >/dev/null 2>&1; then + update-mime-database /usr/share/mime || true +fi +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database /usr/share/applications || true +fi +exit 0 diff --git a/src-tauri/macos/Info.plist b/src-tauri/macos/Info.plist new file mode 100644 index 00000000..77338842 --- /dev/null +++ b/src-tauri/macos/Info.plist @@ -0,0 +1,25 @@ + + + + + CFBundleDocumentTypes + + + CFBundleTypeNameKromacut filament profile + CFBundleTypeExtensionskfilkapp + CFBundleTypeRoleEditor + CFBundleTypeIconFilekromacut-document.icns + LSHandlerRankOwner + LSItemContentTypescom.kromacut.filament-profile + + + CFBundleTypeNameKromacut color palette + CFBundleTypeExtensionskpal + CFBundleTypeRoleEditor + CFBundleTypeIconFilekromacut-document.icns + LSHandlerRankOwner + LSItemContentTypescom.kromacut.palette + + + + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da5a578d..47202e77 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ use std::process::Command; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Manager, State}; +mod opened_files; +use opened_files::{enqueue_and_notify, take_opened_file, OpenedFiles}; const RELEASES_URL: &str = "https://github.com/vycdev/Kromacut/releases"; @@ -211,6 +213,27 @@ fn open_releases_page() -> Result<(), String> { open_external_url(RELEASES_URL) } +fn legal_page_url(path: &str) -> Result { + let segments: Vec<_> = path.strip_prefix('/').unwrap_or("").split('/').collect(); + let supported = match segments.as_slice() { + ["privacy" | "terms"] => true, + [language, "privacy" | "terms"] => matches!( + *language, + "fr" | "de" | "it" | "ro" | "es" | "ja" | "zh-CN" | "hi" | "pt-PT" | "uk" | "bn" + ), + _ => false, + }; + if !supported { + return Err("Unsupported legal page".to_string()); + } + Ok(format!("https://kromacut.com{path}")) +} + +#[tauri::command] +fn open_legal_page(path: String) -> Result<(), String> { + open_external_url(&legal_page_url(&path)?) +} + fn open_external_url(url: &str) -> Result<(), String> { #[cfg(target_os = "windows")] let result = Command::new("cmd").args(["/C", "start", "", url]).spawn(); @@ -223,25 +246,40 @@ fn open_external_url(url: &str) -> Result<(), String> { result .map(|_| ()) - .map_err(|e| format!("Failed to open releases page: {}", e)) + .map_err(|e| format!("Failed to open web page: {}", e)) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .manage(DiagnosticWriterState::default()) + .manage(OpenedFiles::default()) + .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { + enqueue_and_notify( + app, + args.into_iter().skip(1).map(PathBuf::from), + Path::new(&cwd), + ); + })) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ check_for_updates, get_app_version, open_releases_page, + open_legal_page, + take_opened_file, begin_auto_paint_diagnostic, append_auto_paint_diagnostic, finish_auto_paint_diagnostic, open_auto_paint_diagnostics_directory ]) .setup(|app| { + #[cfg(any(target_os = "windows", target_os = "linux"))] + app.state::().enqueue( + std::env::args_os().skip(1).map(PathBuf::from), + &std::env::current_dir()?, + ); if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() @@ -251,8 +289,17 @@ pub fn run() { } Ok(()) }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|_app, _event| { + #[cfg(target_os = "macos")] + if let tauri::RunEvent::Opened { urls } = _event { + let paths = urls.into_iter().filter_map(|url| url.to_file_path().ok()); + if let Ok(cwd) = std::env::current_dir() { + enqueue_and_notify(_app, paths, &cwd); + } + } + }); } #[cfg(test)] @@ -263,6 +310,38 @@ mod tests { use std::fs::{create_dir_all, read_to_string, remove_dir_all, File}; use std::io::Cursor; + #[test] + fn legal_page_urls_only_allow_published_notices() { + for language in [ + "", "fr", "de", "it", "ro", "es", "ja", "zh-CN", "hi", "pt-PT", "uk", "bn", + ] { + for page in ["privacy", "terms"] { + let path = if language.is_empty() { + format!("/{page}") + } else { + format!("/{language}/{page}") + }; + assert_eq!( + super::legal_page_url(&path).unwrap(), + format!("https://kromacut.com{path}") + ); + } + } + for path in [ + "https://example.com/privacy", + "//example.com/privacy", + "/app", + "/xx/privacy", + "/ro/../privacy", + "/privacy?next=example.com", + "/privacy&calc", + "privacy", + "/terms/", + ] { + assert!(super::legal_page_url(path).is_err(), "{path}"); + } + } + fn diagnostic_test_directory(name: &str) -> std::path::PathBuf { std::env::temp_dir().join(format!( "kromacut-{name}-{}-{}", @@ -278,6 +357,22 @@ mod tests { assert!(is_different_version("3.0.0", "2.9.9")); } + #[test] + fn platform_file_association_configs_are_valid_tauri_configs() { + let base: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + for (platform, field) in [ + (include_str!("../tauri.linux.conf.json"), "linux"), + (include_str!("../tauri.macos.conf.json"), "macOS"), + ] { + let mut merged = base.clone(); + let overlay: serde_json::Value = serde_json::from_str(platform).unwrap(); + merged["bundle"][field] = overlay["bundle"][field].clone(); + let config: tauri::Config = serde_json::from_value(merged).unwrap(); + assert_eq!(config.bundle.file_associations.unwrap().len(), 2); + } + } + #[test] fn ignores_exact_version_matches() { assert!(!is_different_version("2.6.0", "2.6.0")); diff --git a/src-tauri/src/opened_files.rs b/src-tauri/src/opened_files.rs new file mode 100644 index 00000000..f3628042 --- /dev/null +++ b/src-tauri/src/opened_files.rs @@ -0,0 +1,197 @@ +use serde::Serialize; +use std::collections::VecDeque; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use tauri::{AppHandle, Emitter, Manager, State}; + +const MAX_FILE_BYTES: u64 = 32 * 1024 * 1024; +const MAX_PENDING_FILES: usize = 64; + +#[derive(Clone, Copy, Debug, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum FileKind { + Profile, + Palette, +} + +#[derive(Debug, Serialize)] +pub struct OpenedFile { + name: String, + kind: FileKind, + content: Option, + error: Option, +} + +#[derive(Default)] +pub struct OpenedFiles(pub Mutex>); + +fn file_kind(path: &Path) -> Option { + match path.extension()?.to_str()?.to_ascii_lowercase().as_str() { + "kfil" | "kapp" => Some(FileKind::Profile), + "kpal" => Some(FileKind::Palette), + _ => None, + } +} + +impl OpenedFiles { + pub fn enqueue(&self, paths: impl IntoIterator, cwd: &Path) { + let mut pending = self.0.lock().unwrap_or_else(|error| error.into_inner()); + for path in paths { + if file_kind(&path).is_none() { + continue; + } + let absolute = if path.is_absolute() { + path + } else { + cwd.join(path) + }; + if pending.len() < MAX_PENDING_FILES && !pending.contains(&absolute) { + pending.push_back(absolute); + } + } + } +} + +pub fn enqueue_and_notify(app: &AppHandle, paths: impl IntoIterator, cwd: &Path) { + app.state::().enqueue(paths, cwd); + // The event is only a wake-up. The queue remains available until the UI takes it. + let _ = app.emit("kromacut-opened-files", ()); + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } +} + +fn read_opened_file(path: PathBuf) -> OpenedFile { + let name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + let kind = file_kind(&path).expect("only supported files enter the queue"); + let result = (|| -> Result { + let metadata = std::fs::metadata(&path).map_err(|error| error.to_string())?; + if !metadata.is_file() { + return Err("Not a regular file".into()); + } + if metadata.len() > MAX_FILE_BYTES { + return Err("File exceeds the 32 MiB import limit".into()); + } + let file = File::open(&path).map_err(|error| error.to_string())?; + if !file + .metadata() + .map_err(|error| error.to_string())? + .is_file() + { + return Err("Not a regular file".into()); + } + let mut content = String::new(); + file.take(MAX_FILE_BYTES + 1) + .read_to_string(&mut content) + .map_err(|error| error.to_string())?; + if content.len() as u64 > MAX_FILE_BYTES { + return Err("File exceeds the 32 MiB import limit".into()); + } + Ok(content) + })(); + match result { + Ok(content) => OpenedFile { + name, + kind, + content: Some(content), + error: None, + }, + Err(error) => OpenedFile { + name, + kind, + content: None, + error: Some(error), + }, + } +} + +#[tauri::command] +pub async fn take_opened_file(state: State<'_, OpenedFiles>) -> Result, String> { + let path = state + .0 + .lock() + .unwrap_or_else(|error| error.into_inner()) + .pop_front(); + match path { + Some(path) => tauri::async_runtime::spawn_blocking(move || read_opened_file(path)) + .await + .map(Some) + .map_err(|error| error.to_string()), + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn queue_accepts_only_owned_extensions_and_resolves_relative_paths() { + let queue = OpenedFiles::default(); + let cwd = std::env::current_dir().unwrap(); + queue.enqueue( + [ + "My spools.KFIL", + "old.kapp", + "colors.kpal", + "picture.png", + "model.3mf", + "raw.json", + ] + .map(PathBuf::from), + &cwd, + ); + let pending = queue.0.lock().unwrap(); + assert_eq!(pending.len(), 3); + assert_eq!(pending[0], cwd.join("My spools.KFIL")); + assert_eq!(file_kind(&pending[1]), Some(FileKind::Profile)); + assert_eq!(file_kind(&pending[2]), Some(FileKind::Palette)); + } + + #[test] + fn duplicate_wakeups_do_not_duplicate_pending_files_but_reopening_is_allowed() { + let queue = OpenedFiles::default(); + let cwd = std::env::current_dir().unwrap(); + queue.enqueue([PathBuf::from("one.kfil"), cwd.join("one.kfil")], &cwd); + assert_eq!(queue.0.lock().unwrap().len(), 1); + queue.0.lock().unwrap().pop_front(); + queue.enqueue([PathBuf::from("one.kfil")], &cwd); + assert_eq!(queue.0.lock().unwrap().len(), 1); + } + + #[test] + fn reader_reports_missing_directory_and_oversized_files_without_importing() { + let directory = std::env::temp_dir().join(format!( + "kromacut-open-files-{}-{}", + std::process::id(), + super::super::unix_timestamp_millis().unwrap() + )); + std::fs::create_dir_all(&directory).unwrap(); + assert!(read_opened_file(directory.join("missing.kfil")) + .error + .is_some()); + let folder = directory.join("folder.kpal"); + std::fs::create_dir(&folder).unwrap(); + assert!(read_opened_file(folder).content.is_none()); + let huge = directory.join("huge.kfil"); + File::create(&huge) + .unwrap() + .set_len(MAX_FILE_BYTES + 1) + .unwrap(); + assert!(read_opened_file(huge).error.unwrap().contains("32 MiB")); + let valid = directory.join("valid.kapp"); + std::fs::write(&valid, "\u{feff}{\"name\":\"Unicode 日本語\"}").unwrap(); + let opened = read_opened_file(valid); + assert_eq!(opened.kind, FileKind::Profile); + assert!(opened.content.unwrap().contains("日本語")); + std::fs::remove_dir_all(directory).unwrap(); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7bfc3702..c045ff79 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Kromacut", - "version": "4.1.0", + "version": "4.2.0", "identifier": "com.kromacut.lithophane", "build": { "frontendDist": "../dist", @@ -29,6 +29,32 @@ "bundle": { "active": true, "targets": "all", + "fileAssociations": [ + { + "ext": ["kfil", "kapp"], + "name": "Kromacut.FilamentProfile", + "description": "Kromacut filament profile", + "mimeType": "application/x-kromacut-filament-profile", + "role": "Editor", + "rank": "Owner", + "exportedType": { + "identifier": "com.kromacut.filament-profile", + "conformsTo": ["public.json"] + } + }, + { + "ext": ["kpal"], + "name": "Kromacut.Palette", + "description": "Kromacut color palette", + "mimeType": "application/x-kromacut-palette", + "role": "Editor", + "rank": "Owner", + "exportedType": { + "identifier": "com.kromacut.palette", + "conformsTo": ["public.json"] + } + } + ], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -42,7 +68,8 @@ "silent": true }, "nsis": { - "minimumWebview2Version": "114.0.1823.67" + "minimumWebview2Version": "114.0.1823.67", + "installerHooks": "windows/file-associations.nsh" } }, "macOS": { diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json new file mode 100644 index 00000000..a5b6a532 --- /dev/null +++ b/src-tauri/tauri.linux.conf.json @@ -0,0 +1,40 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "bundle": { + "linux": { + "deb": { + "postInstallScript": "linux/refresh-file-associations.sh", + "postRemoveScript": "linux/refresh-file-associations.sh", + "desktopTemplate": "linux/kromacut.desktop", + "files": { + "/usr/share/mime/packages/kromacut.xml": "linux/kromacut-mime.xml", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-filament-profile.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-filament-profile.png": "icons/128x128.png", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-palette.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-palette.png": "icons/128x128.png" + } + }, + "rpm": { + "postInstallScript": "linux/refresh-file-associations.sh", + "postRemoveScript": "linux/refresh-file-associations.sh", + "desktopTemplate": "linux/kromacut.desktop", + "files": { + "/usr/share/mime/packages/kromacut.xml": "linux/kromacut-mime.xml", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-filament-profile.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-filament-profile.png": "icons/128x128.png", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-palette.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-palette.png": "icons/128x128.png" + } + }, + "appimage": { + "files": { + "/usr/share/mime/packages/kromacut.xml": "linux/kromacut-mime.xml", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-filament-profile.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-filament-profile.png": "icons/128x128.png", + "/usr/share/icons/hicolor/32x32/mimetypes/application-x-kromacut-palette.png": "icons/32x32.png", + "/usr/share/icons/hicolor/128x128/mimetypes/application-x-kromacut-palette.png": "icons/128x128.png" + } + } + } + } +} diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 00000000..c9b9e5b1 --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,11 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "bundle": { + "macOS": { + "files": { + "Resources/kromacut-document.icns": "icons/icon.icns" + }, + "infoPlist": "macos/Info.plist" + } + } +} diff --git a/src-tauri/windows/file-associations.nsh b/src-tauri/windows/file-associations.nsh new file mode 100644 index 00000000..fdd3a9f8 --- /dev/null +++ b/src-tauri/windows/file-associations.nsh @@ -0,0 +1,37 @@ +; Tauri includes FileAssociation.nsh before this hook file. Retain its +; registration calls while preserving the original choice across reinstalls, +; quoting executable/document paths, and respecting later changes on removal. +!include LogicLib.nsh +!ifndef KROMACUT_ASSOCIATIONS_ROOT + !define KROMACUT_ASSOCIATIONS_ROOT "Software\Classes" +!endif + +!macroundef APP_ASSOCIATE +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ReadRegStr $R0 SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "" + ${If} $R0 != "${FILECLASS}" + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "${FILECLASS}_backup" "$R0" + ${EndIf} + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "" "${FILECLASS}" + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}\shell" "" "open" + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}\shell\open\command" "" '$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"' +!macroend + +!macroundef APP_UNASSOCIATE +!macro APP_UNASSOCIATE EXT FILECLASS + ReadRegStr $R0 SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "" + ${If} $R0 == "${FILECLASS}" + ReadRegStr $R0 SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "${FILECLASS}_backup" + ${If} $R0 == "" + DeleteRegValue SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "" + ${Else} + WriteRegStr SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "" "$R0" + ${EndIf} + ${EndIf} + DeleteRegValue SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" "${FILECLASS}_backup" + DeleteRegKey /ifempty SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\.${EXT}" + DeleteRegKey SHCTX "${KROMACUT_ASSOCIATIONS_ROOT}\${FILECLASS}" +!macroend diff --git a/src/App.tsx b/src/App.tsx index 2d5b5baa..b0e36e18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,8 @@ import { normalizeSeparationMaxDeltaE } from './lib/autoPaint'; import ResizableSplitter from './components/ResizableSplitter'; import { ControlsPanel } from './components/ControlsPanel'; import { usePaletteManager } from './hooks/usePaletteManager'; +import { useDesktopFileOpen } from './hooks/useDesktopFileOpen'; +import { DesktopFileDialog } from './components/DesktopFileDialog'; import { UpdateChecker } from './components/UpdateChecker'; import ProgressOverlay from './components/ProgressOverlay'; import DocsPage from './components/docs/DocsPage'; @@ -263,7 +265,27 @@ function App(): React.ReactElement | null { handleDeletePalette, handleExportPalette, handleImportFile: handleImportPaletteFile, + importText: importPaletteText, } = usePaletteManager(); + const [paletteEditorOpen, setPaletteEditorOpen] = useState(false); + const [calibrationDialogOpen, setCalibrationDialogOpen] = useState(false); + const [profileSaveNewOpen, setProfileSaveNewOpen] = useState(false); + const [profileRenameOpen, setProfileRenameOpen] = useState(false); + const [filamentEditing, setFilamentEditing] = useState(false); + const { file: queuedDesktopFile, finish: finishDesktopFile } = useDesktopFileOpen(); + // OS file opens can arrive through an active editor. Keep the request queued + // until its draft is saved or dismissed, before changing either workspace. + const desktopFile = + paletteEditorOpen || + calibrationDialogOpen || + profileSaveNewOpen || + profileRenameOpen || + filamentEditing + ? null + : queuedDesktopFile; + const handledPaletteFile = useRef(null); + const [failedPaletteFileId, setFailedPaletteFileId] = useState(null); + const [completedPaletteFileId, setCompletedPaletteFileId] = useState(null); // Keep the postprocess color count in sync with the selected palette's // enabled size. Edits, per-color toggles, clones, and imports all change @@ -390,6 +412,28 @@ function App(): React.ReactElement | null { }; }, []); const [docsOpen, setDocsOpen] = useState(() => isDocsRoute(window.location.pathname)); + useEffect(() => { + if (!desktopFile || desktopFile.content === null) return; + setDocsOpen(false); + if (isDocsRoute(window.location.pathname)) window.history.pushState(null, '', toolPath); + if (desktopFile.kind === 'profile') { + handleModeChange('3d'); + } else if (handledPaletteFile.current !== desktopFile.id) { + handledPaletteFile.current = desktopFile.id; + handleModeChange('2d'); + try { + if (importPaletteText(desktopFile.content, true)) + setCompletedPaletteFileId(desktopFile.id); + else setFailedPaletteFileId(desktopFile.id); + } catch (error) { + console.error('Could not import the opened palette', error); + setFailedPaletteFileId(desktopFile.id); + } + } + }, [desktopFile, handleModeChange, importPaletteText, toolPath]); + useEffect(() => { + if (completedPaletteFileId !== null) finishDesktopFile(completedPaletteFileId); + }, [completedPaletteFileId, finishDesktopFile]); const [isOrtho, setIsOrtho] = useState(loadCameraMode); const [previewRenderMode, setPreviewRenderMode] = useState(loadPreviewRenderMode); @@ -630,6 +674,7 @@ function App(): React.ReactElement | null { exportObjectTo3MFBlob(obj, { layerHeight: builtModelState.layerHeight, firstLayerHeight: builtModelState.slicerFirstLayerHeight, + smoothMeshingStrength: builtModelState.smoothMeshingStrength, layerFilamentColors: builtModelAutoPaint ? builtModelState.autoPaintFilamentSwatches?.map((s) => s.hex) : undefined, @@ -798,6 +843,8 @@ function App(): React.ReactElement | null { onDeletePalette={handleDeletePalette} onExportPalette={handleExportPalette} onImportPaletteFile={handleImportPaletteFile} + paletteEditorOpen={paletteEditorOpen} + onPaletteEditorOpenChange={setPaletteEditorOpen} /> )} @@ -941,7 +1002,9 @@ function App(): React.ReactElement | null { printableFeaturePixels={ builtModelState.printableFeaturePixels } - smoothMeshing={builtModelState.smoothMeshing} + smoothMeshingStrength={ + builtModelState.smoothMeshingStrength + } isOrtho={isOrtho} flatPaint={builtFlatPaint} flatPaintFaceUp={!!builtModelState.flatPaintFaceUp} @@ -1072,6 +1135,16 @@ function App(): React.ReactElement | null { {/* Update checker for Tauri desktop app */} + {desktopFile && + (desktopFile.content === null || failedPaletteFileId === desktopFile.id) && ( + { + setFailedPaletteFileId(null); + finishDesktopFile(desktopFile.id); + }} + /> + )} {printing && } diff --git a/src/components/AutoPaintTab.tsx b/src/components/AutoPaintTab.tsx index d6834bdf..b6b6ae72 100644 --- a/src/components/AutoPaintTab.tsx +++ b/src/components/AutoPaintTab.tsx @@ -334,12 +334,15 @@ interface AutoPaintSliceData { } interface AutoPaintTabProps { + calibrationDialogOpen: boolean; + onCalibrationDialogOpenChange: (open: boolean) => void; // Filament state filaments: Filament[]; addFilament: () => void; addFilamentWithProps: (props: { color: string; td: number; name: string }) => void; removeFilament: (id: string) => void; updateFilament: (id: string, updates: Partial>) => void; + onFilamentEditingChange: (id: string, editing: boolean) => void; // Profile state profiles: AutoPaintProfile[]; @@ -431,11 +434,14 @@ interface AutoPaintTabProps { } export default function AutoPaintTab({ + calibrationDialogOpen, + onCalibrationDialogOpenChange, filaments, addFilament, addFilamentWithProps, removeFilament, updateFilament, + onFilamentEditingChange, profiles, activeProfileId, isDirty, @@ -537,19 +543,16 @@ export default function AutoPaintTab({ ); const exactBaseOrderIsLarge = exactBaseOrderCount >= 1_000_000 || filaments.length >= 9; - // Calibration dialog state - const [calibrationDialogOpen, setCalibrationDialogOpen] = React.useState(false); - // Built-in templates are read-only: no overwrite, rename, or delete const isTemplateActive = activeProfileId !== null && isTemplateProfileId(activeProfileId); const handleOpenCalibration = React.useCallback(() => { - setCalibrationDialogOpen(true); - }, []); + onCalibrationDialogOpenChange(true); + }, [onCalibrationDialogOpenChange]); const handleCloseCalibration = React.useCallback(() => { - setCalibrationDialogOpen(false); - }, []); + onCalibrationDialogOpenChange(false); + }, [onCalibrationDialogOpenChange]); const handleApplyCalibration = React.useCallback( (updates: CalibrationApplyUpdate[]) => { @@ -863,6 +866,7 @@ export default function AutoPaintTab({ filament={f} onUpdate={updateFilament} onRemove={removeFilament} + onEditingChange={onFilamentEditingChange} /> ))} diff --git a/src/components/ControlsPanel.tsx b/src/components/ControlsPanel.tsx index edda1018..f4b17463 100644 --- a/src/components/ControlsPanel.tsx +++ b/src/components/ControlsPanel.tsx @@ -56,6 +56,8 @@ interface Props { onDeletePalette: (id: string) => void; onExportPalette: (id: string) => void; onImportPaletteFile: (e: React.ChangeEvent) => void; + paletteEditorOpen: boolean; + onPaletteEditorOpenChange: (open: boolean) => void; } export const ControlsPanel: React.FC = ({ @@ -82,6 +84,8 @@ export const ControlsPanel: React.FC = ({ onDeletePalette, onExportPalette, onImportPaletteFile, + paletteEditorOpen, + onPaletteEditorOpenChange, }) => { const { t } = useTranslation('workspace'); // Local state for relaxed typing @@ -299,6 +303,8 @@ export const ControlsPanel: React.FC = ({ onDeletePalette={onDeletePalette} onExportPalette={onExportPalette} onImportFile={onImportPaletteFile} + dialogOpen={paletteEditorOpen} + onDialogOpenChange={onPaletteEditorOpenChange} />
diff --git a/src/components/DesktopFileDialog.tsx b/src/components/DesktopFileDialog.tsx new file mode 100644 index 00000000..5e6fed80 --- /dev/null +++ b/src/components/DesktopFileDialog.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from 'react-i18next'; +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogCancel, + AlertDialogAction, +} from './ui/alert-dialog'; + +interface Props { + name: string; + onDismiss: () => void; + onConfirm?: () => void; +} + +/** Uses the app's existing dialog and buttons for OS-open errors and unsaved edits. */ +export function DesktopFileDialog({ name, onDismiss, onConfirm }: Props) { + const { t } = useTranslation('common'); + return ( + { + if (!open) onDismiss(); + }} + > + + + + {t(onConfirm ? 'desktopFiles.unsavedTitle' : 'desktopFiles.failedTitle')} + + + {t( + onConfirm + ? 'desktopFiles.unsavedDescription' + : name + ? 'desktopFiles.failedDescription' + : 'desktopFiles.unavailable', + { name } + )} + + + + + {t(onConfirm ? 'desktopFiles.keepEdits' : 'desktopFiles.close')} + + {onConfirm && ( + { + event.preventDefault(); + onConfirm(); + }} + > + {t('desktopFiles.openProfile')} + + )} + + + + ); +} diff --git a/src/components/DesktopProfileImport.tsx b/src/components/DesktopProfileImport.tsx new file mode 100644 index 00000000..9fbb1473 --- /dev/null +++ b/src/components/DesktopProfileImport.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef, useState } from 'react'; +import type { DesktopFileRequest } from '../lib/desktopFileInbox'; +import { DesktopFileDialog } from './DesktopFileDialog'; + +interface Props { + file: DesktopFileRequest | null; + hasUnsavedChanges: boolean; + importText: (content: string, name: string, selectExisting?: boolean) => boolean; + onImported: () => void; + onFinished: (id: number) => void; +} + +export function DesktopProfileImport({ + file, + hasUnsavedChanges, + importText, + onImported, + onFinished, +}: Props) { + const handled = useRef(null); + const [dialog, setDialog] = useState<{ file: DesktopFileRequest; confirm: boolean } | null>( + null + ); + const [completedId, setCompletedId] = useState(null); + const callbacks = useRef({ importText, onImported, onFinished }); + callbacks.current = { importText, onImported, onFinished }; + + const importFile = (request: DesktopFileRequest) => { + let imported = false; + try { + imported = callbacks.current.importText(request.content!, request.name, true); + } catch (error) { + console.error('Could not import the opened profile', error); + } + if (imported) { + callbacks.current.onImported(); + setCompletedId(request.id); + setDialog(null); + } else { + setDialog({ file: request, confirm: false }); + } + }; + const runImport = useRef(importFile); + runImport.current = importFile; + useEffect(() => { + if (!file || handled.current === file.id) return; + handled.current = file.id; + if (hasUnsavedChanges) setDialog({ file, confirm: true }); + else runImport.current(file); + }, [file, hasUnsavedChanges]); + // Advance only after the imported profiles/filaments have committed to React state. + useEffect(() => { + if (completedId !== null) callbacks.current.onFinished(completedId); + }, [completedId]); + + if (!dialog) return null; + return ( + { + callbacks.current.onFinished(dialog.file.id); + setDialog(null); + }} + onConfirm={dialog.confirm ? () => runImport.current(dialog.file) : undefined} + /> + ); +} diff --git a/src/components/FilamentRow.tsx b/src/components/FilamentRow.tsx index 4953a2bb..25ab97ea 100644 --- a/src/components/FilamentRow.tsx +++ b/src/components/FilamentRow.tsx @@ -25,12 +25,14 @@ interface FilamentRowProps { filament: Filament; onUpdate: (id: string, updates: Partial>) => void; onRemove: (id: string) => void; + onEditingChange: (id: string, editing: boolean) => void; } const FilamentRow = React.memo(function FilamentRow({ filament, onUpdate, onRemove, + onEditingChange, }: FilamentRowProps) { const { t } = useTranslation('printing'); // Local state for the input value to allow free typing @@ -63,6 +65,16 @@ const FilamentRow = React.memo(function FilamentRow({ // "Convert from TD" popover state (conventional backlit/lithophane value). const [convertDraft, setConvertDraft] = useState(''); const [convertOpen, setConvertOpen] = useState(false); + const [colorOpen, setColorOpen] = useState(false); + const [inputFocused, setInputFocused] = useState(false); + const editing = inputFocused || colorOpen || convertOpen || localColor !== filament.color; + + // OS opens do not blur the active input. Hold them until drafts are committed, + // including color changes still waiting for the debounced parent update. + useEffect(() => { + onEditingChange(filament.id, editing); + }, [filament.id, editing, onEditingChange]); + useEffect(() => () => onEditingChange(filament.id, false), [filament.id, onEditingChange]); // Sync local TD state if prop changes externally useEffect(() => { @@ -111,7 +123,12 @@ const FilamentRow = React.memo(function FilamentRow({ }; const handleNameKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); + if (e.key === 'Enter') { + // A queued import may focus its confirmation as this edit commits. + // Do not let the same Enter also activate that dialog's button. + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + } }; const handleBlur = () => { @@ -140,15 +157,39 @@ const FilamentRow = React.memo(function FilamentRow({ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { + e.preventDefault(); handleBlur(); (e.target as HTMLInputElement).blur(); } }; return ( -
+
{ + // Portal inputs are protected by their popover's open state; + // unmounting them does not reliably fire a blur event. + const focused = + event.target instanceof HTMLInputElement && + event.currentTarget.contains(event.target); + if (focused) onEditingChange(filament.id, true); + setInputFocused(focused); + }} + onBlurCapture={(event) => { + setInputFocused( + event.relatedTarget instanceof HTMLInputElement && + event.currentTarget.contains(event.relatedTarget) + ); + }} + > {/* Color Picker Popover */} - + { + if (open) onEditingChange(filament.id, true); + setColorOpen(open); + }} + >
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index dad18658..b0061d65 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -387,7 +387,7 @@ function ShowcaseCard({ items }: { items: ShowcaseItem[] }) { i18nKey="landing.originalArtwork" values={{ title: artwork.label }} components={{ - source: ( + credit: ( void; onExportPalette: (id: string) => void; onImportFile: (e: React.ChangeEvent) => void; + dialogOpen: boolean; + onDialogOpenChange: (open: boolean) => void; } /** Check if a string is a valid hex color (#RGB or #RRGGBB). */ @@ -82,9 +84,10 @@ export const PaletteManager: React.FC = ({ onDeletePalette, onExportPalette, onImportFile, + dialogOpen, + onDialogOpenChange: setDialogOpen, }) => { const { t } = useTranslation('workspace'); - const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [editor, setEditor] = useState({ name: '', @@ -97,7 +100,7 @@ export const PaletteManager: React.FC = ({ setEditingId(null); setEditor({ name: '', colors: [{ hex: '#FF0000', enabled: true, name: '' }] }); setDialogOpen(true); - }, []); + }, [setDialogOpen]); const openEdit = useCallback(() => { const cp = customPalettes.find((p) => p.id === selectedPalette); @@ -113,7 +116,7 @@ export const PaletteManager: React.FC = ({ })), }); setDialogOpen(true); - }, [customPalettes, selectedPalette]); + }, [customPalettes, selectedPalette, setDialogOpen]); const handleSave = useCallback(() => { const trimmed = editor.name.trim(); @@ -129,7 +132,7 @@ export const PaletteManager: React.FC = ({ onCreatePalette(trimmed, colors, disabledColors, colorNames); } setDialogOpen(false); - }, [editor, editingId, onCreatePalette, onUpdatePalette]); + }, [editor, editingId, onCreatePalette, onUpdatePalette, setDialogOpen]); const addColor = useCallback(() => { setEditor((prev) => ({ diff --git a/src/components/PrintInstructions.tsx b/src/components/PrintInstructions.tsx index 5ea49736..b572c35f 100644 --- a/src/components/PrintInstructions.tsx +++ b/src/components/PrintInstructions.tsx @@ -1,6 +1,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { CollapsibleCard, DirtyDot } from '@/components/CollapsibleCard'; import type { SwapEntry } from '../hooks/useSwapPlan'; +import type { SmoothMeshingStrength } from '../types'; interface PrintInstructionsProps { swapPlan: SwapEntry[]; @@ -14,6 +15,7 @@ interface PrintInstructionsProps { flatPaint?: boolean; /** Flat Paint is printed face-up without a transparent carrier. */ flatPaintFaceUp?: boolean; + smoothMeshingStrength?: SmoothMeshingStrength; } export default function PrintInstructions({ @@ -26,8 +28,15 @@ export default function PrintInstructions({ colorCount = 0, flatPaint = false, flatPaintFaceUp = false, + smoothMeshingStrength = 'none', }: PrintInstructionsProps) { const { t } = useTranslation('printing'); + const strengthKeys = { + none: 'printSettingsCard.smoothingNone', + minimal: 'printSettingsCard.smoothingMinimal', + medium: 'printSettingsCard.smoothingMedium', + aggressive: 'printSettingsCard.smoothingAggressive', + }; return (
+
+ {t('printSettingsCard.smoothMeshing')}:{' '} + {t(strengthKeys[smoothMeshingStrength])} +
void; onSlicerFirstLayerHeightChange: (v: number) => void; onPixelSizeChange: (v: number) => void; onEffectiveLineWidthChange: (v: number) => void; - onSmoothMeshingChange: (v: boolean) => void; + onSmoothMeshingStrengthChange: (v: SmoothMeshingStrength) => void; onReset: () => void; allDefault?: boolean; } @@ -106,12 +113,12 @@ export default function PrintSettingsCard({ pixelSize, effectiveLineWidth, modelSizeEstimate, - smoothMeshing, + smoothMeshingStrength, onLayerHeightChange, onSlicerFirstLayerHeightChange, onPixelSizeChange, onEffectiveLineWidthChange, - onSmoothMeshingChange, + onSmoothMeshingStrengthChange, onReset, allDefault = false, }: PrintSettingsCardProps) { @@ -324,17 +331,38 @@ export default function PrintSettingsCard({ {t('printSettingsCard.smoothMeshing')}

- {t( - 'printSettingsCard.smoothConnectedColorBoundaryEdgesWithFastWeldedTopology' - )} + {t('printSettingsCard.smoothingHelp')}

- +
diff --git a/src/components/ThreeDControls.tsx b/src/components/ThreeDControls.tsx index 6bd630a3..5bf1f289 100644 --- a/src/components/ThreeDControls.tsx +++ b/src/components/ThreeDControls.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CollapsibleCard, DirtyDot } from '@/components/CollapsibleCard'; import ThreeDColorRow from './ThreeDColorRow'; import { Sortable, SortableContent, SortableOverlay } from '@/components/ui/sortable'; @@ -28,6 +28,7 @@ import type { AutoPaintRepeatLimit, AutoPaintTransitionOpacity, Swatch, + SmoothMeshingStrength, ThreeDControlsStateShape, } from '../types'; import PrintSettingsCard from './PrintSettingsCard'; @@ -36,6 +37,8 @@ import AutoPaintTab from './AutoPaintTab'; import type { ImageDimensions } from '../hooks/useSwatches'; import { concealPrintableFeatureBuffers } from '../lib/printableFeatures.ts'; import { shouldApplyInitialProfileFilaments } from '../lib/threeDWorkLifecycle'; +import type { DesktopFileRequest } from '../lib/desktopFileInbox'; +import { DesktopProfileImport } from './DesktopProfileImport'; // Re-export types for backward compatibility export type { Filament, ThreeDControlsStateShape } from '../types'; @@ -63,6 +66,15 @@ interface ThreeDControlsProps { persisted?: ThreeDControlsStateShape | null; /** Whether `persisted.filaments` belongs to an authoritative saved or in-session workspace. */ hasAutoPaintWorkingState?: boolean; + desktopProfileFile?: DesktopFileRequest | null; + onDesktopFileFinished?: (id: number) => void; + calibrationDialogOpen: boolean; + onCalibrationDialogOpenChange: (open: boolean) => void; + profileSaveNewOpen: boolean; + onProfileSaveNewOpenChange: (open: boolean) => void; + profileRenameOpen: boolean; + onProfileRenameOpenChange: (open: boolean) => void; + onFilamentEditingChange: (editing: boolean) => void; } export default function ThreeDControls({ @@ -76,6 +88,15 @@ export default function ThreeDControls({ onSettingsChange, persisted, hasAutoPaintWorkingState = false, + desktopProfileFile = null, + onDesktopFileFinished, + calibrationDialogOpen, + onCalibrationDialogOpenChange, + profileSaveNewOpen, + onProfileSaveNewOpenChange, + profileRenameOpen, + onProfileRenameOpenChange, + onFilamentEditingChange, }: ThreeDControlsProps) { const { t } = useTranslation('printing'); // --- Filaments --- @@ -91,7 +112,26 @@ export default function ThreeDControls({ }); // --- Profiles --- - const profileManager = useProfileManager({ filaments, setFilaments }); + const profileManager = useProfileManager({ + filaments, + setFilaments, + showSaveNewPopover: profileSaveNewOpen, + setShowSaveNewPopover: onProfileSaveNewOpenChange, + showRenamePopover: profileRenameOpen, + setShowRenamePopover: onProfileRenameOpenChange, + }); + const [profileReady, setProfileReady] = useState(false); + const editingFilaments = useRef(new Set()); + const handleFilamentEditingChange = useCallback( + (id: string, editing: boolean) => { + if (editingFilaments.current.has(id) === editing) return; + if (editing) editingFilaments.current.add(id); + else editingFilaments.current.delete(id); + onFilamentEditingChange(editingFilaments.current.size > 0); + }, + [onFilamentEditingChange] + ); + useEffect(() => () => onFilamentEditingChange(false), [onFilamentEditingChange]); // Apply initial filaments from profile if available (one-time) const [appliedProfileInit] = useState(() => { @@ -110,6 +150,7 @@ export default function ThreeDControls({ if (appliedProfileInit) { setFilaments(appliedProfileInit); } + setProfileReady(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -119,10 +160,10 @@ export default function ThreeDControls({ // --- Print Settings --- const [initialPrintSettings] = useState(() => { const stored = loadPrintSettingsFromStorage(); - const storedSmoothMeshing = - stored?.smoothMeshing ?? - persisted?.smoothMeshing ?? - DEFAULT_PRINT_SETTINGS.smoothMeshing; + const storedSmoothMeshingStrength = + stored?.smoothMeshingStrength ?? + persisted?.smoothMeshingStrength ?? + DEFAULT_PRINT_SETTINGS.smoothMeshingStrength; return { layerHeight: stored?.layerHeight ?? persisted?.layerHeight ?? DEFAULT_PRINT_SETTINGS.layerHeight, @@ -132,7 +173,7 @@ export default function ThreeDControls({ DEFAULT_PRINT_SETTINGS.slicerFirstLayerHeight, pixelSize: stored?.pixelSize ?? persisted?.pixelSize ?? DEFAULT_PRINT_SETTINGS.pixelSize, - smoothMeshing: storedSmoothMeshing, + smoothMeshingStrength: storedSmoothMeshingStrength, }; }); @@ -141,7 +182,9 @@ export default function ThreeDControls({ initialPrintSettings.slicerFirstLayerHeight ); const [pixelSize, setPixelSize] = useState(initialPrintSettings.pixelSize); - const [smoothMeshing, setSmoothMeshing] = useState(initialPrintSettings.smoothMeshing); + const [smoothMeshingStrength, setSmoothMeshingStrength] = useState( + initialPrintSettings.smoothMeshingStrength + ); const [calibrationLayerHeight, setCalibrationLayerHeight] = useState( persisted?.calibrationLayerHeight ?? initialPrintSettings.layerHeight ); @@ -210,13 +253,9 @@ export default function ThreeDControls({ }, []); const flatPaintActive = paintMode === 'autopaint' && flatPaint; - const effectiveSmoothMeshing = flatPaintActive ? false : smoothMeshing; - const handleSmoothMeshingChange = useCallback((enabled: boolean) => { - setSmoothMeshing(enabled); - if (enabled) { - setFlatPaint(false); - } + const handleSmoothMeshingStrengthChange = useCallback((strength: SmoothMeshingStrength) => { + setSmoothMeshingStrength(strength); }, []); const handleFlatPaintChange = useCallback((enabled: boolean) => { @@ -244,7 +283,7 @@ export default function ThreeDControls({ optimizerAlgorithm, optimizerSeed, regionWeightingMode, - smoothMeshing, + smoothMeshingStrength, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -266,7 +305,7 @@ export default function ThreeDControls({ optimizerAlgorithm, optimizerSeed, regionWeightingMode, - smoothMeshing, + smoothMeshingStrength, ]); useEffect(() => { @@ -274,9 +313,9 @@ export default function ThreeDControls({ layerHeight, slicerFirstLayerHeight, pixelSize, - smoothMeshing, + smoothMeshingStrength, }); - }, [layerHeight, slicerFirstLayerHeight, pixelSize, smoothMeshing]); + }, [layerHeight, slicerFirstLayerHeight, pixelSize, smoothMeshingStrength]); // --- Color Slicing --- const { @@ -300,7 +339,7 @@ export default function ThreeDControls({ setLayerHeight(DEFAULT_PRINT_SETTINGS.layerHeight); setSlicerFirstLayerHeight(DEFAULT_PRINT_SETTINGS.slicerFirstLayerHeight); setPixelSize(DEFAULT_PRINT_SETTINGS.pixelSize); - setSmoothMeshing(DEFAULT_PRINT_SETTINGS.smoothMeshing); + setSmoothMeshingStrength(DEFAULT_PRINT_SETTINGS.smoothMeshingStrength); setDitherLineWidth(DEFAULT_EFFECTIVE_LINE_WIDTH); resetHeightsToValues( DEFAULT_PRINT_SETTINGS.layerHeight, @@ -450,6 +489,7 @@ export default function ThreeDControls({ disabled: isInstructionOverLimit, flatPaint: instructionFlatPaint, flatPaintFaceUp: instructionFlatPaintFaceUp, + smoothMeshingStrength: builtState?.smoothMeshingStrength ?? smoothMeshingStrength, }); // --- Apply handler --- @@ -502,7 +542,7 @@ export default function ThreeDControls({ fingerprint: printableFeatureSimulation.fingerprint, }), calibrationLayerHeight, - smoothMeshing, + smoothMeshingStrength, }); return; } @@ -522,7 +562,7 @@ export default function ThreeDControls({ optimizerSeed, regionWeightingMode, calibrationLayerHeight, - smoothMeshing, + smoothMeshingStrength, }); }, [ onChange, @@ -550,7 +590,7 @@ export default function ThreeDControls({ optimizerSeed, regionWeightingMode, calibrationLayerHeight, - smoothMeshing, + smoothMeshingStrength, autoPaintResult, autoPaintSliceData, printableFeatureSimulation, @@ -567,6 +607,18 @@ export default function ThreeDControls({ return (
+ {profileReady && onDesktopFileFinished && ( + 0) + } + importText={profileManager.importText} + onImported={() => setPaintMode('autopaint')} + onFinished={onDesktopFileFinished} + /> + )} {/* Apply button */}