Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 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
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
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/
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ 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
(profile schema v2); 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
45 changes: 44 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
110 changes: 62 additions & 48 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}`);
113 changes: 108 additions & 5 deletions scripts/generate-docs-seo-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ 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-',
};
const diagramName = Object.keys(diagramPrefixes).find((name) => clean.includes(name));
if (diagramName) {
return findBuiltAsset(diagramPrefixes[diagramName]) ?? clean;
}
return clean;
}

Expand All @@ -136,29 +145,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 +341,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 +372,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 +401,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 +425,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 +492,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