Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2191c2e
Add auto-paint regression baseline
vycdev Jun 20, 2026
43eb66e
Add auto-paint benchmark harness
vycdev Jun 20, 2026
1544270
Fix auto-paint region priority
vycdev Jun 21, 2026
966c13a
Make auto-paint optimization deterministic
vycdev Jun 21, 2026
bc1d155
Unify auto-paint optimizer scoring
vycdev Jun 21, 2026
6d41030
Add variable-length auto-paint search
vycdev Jun 21, 2026
d685832
Improve auto-paint progress and 3MF exports
vycdev Jun 21, 2026
46a6662
Harden auto-paint physics and 3MF export
vycdev Jun 21, 2026
109a417
Keep auto-paint plan local
vycdev Jun 21, 2026
9d766cd
Allow explicit exhaustive auto-paint search
vycdev Jun 22, 2026
30ecd49
Add Reddit community links
vycdev Jun 22, 2026
6823226
Improve calibration image sampler
vycdev Jun 24, 2026
15c64b0
Revert gitignore
vycdev Jun 24, 2026
8df9f88
Fix auto-paint printable height planning
vycdev Jun 24, 2026
0ec0b49
Use calibrated TDs for auto-paint transitions
vycdev Jun 24, 2026
2efd547
Chain auto-paint transition colors
vycdev Jun 24, 2026
f1273b5
Use linear-light auto-paint blending
vycdev Jun 24, 2026
7d6174e
Fit filament calibration in linear-light model
vycdev Jun 24, 2026
89716a1
Score auto-paint with CIEDE2000 and p95 tail term
vycdev Jun 24, 2026
dc99787
Replace auto-paint optimizer menu with effort tiers
vycdev Jun 24, 2026
fff98e7
@
vycdev Jun 24, 2026
67ea448
Add auto-paint detail controls and optimizer tiers
vycdev Jun 24, 2026
fc6ce13
Merge branch 'autopaint-improvements' of https://github.com/vycdev/Kr…
vycdev Jun 24, 2026
e58a0d6
Use Stucki kernel for height dithering
vycdev Jun 24, 2026
f3449dc
Score auto-paint with the preview's printable-color mapping
vycdev Jun 25, 2026
5b1b268
Add Preserve color separation auto-paint mode
vycdev Jun 25, 2026
9b60948
Add 8-color frontlit-calibrated profile fixture
vycdev Jun 25, 2026
bd7d734
Trim Auto-paint settings helper UI
vycdev Jun 25, 2026
a3b7326
Clean up v3.2 changelog entries
vycdev Jun 25, 2026
ca4dc7c
Make auto-paint height modes exclusive
vycdev Jun 25, 2026
8499a00
Precompute auto-paint region weights and calibrate foundation
vycdev Jun 28, 2026
8242167
Drop the unused stored calibration transmission field
vycdev Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,26 @@ All notable changes to Kromacut are documented in this file.

### Added

- **Preserve color separation (Auto-paint)** - Enhanced color matching can keep each distinct 2D image color mapped to a distinct printable color when the stack exposes enough printable colors, preserving gradients that would otherwise collapse onto a flat surface. It is mutually exclusive with Height dithering because both modes change the same printable height map.
- **Auto-paint optimization progress** - Enhanced matching now reports approximate search progress while the background optimizer runs.
- **Auto-paint test and benchmark coverage** - Added deterministic stack goldens, layer-invariant regression coverage, realized CIEDE2000 quality-budget tests, and an on-demand benchmark for fixture profiles using the same printable-stack mapper as the optimizer.
- **Reddit community links** - Added r/kromacut links to the app header and README, and use branded Discord, Reddit, and GitHub icons in the header toolbar.

### Changed

- **Calibration image sampler** - The sampler now shows a circular brush over the exact image area it averages, plus a marker for the last captured sample, making it easier to avoid patch edges and glare.
- **Auto-paint enhanced matching** - Optimizer scoring now follows the same layer-snapped, Max Height-compressed printable stack used by preview and export, scores realized print error in CIEDE2000 with a weighted p95 tail term, and uses complete target-color/cache inputs so repeated runs are deterministic.
- **Auto-paint optimizer controls** - Replaced the older optimizer choices with deterministic effort tiers (Fast, Balanced, Thorough, Deep, and Exact base order), selector-based repeated swaps, transition-detail endpoints, and explicit stable seed handling. Legacy saved values migrate to the nearest current tier.
- **Auto-paint optical model** - Beer-Lambert blending now runs in linear-light sRGB, and calibrated filaments use measured RGB-channel TDs for blend simulation and transition-zone thickness. Recalibrate profiles created with earlier releases before using them for new prints.
- **Height dithering kernel** - Height dithering now uses an error-conserving Stucki kernel instead of Floyd-Steinberg, spreading quantization error over a wider area while keeping the existing block-aware dot sizing and edge protection.

### Fixed

- **Auto-paint Max Height** - Auto-paint now plans, scores, previews, and exports the same layer-aligned stack. Height caps round down to a valid printable layer boundary, so a generated model no longer exceeds the requested maximum by adding a final whole layer.
- **Auto-paint region priority** - Center and Edge priority now use the actual locations of each image color. Center prioritizes colors near the image middle; Edge prioritizes colors near the outer border. The optimizer no longer allocates a full-image weight map or guesses location from color brightness.
- **Auto-paint edge cases** - Blank seeds now resolve to stable cacheable values, optimizer cache keys include all target clusters and tuning inputs, and exceptionally tall stacks stop at the intended 500-layer slice-data limit.
- **Desktop 3MF export reliability** - 3MF model XML now streams into the archive in bounded chunks, avoiding desktop WebView `FileReader` `NotReadableError` failures and `RangeError: Invalid string length` on large exports.

## v3.1.0 - 2026-06-18

### Added
Expand Down
56 changes: 33 additions & 23 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"dev": "vite",
"build": "tsc -b && vite build && node scripts/generate-docs-seo-pages.mjs",
"test": "node --no-warnings --experimental-strip-types tests/run-tests.ts",
"test:autopaint:update": "node --no-warnings --experimental-strip-types scripts/generate-auto-paint-goldens.ts",
"benchmark:autopaint": "node --no-warnings --experimental-strip-types tests/benchmark/autoPaintBench.ts",
"test:e2e": "playwright test --grep @smoke",
"test:e2e:matrix": "playwright test --grep @matrix",
"test:e2e:stress": "playwright test --grep @stress",
Expand Down
67 changes: 67 additions & 0 deletions scripts/generate-auto-paint-goldens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { createServer } from 'vite';

import { autoPaintGoldenScenarios } from '../tests/autoPaintGoldenFixtures.ts';

type AutoPaintModule = typeof import('../src/lib/autoPaint.ts');

const LAYER_HEIGHT = 0.08;
const FIRST_LAYER_HEIGHT = 0.16;

async function loadAutoPaintModule(): Promise<AutoPaintModule> {
const server = await createServer({
appType: 'custom',
cacheDir: 'dist/.vite-test-cache',
configFile: false,
logLevel: 'error',
optimizeDeps: { noDiscovery: true },
resolve: { alias: { '@': resolve(process.cwd(), 'src') } },
root: process.cwd(),
server: { hmr: false, middlewareMode: true },
});

try {
return (await server.ssrLoadModule('/src/lib/autoPaint.ts')) as AutoPaintModule;
} finally {
await server.close();
}
}

const { generateAutoLayers } = await loadAutoPaintModule();
const goldens = Object.fromEntries(
autoPaintGoldenScenarios().map((scenario) => {
const result = generateAutoLayers(
scenario.filaments,
scenario.imageSwatches,
LAYER_HEIGHT,
FIRST_LAYER_HEIGHT,
undefined,
scenario.enhancedColorMatch,
scenario.allowRepeatedSwaps,
{ algorithm: 'balanced', seed: scenario.seed },
'uniform',
scenario.imageDimensions
);

return [
scenario.name,
{
filamentOrder: result.filamentOrder,
transitionZones: result.transitionZones.map((zone) => ({
filamentId: zone.filamentId,
startHeight: zone.startHeight,
endHeight: zone.endHeight,
idealThickness: zone.idealThickness,
actualThickness: zone.actualThickness,
})),
totalHeight: result.totalHeight,
compressionRatio: result.compressionRatio,
},
];
})
);

const outputPath = resolve('tests', 'assets', 'auto-paint-goldens.json');
writeFileSync(outputPath, `${JSON.stringify(goldens, null, 2)}\n`);
console.log(`Wrote ${Object.keys(goldens).length} auto-paint goldens to ${outputPath}`);
49 changes: 43 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React, { useEffect, useRef, useState } from 'react';
import ThreeDControls from './components/ThreeDControls';
import type { ThreeDControlsStateShape } from './types';
import {
AUTO_PAINT_REPEAT_LIMITS,
AUTO_PAINT_TRANSITION_OPACITIES,
type AutoPaintRepeatLimit,
type AutoPaintTransitionOpacity,
type ThreeDControlsStateShape,
} from './types';
import ThreeDView from './components/ThreeDView';
import logo from './assets/logo.png';
import tdTestImg from './assets/tdTest.png';
Expand All @@ -27,6 +33,7 @@ import { useAppHandlers, type ExportProgressStep } from './hooks/useAppHandlers'
import { useProcessingState } from './hooks/useProcessingState';
import { useBuildWarning } from './hooks/useBuildWarning';
import { clampProgress } from './lib/progress';
import { normalizeOptimizerTier } from './lib/optimizer';
import ResizableSplitter from './components/ResizableSplitter';
import { ControlsPanel } from './components/ControlsPanel';
import { usePaletteManager } from './hooks/usePaletteManager';
Expand Down Expand Up @@ -77,12 +84,28 @@ type AutoPaintPersisted = Pick<
| 'optimizerSeed'
| 'regionWeightingMode'
| 'enhancedColorMatch'
| 'preserveSeparation'
| 'allowRepeatedSwaps'
| 'maxRepeatedSwaps'
| 'transitionOpacity'
| 'heightDithering'
| 'ditherLineWidth'
| 'flatPaint'
>;

function isOneOf<T extends readonly number[]>(value: unknown, values: T): value is T[number] {
return typeof value === 'number' && values.includes(value as T[number]);
}

function normalizeRepeatLimit(value: unknown, legacyEnabled: unknown): AutoPaintRepeatLimit {
if (isOneOf(value, AUTO_PAINT_REPEAT_LIMITS)) return value;
return legacyEnabled === true ? 4 : 0;
}

function normalizeTransitionOpacity(value: unknown): AutoPaintTransitionOpacity {
return isOneOf(value, AUTO_PAINT_TRANSITION_OPACITIES) ? value : 0.9;
}

const loadAutoPaintPersisted = (): AutoPaintPersisted | null => {
try {
const raw = localStorage.getItem(AUTOPAINT_STORAGE_KEY);
Expand All @@ -100,11 +123,16 @@ const loadAutoPaintPersisted = (): AutoPaintPersisted | null => {
return {
filaments: parsed.filaments,
paintMode,
optimizerAlgorithm: parsed.optimizerAlgorithm,
optimizerAlgorithm: normalizeOptimizerTier(parsed.optimizerAlgorithm),
optimizerSeed: parsed.optimizerSeed,
regionWeightingMode: parsed.regionWeightingMode,
enhancedColorMatch: parsed.enhancedColorMatch ?? false,
allowRepeatedSwaps: parsed.allowRepeatedSwaps ?? false,
preserveSeparation: parsed.preserveSeparation ?? false,
maxRepeatedSwaps: normalizeRepeatLimit(
parsed.maxRepeatedSwaps,
parsed.allowRepeatedSwaps
),
transitionOpacity: normalizeTransitionOpacity(parsed.transitionOpacity),
heightDithering: parsed.heightDithering ?? false,
ditherLineWidth: parsed.ditherLineWidth,
flatPaint: parsed.flatPaint ?? false,
Expand Down Expand Up @@ -242,7 +270,11 @@ function App(): React.ReactElement | null {
regionWeightingMode:
autopaintHydrated.regionWeightingMode ?? prev.regionWeightingMode,
enhancedColorMatch: autopaintHydrated.enhancedColorMatch ?? prev.enhancedColorMatch,
allowRepeatedSwaps: autopaintHydrated.allowRepeatedSwaps ?? prev.allowRepeatedSwaps,
preserveSeparation:
autopaintHydrated.preserveSeparation ?? prev.preserveSeparation,
maxRepeatedSwaps:
autopaintHydrated.maxRepeatedSwaps ?? prev.maxRepeatedSwaps,
transitionOpacity: autopaintHydrated.transitionOpacity ?? prev.transitionOpacity,
heightDithering: autopaintHydrated.heightDithering ?? prev.heightDithering,
ditherLineWidth: autopaintHydrated.ditherLineWidth ?? prev.ditherLineWidth,
flatPaint: autopaintHydrated.flatPaint ?? prev.flatPaint,
Expand All @@ -261,7 +293,9 @@ function App(): React.ReactElement | null {
optimizerSeed: threeDState.optimizerSeed,
regionWeightingMode: threeDState.regionWeightingMode,
enhancedColorMatch: threeDState.enhancedColorMatch,
allowRepeatedSwaps: threeDState.allowRepeatedSwaps,
preserveSeparation: threeDState.preserveSeparation,
maxRepeatedSwaps: threeDState.maxRepeatedSwaps,
transitionOpacity: threeDState.transitionOpacity,
heightDithering: threeDState.heightDithering,
ditherLineWidth: threeDState.ditherLineWidth,
flatPaint: threeDState.flatPaint,
Expand All @@ -273,7 +307,9 @@ function App(): React.ReactElement | null {
threeDState.optimizerSeed,
threeDState.regionWeightingMode,
threeDState.enhancedColorMatch,
threeDState.allowRepeatedSwaps,
threeDState.preserveSeparation,
threeDState.maxRepeatedSwaps,
threeDState.transitionOpacity,
threeDState.heightDithering,
threeDState.ditherLineWidth,
threeDState.flatPaint,
Expand Down Expand Up @@ -684,6 +720,7 @@ function App(): React.ReactElement | null {
builtModelState.autoPaintResult?.filamentOrder
}
enhancedColorMatch={builtModelState.enhancedColorMatch}
preserveSeparation={builtModelState.preserveSeparation}
heightDithering={builtModelState.heightDithering}
ditherLineWidth={builtModelState.ditherLineWidth}
smoothMeshing={builtModelState.smoothMeshing}
Expand Down
1 change: 1 addition & 0 deletions src/assets/discord.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/reddit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading