Skip to content

Commit fc4afdb

Browse files
authored
Release v2.6.0
Date v2.6.0 changelog
2 parents bb40b89 + f46880d commit fc4afdb

5 files changed

Lines changed: 51 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
All notable changes to Kromacut are documented in this file.
44

5-
## v2.6.0 - unreleased
5+
## v2.6.0 - 2026-05-17
66

77
### Added
88

@@ -39,6 +39,7 @@ All notable changes to Kromacut are documented in this file.
3939
- **Print setting decimal inputs** - Pixel size, layer height, and first-layer height fields now allow partial decimal edits like `0` and `0.` before committing or clamping to valid print settings
4040
- **Progress step feedback** - Long-running overlays now show separate overall and current-step progress bars across quantization, dedither passes, mesh generation, STL export, and 3MF export; exports also give the browser a frame to render the overlay before heavy work starts and keep very fast exports visible briefly
4141
- **Compact STL topology** - Fused heightfield STL exports now triangulate conforming surface boundaries and repair diagonal corner contacts, preventing non-manifold edges in large 4-color image stacks
42+
- **Layer preview exports** - STL and 3MF exports now include every generated physical layer regardless of the 3D preview trim range, preserving complete models and correct filament color mapping
4243

4344
## v2.5.0 - 2026-05-03
4445

‎src/lib/export3mf.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ export async function exportObjectTo3MFBlob(
6464
</Relationships>`;
6565
zip.folder('_rels')?.file('.rels', rels);
6666

67-
// Collect visible meshes. Each preview layer should already be one manifold mesh,
68-
// so each visible mesh maps directly to one 3MF object.
67+
// Collect generated meshes. Preview range controls may hide layers in the scene,
68+
// but exports must still include every generated physical layer.
6969
const meshes: THREE.Mesh[] = [];
7070
root.updateMatrixWorld(true);
7171
root.traverse((obj) => {
7272
if ((obj as THREE.Mesh).isMesh) {
7373
const m = obj as THREE.Mesh;
74-
if (m.geometry && m.visible) {
74+
if (m.geometry) {
7575
meshes.push(m);
7676
}
7777
}

‎src/lib/exportStl.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,13 +1095,14 @@ export async function exportObjectToStlBlob(
10951095
root: THREE.Object3D,
10961096
onProgress?: (p: number) => void
10971097
): Promise<Blob> {
1098-
// 1. Collect all meshes
1098+
// 1. Collect all generated meshes. Preview range controls may hide layers in the scene,
1099+
// but exports must still include every generated physical layer.
10991100
const meshes: THREE.Mesh[] = [];
11001101
root.updateMatrixWorld(true);
11011102
root.traverse((obj) => {
11021103
if ((obj as THREE.Mesh).isMesh) {
11031104
const m = obj as THREE.Mesh;
1104-
if (m.geometry && m.visible) {
1105+
if (m.geometry) {
11051106
meshes.push(m);
11061107
}
11071108
}

‎tests/e2e/kromacut-flow.spec.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ async function exportModel(
454454

455455
const [download] = await Promise.all([
456456
page.waitForEvent('download', { timeout }),
457-
page.getByTestId(buttonTestId).click(),
457+
page.getByTestId(buttonTestId).click({ noWaitAfter: true }),
458458
]);
459459

460460
const downloadPath = await download.path();
@@ -513,7 +513,16 @@ async function openDownloadMenu(page: Page, expectedItemTestId: string) {
513513
await expect(trigger).toBeEnabled();
514514

515515
for (let attempt = 0; attempt < 3; attempt++) {
516-
await trigger.click({ force: attempt > 0 });
516+
await trigger.evaluate((element: HTMLElement) => {
517+
element.scrollIntoView({ block: 'center', inline: 'center' });
518+
});
519+
try {
520+
await trigger.click({ force: attempt > 0, noWaitAfter: true, timeout: 10_000 });
521+
} catch {
522+
// Large stress scenes can keep Chromium busy enough that Playwright's
523+
// auto-scroll click times out after the trigger is already visible.
524+
await trigger.evaluate((element: HTMLElement) => element.click());
525+
}
517526

518527
if (await expectedItem.isVisible({ timeout: 2000 }).catch(() => false)) {
519528
return;

‎tests/export3mf.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,7 @@ function buildProfileImageLayerSpecs(
13821382
}));
13831383
}
13841384

1385-
test('3MF export keeps visible meshes as separate layer objects', async () => {
1385+
test('3MF export keeps generated meshes as separate layer objects', async () => {
13861386
const root = new THREE.Group();
13871387
root.add(createLayerMesh(createSharedCubeGeometry().toNonIndexed(), 0xff0000));
13881388
root.add(createLayerMesh(createSharedCubeGeometry().toNonIndexed(), 0x00ff00));
@@ -1395,6 +1395,37 @@ test('3MF export keeps visible meshes as separate layer objects', async () => {
13951395
);
13961396
});
13971397

1398+
test('exports include preview-hidden layers with their original filament colors', async () => {
1399+
const root = new THREE.Group();
1400+
const first = createLayerMesh(createSharedCubeGeometry(), 0xff0000);
1401+
const hiddenMiddle = createLayerMesh(createSharedCubeGeometry(), 0x00ff00);
1402+
const last = createLayerMesh(createSharedCubeGeometry(), 0x0000ff);
1403+
const filamentColors = ['#111111', '#222222', '#333333'];
1404+
1405+
hiddenMiddle.visible = false;
1406+
root.add(first, hiddenMiddle, last);
1407+
1408+
const archive = await exportArchiveXml(root, {
1409+
layerFilamentColors: filamentColors,
1410+
});
1411+
const objects = parseMeshObjects(archive.modelXml);
1412+
1413+
assert.equal(objects.length, 3, '3MF should include preview-hidden generated layers');
1414+
assert.deepEqual(parseBaseMaterialColors(archive.modelXml), ['111111', '222222', '333333']);
1415+
assert.deepEqual(
1416+
objects.map((object) => object.materialIndex),
1417+
[0, 1, 2],
1418+
'3MF layer colors should keep original layer indices when some layers are hidden'
1419+
);
1420+
1421+
const exportedStl = await parseSingleBinaryStlMesh(await exportObjectToStlBlob(root));
1422+
assert.equal(
1423+
exportedStl.triangleCount,
1424+
36,
1425+
'STL should include all generated cube layers, including the hidden preview layer'
1426+
);
1427+
});
1428+
13981429
test('3MF export progress stays monotonic for large indexed meshes', async () => {
13991430
const { exportObjectTo3MFBlob } = await loadExport3mfModule();
14001431
const root = new THREE.Group();

0 commit comments

Comments
 (0)