Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ All notable changes to Kromacut are documented in this file.
- **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

Expand Down
2 changes: 1 addition & 1 deletion src/components/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ function ShowcaseCard({ items }: { items: ShowcaseItem[] }) {
i18nKey="landing.originalArtwork"
values={{ title: artwork.label }}
components={{
source: (
credit: (
<a
href={artwork.href}
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/bn/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}}টি ছবি",
"photos_other": "{{count}}টি ছবি",
"openPhoto": "{{title}} — {{kind}} {{index}} পূর্ণ আকারে খুলুন",
"originalArtwork": "মূল শিল্পকর্ম: <source>{{title}}</source>",
"originalArtwork": "মূল শিল্পকর্ম: <credit>{{title}}</credit>",
"themeLabel": "থিম: {{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/de/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} Foto",
"photos_other": "{{count}} Fotos",
"openPhoto": "{{title}} – {{kind}} {{index}} in voller Größe öffnen",
"originalArtwork": "Originalmotiv: <source>{{title}}</source>",
"originalArtwork": "Originalmotiv: <credit>{{title}}</credit>",
"themeLabel": "Design: {{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} photo",
"photos_other": "{{count}} photos",
"openPhoto": "Open {{title}} — {{kind}} {{index}} at full size",
"originalArtwork": "Original artwork: <source>{{title}}</source>",
"originalArtwork": "Original artwork: <credit>{{title}}</credit>",
"themeLabel": "Theme: {{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/es/public.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/fr/public.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/hi/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} फ़ोटो",
"photos_other": "{{count}} फ़ोटो",
"openPhoto": "{{title}} — {{kind}} {{index}} पूरे आकार में खोलें",
"originalArtwork": "मूल कलाकृति: <source>{{title}}</source>",
"originalArtwork": "मूल कलाकृति: <credit>{{title}}</credit>",
"themeLabel": "थीम: {{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/it/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} foto",
"photos_other": "{{count}} foto",
"openPhoto": "Apri {{title}} — {{kind}} {{index}} a dimensione intera",
"originalArtwork": "Opera originale: <source>{{title}}</source>",
"originalArtwork": "Opera originale: <credit>{{title}}</credit>",
"themeLabel": "Tema: {{theme}}",
"photos_many": "{{count}} foto"
},
Expand Down
2 changes: 1 addition & 1 deletion src/locales/ja/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "写真{{count}}枚",
"photos_other": "写真{{count}}枚",
"openPhoto": "{{title}} — {{kind}} {{index}}を原寸で開く",
"originalArtwork": "元作品:<source>{{title}}</source>",
"originalArtwork": "元作品:<credit>{{title}}</credit>",
"themeLabel": "テーマ:{{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/pt-PT/public.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/ro/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} fotografie",
"photos_other": "{{count}} de fotografii",
"openPhoto": "Deschide {{title}} — {{kind}} {{index}} la dimensiune completă",
"originalArtwork": "Ilustrație originală: <source>{{title}}</source>",
"originalArtwork": "Ilustrație originală: <credit>{{title}}</credit>",
"themeLabel": "Temă: {{theme}}",
"photos_few": "{{count}} fotografii"
},
Expand Down
2 changes: 1 addition & 1 deletion src/locales/uk/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}} фото",
"photos_other": "{{count}} фото",
"openPhoto": "Відкрити {{title}} — {{kind}} {{index}} у повному розмірі",
"originalArtwork": "Оригінальна робота: <source>{{title}}</source>",
"originalArtwork": "Оригінальна робота: <credit>{{title}}</credit>",
"themeLabel": "Тема: {{theme}}",
"photos_few": "{{count}} фото",
"photos_many": "{{count}} фото"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/zh-CN/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"photos_one": "{{count}}张照片",
"photos_other": "{{count}}张照片",
"openPhoto": "全尺寸打开{{title}} — {{kind}} {{index}}",
"originalArtwork": "原作:<source>{{title}}</source>",
"originalArtwork": "原作:<credit>{{title}}</credit>",
"themeLabel": "主题:{{theme}}"
},
"navigation": {
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/landing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test.describe('landing page smoke @smoke', () => {
await batmanga.screenshot({ path: testInfo.outputPath(`batmanga-${width}.png`) });
const naruto = cards.filter({ has: page.getByRole('heading', { name: 'Naruto', exact: true }) });
await expect(naruto).toHaveCount(1);
await expect(naruto.locator('footer').getByRole('link', { name: 'Naruto artwork — source on Pinterest' })).toHaveAttribute('href', 'https://in.pinterest.com/pin/169870217190172931/');
await expect(naruto.locator('footer').getByRole('link', { name: 'Naruto', exact: true })).toHaveAttribute('href', 'https://in.pinterest.com/pin/169870217190172931/');
await expect(naruto.locator('header').getByText('110.4 × 190.2 mm footprint')).toHaveCount(1);
expect(await naruto.locator('figcaption > p:first-child').allTextContents()).toEqual(['Kromacut prediction', 'Slicer preview', 'Finished print']);
expect(await naruto.locator('img').evaluateAll(images => images.map(image => image.getAttribute('alt')))).toEqual([
Expand Down
7 changes: 5 additions & 2 deletions tests/e2e/sticky-mobile-cta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ test.describe('sticky mobile landing CTA @smoke', () => {

await scrollToFooter(page);
await expect(sticky).toBeVisible();
await page.getByRole('navigation', { name: 'Footer navigation' })
.getByRole('link', { name: 'Terms', exact: true }).focus();
const footerNavigation = page.getByRole('navigation', { name: 'Footer navigation' });
await footerNavigation.getByRole('link', { name: 'Terms', exact: true }).focus();
// The footer language selector is the last control before the sticky action.
await page.keyboard.press('Tab');
await expect(footerNavigation.getByTestId('app-language')).toBeFocused();
await page.keyboard.press('Tab');
const stickyAction = sticky.getByRole('link', { name: 'Open Kromacut', exact: true });
await expect(stickyAction).toBeFocused();
Expand Down
16 changes: 15 additions & 1 deletion tests/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readdirSync, readFileSync } from 'node:fs';
import {
matchLanguage,
resolveLanguage,
Expand Down Expand Up @@ -237,3 +237,17 @@ test('the current release feed provides real notes for every supported language'
if (code !== 'en') assert.notEqual(localized?.text, feed.release_notes);
}
});

test('translation component tags avoid void HTML element names', () => {
// Trans parses <source>, <link>, etc. as void elements, so the wrapped text
// renders after the component and a translated link becomes empty.
const pairedVoidTag =
/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)>[^"]*?<\/\1>/;
for (const { code } of SUPPORTED_LANGUAGES) {
for (const file of readdirSync(`src/locales/${code}`)) {
if (!file.endsWith('.json')) continue;
const resource = readFileSync(`src/locales/${code}/${file}`, 'utf8');
assert.doesNotMatch(resource, pairedVoidTag, `${code}/${file}`);
}
}
});