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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
dist
node_modules
printventory.db
printventory.db-shm
printventory.db-wal
package-lock.json
test-files.zip
git_credentials
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes contributed via pull request are documented in this file.

## [Unreleased]

### Added

- **Folder and ZIP bundle grouping** — When scanning, models that share the same parent folder or the same ZIP archive (2+ files) are grouped into a single row in List, Preview, and Detailed views. Single-file folders stay as individual entries.
- **Bundle 3D preview** — Click a folder or ZIP bundle to open one preview dialog showing every STL/3MF part laid out on a grid, with per-part colors for clarity. Up to 32 previewable parts per bundle.
- **Bundle details panel** — Double-click a bundle (or use **Open 3D preview** from the panel) to see path, combined size, print status, and a sortable file list. Chevron still expands/collapses the bundle in the grid.
- **Send to Slicer in preview** — The 3D preview dialog includes a **Send to Slicer** button. Works for single models and full bundles (all STL/3MF paths). If multiple slicers are configured, a picker is shown.
- **New slicer instance on send** — macOS launches slicers with `open -n` so a new window opens even when the slicer is already running. Prusa-family binaries also receive `--single-instance=0` when launched directly.
- **`bundle-keys.js`** — Shared logic to derive `bundleKey`, `bundleLabel`, and `bundleKind` from file paths (including `zipPath::entry` paths).
- **`npm run test:bundle`** — Unit tests for bundle key derivation.

### Changed

- Scan insert/update and `saveModel` persist bundle metadata (`bundleKey`, `bundleLabel`, `bundleKind`) with automatic migration on startup.
- Context menu **Open in Slicer** and preview **Send to Slicer** share the same launch helper (`buildSlicerLaunchCommand` / `open-file-in-slicer` IPC).
- `window.openSlicerSettings` is exposed from `slicer.js` for use from the preview flow.

### Database

- New optional columns on `models`: `bundleKey`, `bundleLabel`, `bundleKind` (backfilled on existing databases).
26 changes: 23 additions & 3 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Printventory is a desktop application for managing your 3D printing model collec
- Track print status
- Add source URLs and notes
- Link related models (parent/child relationships)
- **Folder and ZIP bundles**: Models in the same subfolder or ZIP archive (2+ files) appear as one grouped row; click to preview all parts in 3D, double-click for bundle details
- Assign licenses to models

### Multi-Edit Features
Expand Down Expand Up @@ -162,8 +163,26 @@ Printventory offers comprehensive settings to customize your experience:
- Rendering performance optimization

### Slicer Path
- Configure path to your slicer application
- Used for opening models in external slicers
- Configure one or more slicer applications (name + path)
- **Open in Slicer** from the right-click context menu
- **Send to Slicer** from the 3D preview dialog (single model or full bundle)
- On macOS, each send opens a **new slicer instance** so models load even when the slicer is already open

### Bundle groups (folders and ZIP archives)

When a scan finds **two or more** STL/3MF files in the same folder or inside the same ZIP file, Printventory shows them as one **bundle** row instead of many separate entries.

| Action | Result |
|--------|--------|
| Click bundle row | Opens **3D preview** with every part laid out on a grid |
| Double-click bundle row | Opens **Bundle details** (path, sizes, print status, file list) |
| Chevron (▸ / ▾) | Expand or collapse individual files in the grid |
| **Send to Slicer** (in preview) | Sends all bundle STL/3MF files to your chosen slicer |

**Notes:**
- Single-file folders are not grouped (they stay normal model rows).
- Bundle preview supports up to 32 STL/3MF parts per open; larger bundles show the first 32 with a notice.
- ZIP entries are extracted to a temp file before sending to the slicer, same as the context menu.

### STL Home
- Set default directory for file operations
Expand All @@ -181,6 +200,7 @@ Printventory offers comprehensive settings to customize your experience:
- Enable multi-edit mode for batch operations
- Use the Tag Manager to organize and clean up tags across your collection
- Link related models using parent/child relationships
- Look for **folder** and **ZIP** bundle rows when a multi-part project was scanned together; click to preview all parts at once

### AI Tagging
- Start with a small batch to test your AI configuration
Expand All @@ -189,7 +209,7 @@ Printventory offers comprehensive settings to customize your experience:
- Review AI-suggested tags before accepting to ensure accuracy

### View Modes
- Use List view for quick scanning of large collections
- Use List view for quick scanning of large collections; bundle groups appear as one row with a part count
- Use Detailed view when reviewing models for printing
- Preview view offers a good balance between information and space

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Printventory is an Electron-based desktop application for managing your 3D print
- **Source URLs**: Store links to where you found or purchased models
- **Notes**: Add custom notes to any model
- **Parent/Child Relationships**: Link related models together
- **Folder & ZIP bundle grouping**: Automatically group models from the same subfolder or ZIP archive; preview all parts in one 3D view
- **License Tracking**: Assign licenses to models

### Advanced Features
Expand All @@ -29,6 +30,8 @@ Printventory is an Electron-based desktop application for managing your 3D print
- **Duplicate Detection**: Find duplicate files based on content hash with visual comparison
- **Print Roulette**: Randomly select models from your collection
- **AI Tagging**: Automated tag suggestions using AI
- **3D bundle preview**: Open every STL/3MF in a folder or ZIP in a single preview layout
- **Send to Slicer from preview**: Open the current model or entire bundle in your configured slicer (new instance when already running)
- **Search & Filter**: Real-time search by filename and filter by designer, tags, print status, parent model, or license
- **Tag Manager**: Comprehensive tag management interface
- **Metadata Editor**: Bulk metadata editing capabilities
Expand All @@ -42,6 +45,8 @@ Printventory is an Electron-based desktop application for managing your 3D print

For a complete list of features and detailed usage instructions, see the [GUIDE.md](GUIDE.md) file.

See [CHANGELOG.md](CHANGELOG.md) for recent feature additions and migration notes.

## Installation

### Pre-built Releases
Expand Down
48 changes: 48 additions & 0 deletions bundle-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Derive folder / zip bundle identity from a model filePath.
* Shared by main process (scan, save) and tests.
*/
const path = require('path');

function normalizePath(filepath) {
return String(filepath || '').replace(/\\/g, '/');
}

/**
* @param {string} filePath
* @returns {{ bundleKey: string, bundleLabel: string, bundleKind: string }}
*/
function deriveBundleFromFilePath(filePath) {
const empty = { bundleKey: '', bundleLabel: '', bundleKind: '' };
if (!filePath || typeof filePath !== 'string') return empty;
if (filePath.startsWith('url::')) return empty;

if (filePath.includes('::')) {
const zipPath = filePath.split('::')[0];
const normalized = normalizePath(zipPath);
if (!normalized) return empty;
const label = path.basename(normalized) || normalized;
return {
bundleKey: `zip:${normalized.toLowerCase()}`,
bundleLabel: label,
bundleKind: 'zip',
};
}

const dir = path.dirname(filePath);
const normalizedDir = normalizePath(dir);
if (!normalizedDir || normalizedDir === '.' || normalizedDir === '/') {
return empty;
}
const label = path.basename(normalizedDir) || normalizedDir;
return {
bundleKey: `folder:${normalizedDir.toLowerCase()}`,
bundleLabel: label,
bundleKind: 'folder',
};
}

module.exports = {
deriveBundleFromFilePath,
normalizePath,
};
39 changes: 39 additions & 0 deletions bundle-keys.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node
'use strict';

const assert = require('assert');
const { deriveBundleFromFilePath } = require('./bundle-keys');

function test(name, fn) {
try {
fn();
console.log(`ok ${name}`);
} catch (err) {
console.error(`FAIL ${name}:`, err.message);
process.exitCode = 1;
}
}

test('folder bundle from sibling STLs', () => {
const stem = deriveBundleFromFilePath('/Downloads/flower-model/stem.stl');
const petal = deriveBundleFromFilePath('/Downloads/flower-model/petal.stl');
assert.strictEqual(stem.bundleKind, 'folder');
assert.strictEqual(stem.bundleLabel, 'flower-model');
assert.strictEqual(stem.bundleKey, petal.bundleKey);
});

test('zip bundle from archive entry', () => {
const entry = deriveBundleFromFilePath('C:\\Models\\flower.zip::parts/flower.stl');
assert.strictEqual(entry.bundleKind, 'zip');
assert.strictEqual(entry.bundleLabel, 'flower.zip');
assert.ok(entry.bundleKey.startsWith('zip:'));
});

test('url models have no bundle', () => {
const url = deriveBundleFromFilePath('url::https://example.com/model.stl');
assert.strictEqual(url.bundleKey, '');
});

if (process.exitCode) {
process.exit(process.exitCode);
}
3 changes: 2 additions & 1 deletion guide.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ const guidePages = [
<li><strong>Backup/Restore</strong> – Safeguard your data with easy backup and restore options! 💾</li>
<li><strong>De-Dup</strong> – Say goodbye to clutter! Easily clean up duplicate files in your library, ensuring your collection stays organized and efficient! 🧹</li>
<li><strong>AI Tagging</strong> – Configure your AI services in <strong>Settings > AI Config</strong> to enable powerful AI-assisted tagging! Right-click on one or multiple models to "Generate Tags using AI" and automatically categorize your models. 🤖</li>
<li><strong>Slicer Integration</strong> – Set your preferred slicer path in <strong>Settings</strong> to enable the "Open in Slicer" right-click option. Quickly prepare models for printing with your favorite slicer! 🖨️</li>
<li><strong>Slicer Integration</strong> – Configure slicers in <strong>Settings</strong> for right-click <strong>Open in Slicer</strong> and the preview dialog <strong>Send to Slicer</strong> button. Sending again while a slicer is open starts a new instance with your model loaded. 🖨️</li>
<li><strong>Folder &amp; ZIP bundles</strong> – Multi-part folders and ZIP archives group into one row. Click for an all-parts 3D preview; double-click for bundle details. 📦</li>
</ul>
Thank you for choosing Printventory! Visit <strong>Help > Support Printventory</strong> to learn how you can support this amazing project!`,
image: ""
Expand Down
38 changes: 38 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,40 @@ <h3>Model Details</h3>
</div>
<button id="save-model-button"></button>
</div>

<div id="bundle-details" class="model-details bundle-details hidden">
<h3 id="bundle-details-title">Bundle Details</h3>
<p id="bundle-details-subtitle" class="bundle-details-subtitle"></p>
<div class="form-group">
<label>Container path</label>
<div class="bundle-details-path-row">
<input type="text" id="bundle-details-path" readonly>
<button type="button" id="bundle-details-show-path" class="icon-button" title="Show in folder">↗</button>
</div>
</div>
<div class="bundle-details-stats" id="bundle-details-stats"></div>
<div class="form-group">
<label>Contents</label>
<div class="bundle-contents-table-wrap">
<table class="bundle-contents-table" id="bundle-contents-table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col">Size</th>
<th scope="col">Printed</th>
<th scope="col">Designer</th>
</tr>
</thead>
<tbody id="bundle-contents-body"></tbody>
</table>
</div>
</div>
<div class="bundle-details-actions">
<button type="button" id="bundle-details-preview">Open 3D preview</button>
<button type="button" id="bundle-details-expand-grid">Expand in grid</button>
<button type="button" id="bundle-details-close">Close</button>
</div>
</div>
<dialog id="new-designer-dialog" class="modal">
<form method="dialog">
<h3>Add New Designer</h3>
Expand Down Expand Up @@ -943,6 +977,9 @@ <h3 id="preview-model-name">Model Preview</h3>
</div>
<div class="preview-controls">
<div class="preview-control-group">
<button type="button" id="preview-send-to-slicer" class="preview-control-button preview-slicer-button" title="Open in slicer">
<span>🖨️</span> Send to Slicer
</button>
<button type="button" id="preview-reset-view" class="preview-control-button" title="Reset View">
<span>🔄</span> Reset View
</button>
Expand All @@ -953,6 +990,7 @@ <h3 id="preview-model-name">Model Preview</h3>
<span>📐</span> Axes
</button>
</div>
<div id="preview-slicer-menu" class="preview-slicer-menu hidden" role="menu" aria-label="Choose slicer"></div>
<div class="preview-info">
<span id="preview-file-type"></span>
<span id="preview-dimensions"></span>
Expand Down
Loading
Loading