Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
149 commits
Select commit Hold shift + click to select a range
91d2355
Add support for importing filament profiles from HueForge spool CSV e…
Bjohnson131 Jun 12, 2026
1538067
Enhance CSV/TSV parsing in profile manager to handle quoted fields an…
Bjohnson131 Jun 14, 2026
2f4a4e6
Enhance CSV parsing to normalize hex colors and skip invalid rows
Bjohnson131 Jun 14, 2026
35f2e85
Prepare 3.2.0 development cycle
vycdev Jun 18, 2026
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
8b838c5
update to note tsv in docs, add escaped newline parsing.
Bjohnson131 Jun 21, 2026
e4ae4b3
Merge branch 'develop' into csv-import
Bjohnson131 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
8ee7e75
Merge pull request #37 from Bjohnson131/csv-import
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
cf1dbbd
Add reference to AGENTS.md in CLAUDE.md
vycdev Jun 29, 2026
354f736
Replace photo calibration with frontlit opacity-read calibration
vycdev Jul 2, 2026
bd4fa71
Remove the Load TD Test header button
vycdev Jul 2, 2026
ad6567b
Fix frontlit calibration review issues
vycdev Jul 2, 2026
8d0a2a7
Add multi-base color calibration
vycdev Jul 2, 2026
607b550
Migrate TD to hiding distance with single-meaning storage
vycdev Jul 3, 2026
5aa9c78
Remove the per-row calibrate button
vycdev Jul 4, 2026
85c8d73
Polish the auto-paint filament list UI
vycdev Jul 4, 2026
c8bb0ae
Keep filament row action buttons always visible
vycdev Jul 4, 2026
dd897e6
Polish the auto-paint results UI
vycdev Jul 4, 2026
0290459
Drop decorative icons from the auto-paint results section
vycdev Jul 4, 2026
8c0d04e
Share the session JND fit between calibration preview and save
vycdev Jul 4, 2026
e9e89fb
Document that re-calibrating a filament replaces its stored value
vycdev Jul 4, 2026
e710908
Block calibration save while the session JND fit is pending
vycdev Jul 4, 2026
fad7c60
Deactivate a filament's calibration when its color is edited
vycdev Jul 4, 2026
f83f48e
Merge branch 'develop' into autopaint-improvements
vycdev Jul 5, 2026
2b2286a
Add TileSettings and GridSettings interfaces for multi-image tile con…
Bjohnson131 Jul 6, 2026
369d530
Fix filament calibration confidence updates
vycdev Jul 6, 2026
1782d2c
test: add calibrated frontlit profile fixture
vycdev Jul 6, 2026
0b76801
update gitingore
vycdev Jul 6, 2026
e37b7d1
Fix auto-paint calibration cache state
vycdev Jul 6, 2026
a33ebab
Fix next-best-color channel HD blending
vycdev Jul 7, 2026
8e94c29
Merge pull request #56 from Bjohnson131/FEAT-#35
vycdev Jul 7, 2026
bac5661
Fix code-review findings across optimizer, calibration, and profiles
vycdev Jul 7, 2026
2a11673
Fix deterministic optimizer tie breaks
vycdev Jul 7, 2026
d69694c
Merge pull request #41 from vycdev/autopaint-improvements
vycdev Jul 8, 2026
185305b
Update dependencies to resolve Dependabot security alerts
vycdev Jul 9, 2026
67a39e6
Add collapsible control groups
vycdev Jul 9, 2026
2671f0f
feat: 3D inspection view modes - The 3D preview toolbar now offers Sh…
vycdev Jul 10, 2026
cf3a26d
feat: auto-paint preview color toggle - The 3D preview toolbar now of…
vycdev Jul 10, 2026
8b1a566
feat: 2D touch-up tools - The 2D preview toolbar now includes five ha…
vycdev Jul 11, 2026
1dd7447
feat: add calibration theory documentation and related diagrams for i…
Bjohnson131 Jul 12, 2026
7bdcc16
Update DocsPage.tsx
Bjohnson131 Jul 12, 2026
650098a
Merge pull request #57 from Bjohnson131/calibration-docs
vycdev Jul 12, 2026
1de89d7
fix: smooth meshing 3MF integrity - Smoothed cap faces are now valida…
vycdev Jul 12, 2026
1323495
Merge branch 'develop' of https://github.com/vycdev/Kromacut into dev…
vycdev Jul 12, 2026
4819fcb
feat: add job-specific palette proof export
vycdev Jul 17, 2026
ec8465d
feat: persist palette proof results
vycdev Jul 17, 2026
ed59988
feat: configure palette proof size
vycdev Jul 18, 2026
76ec7a7
fix: align calibration dialog close button
vycdev Jul 18, 2026
3e3d8e3
feat: delete incomplete palette proofs
vycdev Jul 18, 2026
1fbcaaa
feat: iterate and reinforce palette proofs
vycdev Jul 18, 2026
23bdf77
fix: align palette proof orientation
vycdev Jul 18, 2026
341a4bd
feat: add touching palette proof experiment
vycdev Jul 18, 2026
18b4902
fix: standardize touching palette proofs
vycdev Jul 18, 2026
d5b55cf
fix: allow saved palette proof downloads
vycdev Jul 18, 2026
7595f27
feat: close the palette proof appearance loop
vycdev Jul 18, 2026
d2952be
feat: split palette proof iteration modes
vycdev Jul 18, 2026
1d06ada
fix: advance automatic palette proofs
vycdev Jul 19, 2026
5110d15
fix: tighten palette proof fit gates
vycdev Jul 19, 2026
6a70d1f
feat: group palette proof rounds
vycdev Jul 19, 2026
5bbd9f8
feat: allow partial calibration entry
Bjohnson131 Jul 21, 2026
b576f47
feat: add landing page and app route
vycdev2 Jul 21, 2026
d0db1d5
fix: polish landing routes and accessibility
vycdev2 Jul 22, 2026
e0ad9a1
fix: harden landing accessibility and static docs
vycdev2 Jul 22, 2026
d699e4b
fix: refine landing and settings navigation
vycdev Jul 22, 2026
4ccb608
fix: refine responsive landing layout
vycdev Jul 22, 2026
d078846
fix: polish landing themes and docs navigation
vycdev Jul 22, 2026
f7c6162
Merge pull request #63 from vycdev2/feat/issue-62-landing-app
vycdev Jul 22, 2026
7b42c96
feat: enhance filament calibration input handling for merge steps
Bjohnson131 Jul 23, 2026
5cbfca5
Merge branch 'develop' into partial-calibration
Bjohnson131 Jul 23, 2026
6723372
Add MultiImageMode and supporting hooks for multi-image feature
Bjohnson131 Jul 16, 2026
f56d9ae
Implement 3D controls and enhance image processing features in App co…
Bjohnson131 Jul 16, 2026
b25e6d7
feat: implement experimental multi-plate mode toggle and related func…
Bjohnson131 Jul 18, 2026
3fc9f7c
add CHANGELOG.md entry.
Bjohnson131 Jul 23, 2026
3a3fe9d
Merge pull request #59 from Bjohnson131/FEAT-#35
vycdev Jul 23, 2026
f730278
Merge pull request #60 from Bjohnson131/partial-calibration
vycdev Jul 23, 2026
efc9f25
fix(profiles): strip UTF-8 BOM before parsing HueForge CSV/TSV imports
myUdav4iik Jul 23, 2026
b88920e
docs: note BOM-prefixed HueForge imports
vycdev Jul 25, 2026
1b2b6f2
Merge pull request #66 from myUdav4iik/fix/hueforge-csv-bom
vycdev Jul 25, 2026
18d4c2c
Merge develop and resolve integration conflicts
vycdev Jul 25, 2026
341dc4a
fix: harden palette proof feedback
vycdev Jul 25, 2026
76909d7
feat: allow prioritizing palette proof targets
vycdev Jul 26, 2026
de5be8b
feat: select palette proof targets from image
vycdev Jul 26, 2026
7d0c543
fix: lock downloaded palette proof settings
vycdev Jul 26, 2026
a734f51
feat: choose fitted palette proof targets
vycdev Jul 26, 2026
cba1d4c
fix: refine palette proof continuations locally
vycdev Jul 26, 2026
000b971
fix: continue remaining palette proof targets
vycdev Jul 26, 2026
6788bd4
feat: record palette proof match quality
vycdev Jul 26, 2026
eaf924f
refine palette proof result controls
vycdev Jul 26, 2026
36c6a59
widen palette proof results layout
vycdev Jul 26, 2026
7c9508a
broaden palette proof continuation search
vycdev Jul 27, 2026
96ef33f
Honor dead-on palette proof anchors
vycdev Aug 2, 2026
d088df2
Add carrier-free flat paint mode
vycdev Aug 2, 2026
730d376
Add advanced palette management (issue #26)
RMantis Aug 5, 2026
1c8a6d8
Keep local sandbox script out of the branch
RMantis Aug 5, 2026
d8113b8
Add optional per-color names to custom palettes
RMantis Aug 5, 2026
efc1b99
Complete Bambu PLA Basic palette from official hex chart
RMantis Aug 5, 2026
f164666
Add supplier data disclaimers in docs and template-load notice
RMantis Aug 5, 2026
e8afd12
Harden reserved-id and import edge cases from review
RMantis Aug 5, 2026
3841776
Sync color count when the selected palette changes shape
RMantis Aug 5, 2026
9bdb0be
Show a persistent notice while a filament template is active
RMantis Aug 5, 2026
4e846e3
Show trademark non-affiliation caption in supplier dropdowns
RMantis Aug 5, 2026
83e48af
Prefill Save New Profile with a "(copy)" name from the active profile
RMantis Aug 5, 2026
c5310d1
Address second-pass review: id migration, docs sources, a11y, coverage
RMantis Aug 5, 2026
19c9220
Align supplier disclaimer wording with the maintainer's request
RMantis Aug 5, 2026
042e3fc
Fixes Star History
Mubelotix Aug 6, 2026
f1dbe07
Drop same-release changelog fixes; e2e spec now verified green
RMantis Aug 6, 2026
ae0bee4
Merge pull request #70 from Mubelotix/patch-1
vycdev Aug 7, 2026
0690b6d
Constrain supplier dropdown widths
vycdev Aug 7, 2026
e449d41
Fix palette test storage isolation
vycdev Aug 7, 2026
6cf5e0f
Preserve filament brands in profile identity
vycdev Aug 7, 2026
caf2e8b
Merge pull request #71 from RMantis/feat/issue-26-palette-management
vycdev Aug 7, 2026
d223702
Merge remote-tracking branch 'origin/develop' into codex/palette-proo…
vycdev Aug 7, 2026
0ba53a3
feat: add stack matrix calibration
vycdev Aug 8, 2026
932ae59
feat: refine stack matrix photo alignment
vycdev Aug 8, 2026
5f15ad3
feat: polish stack matrix photo workflow
vycdev Aug 8, 2026
234f5b3
Merge pull request #67 from vycdev/codex/palette-proof-feedback
vycdev Aug 8, 2026
bf39fde
fix: address post-merge calibration review
vycdev Aug 8, 2026
d0e22e2
fix: constrain palette proof history menu
vycdev Aug 8, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ test-results/
src-tauri/target/
src-tauri/gen/
src-tauri/WixTools/

# Agent workspace files
.agents/
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ geometry, slicer behavior, and print instructions at the same time.

## Domain Notes

- **Transmission Distance (TD):** Used by auto-paint to model light through thin filament layers
with Beer-Lambert-style optical simulation. TD is not just a display value; changing how it is
stored or rounded can change generated layer stacks.
- **Hiding Distance (HD):** Used by auto-paint to model light through thin filament layers with
Beer-Lambert-style optical simulation. `filament.td` stores the frontlit hiding distance in mm
(introduced in profile schema v2; v3 adds appearance evidence); the field keeps its historical
`td` name for data compatibility. Conventional
backlit/lithophane Transmission Distance values are an *input format* only — converted ×0.1
(`FRONTLIT_TD_SCALE`) at entry or during the one-time v1→v2 migration, never stored. HD is not
just a display value; changing how it is stored or rounded can change generated layer stacks.
- **Auto-paint layers:** Auto-paint chooses physical filament stacks for target image colors. The
worker path exists so optimizer choices such as exhaustive, simulated annealing, and genetic
search do not freeze the UI.
Expand Down
64 changes: 63 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
112 changes: 63 additions & 49 deletions README.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@
<meta name="twitter:image" content="https://kromacut.com/android-chrome-512x512.png" />
<meta name="twitter:image:alt" content="Kromacut logo" />
<title>Kromacut - Free Image-to-3D Color Layer Print Generator</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://kromacut.com/#website",
"url": "https://kromacut.com/",
"name": "Kromacut",
"description": "Turn 2D images into stacked, color-layered 3D prints.",
"potentialAction": {
"@type": "CreateAction",
"target": "https://kromacut.com/app"
}
},
{
"@type": "SoftwareApplication",
"@id": "https://kromacut.com/#application",
"name": "Kromacut",
"applicationCategory": "DesignApplication",
"operatingSystem": "Web, Windows, macOS, Linux",
"url": "https://kromacut.com/",
"description": "Browser-first image-to-3D color-layer print generator with STL and 3MF export.",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}
]
}
</script>
</head>
<body>
<div id="root"></div>
Expand Down
877 changes: 544 additions & 333 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "kromacut",
"private": true,
"version": "3.1.0",
"version": "3.2.0",
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"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 All @@ -27,6 +29,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-collapsible": "^1.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
Expand Down
3 changes: 2 additions & 1 deletion public/site.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "Kromacut",
"short_name": "Kromacut",
"description": "Turn 2D images into color-layered 3D prints for free and export STL or 3MF models.",
"start_url": "/",
"start_url": "/app",
"id": "/",
"scope": "/",
"icons": [
{
Expand Down
4 changes: 2 additions & 2 deletions public/version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "3.1.0",
"version": "3.2.0",
"download_url": "https://github.com/vycdev/Kromacut/releases/latest",
"release_notes": "Kromacut 3.1.0 adds orthographic preview mode, experimental Flat Paint, desktop update settings, and next-best-color filament suggestions."
"release_notes": "Upcoming Kromacut 3.2.0 release with new features and improvements."
}
65 changes: 65 additions & 0 deletions scripts/generate-auto-paint-goldens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 }
);

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}`);
114 changes: 109 additions & 5 deletions scripts/generate-docs-seo-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ function resolveDocImage(src) {
if (clean.includes('kromacut-logo.png')) {
return findBuiltAsset('logo-') ?? clean;
}
const diagramPrefixes = {
'06_frontlit_hiding_distance.svg': '06_frontlit_hiding_distance-',
'07_calibration_wedge.svg': '07_calibration_wedge-',
'08_opacity_solve.svg': '08_opacity_solve-',
'09_palette_proof.svg': '09_palette_proof-',
};
const diagramName = Object.keys(diagramPrefixes).find((name) => clean.includes(name));
if (diagramName) {
return findBuiltAsset(diagramPrefixes[diagramName]) ?? clean;
}
return clean;
}

Expand All @@ -136,29 +146,36 @@ function resolveDocHref(href, currentDocSlug, docsBySlug) {

function renderInline(markdown, currentDocSlug, docsBySlug) {
let html = escapeHtml(markdown);
const protectedHtml = [];
const protect = (value) => {
const token = `\u0000${protectedHtml.length}\u0000`;
protectedHtml.push(value);
return token;
};

html = html.replace(/`([^`]+)`/g, (_match, code) => protect(`<code>${code}</code>`));

html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, rawSrc) => {
const [srcPart, titlePart] = rawSrc.trim().split(/\s+["']/);
const title = titlePart ? titlePart.replace(/["']$/, '') : '';
const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
return `<img src="${escapeHtml(resolveDocImage(srcPart))}" alt="${escapeHtml(alt)}"${titleAttr} loading="lazy">`;
return protect(`<img src="${escapeHtml(resolveDocImage(srcPart))}" alt="${escapeHtml(alt)}"${titleAttr} loading="lazy">`);
});

html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
const resolved = resolveDocHref(href, currentDocSlug, docsBySlug);
const externalAttrs = /^(https?:|mailto:)/i.test(resolved)
? ' target="_blank" rel="noopener noreferrer"'
: '';
return `<a href="${escapeHtml(resolved)}"${externalAttrs}>${renderInline(label, currentDocSlug, docsBySlug)}</a>`;
return protect(`<a href="${escapeHtml(resolved)}"${externalAttrs}>${renderInline(label, currentDocSlug, docsBySlug)}</a>`);
});

html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');

return html;
return html.replace(/\u0000(\d+)\u0000/g, (_match, index) => protectedHtml[Number(index)]);
}

function isBlockStart(line) {
Expand Down Expand Up @@ -325,6 +342,28 @@ function updateDocHead(template, doc) {
return html;
}

function updateAppHead(template) {
const title = 'Kromacut App - Image to 3D Print Tool';
const description =
'Create color-layered 3D prints from images with Kromacut. The browser tool runs locally and exports STL or 3MF models.';
const url = `${siteUrl}/app`;
let html = template.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(title)}</title>`);

html = updateMeta(html, 'name', 'description', description);
html = updateMeta(html, 'name', 'robots', 'noindex,nofollow');
html = html.replace(
/<link\s+rel="canonical"\s+href="[^"]*"\s*\/?>/,
`<link rel="canonical" href="${url}" />`
);
html = updateMeta(html, 'property', 'og:title', title);
html = updateMeta(html, 'property', 'og:description', description);
html = updateMeta(html, 'property', 'og:url', url);
html = updateMeta(html, 'name', 'twitter:title', title);
html = updateMeta(html, 'name', 'twitter:description', description);

return html;
}

function renderStaticRoot(doc, docs, docsBySlug) {
const nav = docs
.map((entry) => `<li><a href="/docs/${entry.slug}">${escapeHtml(entry.title)}</a></li>`)
Expand All @@ -334,7 +373,8 @@ function renderStaticRoot(doc, docs, docsBySlug) {
return `<div id="root">
<main class="seo-doc-page">
<nav aria-label="Documentation">
<a href="/">Kromacut app</a>
<a href="/">Kromacut home</a>
<a href="/app">Open Kromacut</a>
<ul>${nav}</ul>
</nav>
<article>
Expand Down Expand Up @@ -362,6 +402,12 @@ function writeDocPage(template, doc, docs, docsBySlug, slug = doc.slug) {
}
}

function writeAppPage(template) {
const outputDir = path.join(distDir, 'app');
mkdirSync(outputDir, { recursive: true });
writeFileSync(path.join(outputDir, 'index.html'), updateAppHead(template));
}

function writeSitemap(docs) {
const urls = ['/', ...docs.map((doc) => `/docs/${doc.slug}`)];
const body = urls
Expand All @@ -380,6 +426,62 @@ function writeRobots() {
);
}

function verifyGeneratedOutput(docs) {
const requiredFiles = [
'index.html',
'app/index.html',
'docs/index.html',
'robots.txt',
'sitemap.xml',
'site.webmanifest',
'version.json',
];
const assertGenerated = (condition, message) => {
if (!condition) throw new Error(`Generated output verification failed: ${message}`);
};

requiredFiles.forEach((file) => {
assertGenerated(existsSync(path.join(distDir, file)), `missing dist/${file}`);
});

const rootHtml = readFileSync(path.join(distDir, 'index.html'), 'utf8');
const appHtml = readFileSync(path.join(distDir, 'app', 'index.html'), 'utf8');
const sitemap = readFileSync(path.join(distDir, 'sitemap.xml'), 'utf8');
const robots = readFileSync(path.join(distDir, 'robots.txt'), 'utf8');
const manifest = JSON.parse(readFileSync(path.join(distDir, 'site.webmanifest'), 'utf8'));
const version = JSON.parse(readFileSync(path.join(distDir, 'version.json'), 'utf8'));

assertGenerated(rootHtml.includes('<link rel="canonical" href="https://kromacut.com/"'), 'root canonical URL is missing');
assertGenerated(/(?:src|href)="\/assets\//.test(rootHtml), 'root does not use root-relative built assets');
assertGenerated(appHtml.includes('<meta name="robots" content="noindex,nofollow"'), '/app noindex metadata is missing');
assertGenerated(appHtml.includes('<link rel="canonical" href="https://kromacut.com/app"'), '/app canonical URL is missing');
assertGenerated(sitemap.includes('<loc>https://kromacut.com/</loc>'), 'root is missing from sitemap');
assertGenerated(!sitemap.includes('https://kromacut.com/app'), '/app must not appear in sitemap');
assertGenerated(robots.includes('Sitemap: https://kromacut.com/sitemap.xml'), 'robots.txt sitemap reference is missing');
assertGenerated(manifest.start_url === '/app', 'manifest start_url must be /app');
assertGenerated(manifest.id === '/', 'manifest id must remain stable at /');
assertGenerated(typeof version.version === 'string' && version.version.length > 0, 'version.json is invalid');

docs.forEach((doc) => {
const relativePage = path.join('docs', doc.slug, 'index.html');
const pagePath = path.join(distDir, relativePage);
assertGenerated(existsSync(pagePath), `missing dist/${relativePage}`);
const html = readFileSync(pagePath, 'utf8');
assertGenerated(
html.includes(`<link rel="canonical" href="${siteUrl}/docs/${doc.slug}"`),
`canonical URL is missing for ${doc.slug}`
);

for (const match of html.matchAll(/<img\s+[^>]*src="([^"]+)"/g)) {
const src = match[1];
if (/^(https?:|data:)/i.test(src)) continue;
assertGenerated(!src.includes('<') && !src.includes('>'), `malformed image URL in ${doc.slug}: ${src}`);
assertGenerated(src.startsWith('/'), `image URL is not root-relative in ${doc.slug}: ${src}`);
assertGenerated(existsSync(path.join(distDir, src.slice(1))), `missing image used by ${doc.slug}: ${src}`);
}
});
}

if (!existsSync(distIndexPath)) {
throw new Error('dist/index.html was not found. Run this script after vite build.');
}
Expand All @@ -391,5 +493,7 @@ const overviewDoc = docsBySlug.get('overview') ?? docs[0];

docs.forEach((doc) => writeDocPage(template, doc, docs, docsBySlug));
if (overviewDoc) writeDocPage(template, overviewDoc, docs, docsBySlug, '');
writeAppPage(template);
writeSitemap(docs);
writeRobots();
verifyGeneratedOutput(docs);
Loading