freecad-preview-extractor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/package.json +42 -0
- package/src/extract-png-from-fcstd.js +120 -0
- package/src/ignore-utils.js +97 -0
- package/src/index.js +223 -0
- package/src/isofit.FCMacro +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andrew Kondratev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# FreeCAD preview extractor
|
|
2
|
+
|
|
3
|
+
[](https://github.com/andruhon/freecad-preview-extractor/actions/workflows/unit-tests.yml) [](https://github.com/andruhon/freecad-preview-extractor/actions/workflows/integration-tests.yml)
|
|
4
|
+
|
|
5
|
+
A CLI tool for extracting and generating preview thumbnails from FreeCAD files.
|
|
6
|
+
|
|
7
|
+
Preview is extracted for each `FILENAME.FCStd` file as `FILENAME-preview.png`
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Extract existing previews**: Quickly extract embedded thumbnail images from `.FCStd` files
|
|
12
|
+
- **Generate new previews**: Use FreeCAD to create fresh previews with isometric view and fit-to-view
|
|
13
|
+
- **Batch processing**: Process all FreeCAD files in a directory recursively
|
|
14
|
+
- **Single file mode**: Extract or generate previews for specific files
|
|
15
|
+
- **Ignore patterns**: Use an ignore config file (like `.gitignore`) to exclude specific files from batch processing
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/andruhon/freecad-preview-extractor.git
|
|
20
|
+
cd ./freecad-preview-extractor
|
|
21
|
+
npm ci
|
|
22
|
+
sudo npm install -g ./
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Extract existing previews from all FreeCAD files
|
|
28
|
+
```bash
|
|
29
|
+
fcxtc
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
freecad-preview-extractor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Extract preview from a specific file
|
|
39
|
+
```bash
|
|
40
|
+
fcxtc filename.FCStd
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Generate preview with FreeCAD before extraction (requires FreeCAD and desktop environment)
|
|
44
|
+
|
|
45
|
+
Adding `--fit` argument will trigger `src/isofit.FCMacro` FreeCAD macros for each file,
|
|
46
|
+
the macros sets model to the isometric view, does "fit into view", and saves the file.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
fcxtc --fit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
or for a specific file:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
fcxtc --fit filename.FCStd
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Ignore specific files during batch processing
|
|
59
|
+
```bash
|
|
60
|
+
fcxtc --ignore-config .myignore
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Create an ignore file (e.g., `.myignore`) with patterns to exclude:
|
|
64
|
+
```
|
|
65
|
+
# Ignore all files in the archived directory
|
|
66
|
+
archived/*.FCStd
|
|
67
|
+
|
|
68
|
+
# Ignore specific files
|
|
69
|
+
test-model.FCStd
|
|
70
|
+
|
|
71
|
+
# Ignore files with specific naming pattern
|
|
72
|
+
*backup*.FCStd
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `--fit` option will:
|
|
76
|
+
1. Open each FreeCAD file with FreeCAD
|
|
77
|
+
2. Run the `isofit.FCMacro` macro to set Isometric View and Fit All
|
|
78
|
+
3. Save the file with the updated preview
|
|
79
|
+
4. Extract the preview image as usual
|
|
80
|
+
|
|
81
|
+
The `--ignore-config` option allows you to exclude specific files from batch processing using pattern matching similar to `.gitignore`:
|
|
82
|
+
- Supports wildcards (`*`, `?`)
|
|
83
|
+
- Supports `**` for recursive matching
|
|
84
|
+
- Supports character ranges (`[abc]`)
|
|
85
|
+
- Lines starting with `#` are treated as comments
|
|
86
|
+
- Empty lines are ignored
|
|
87
|
+
|
|
88
|
+
**Note:** The `--fit` option requires:
|
|
89
|
+
- FreeCAD installed and available in your system PATH
|
|
90
|
+
- A desktop environment / X server (cannot run headless)
|
|
91
|
+
- UI access for FreeCAD to render the view
|
|
92
|
+
|
|
93
|
+
**Note:** The `--ignore-config` option only works in batch processing mode (when no specific file is provided). It is ignored in single-file mode.
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
The project has comprehensive test suites covering unit logic and integration scenarios.
|
|
98
|
+
|
|
99
|
+
### Unit Tests
|
|
100
|
+
Run unit tests for core logic (path handling, ignore patterns, etc.):
|
|
101
|
+
```bash
|
|
102
|
+
npm test
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Integration Tests
|
|
106
|
+
Integration tests are split into two categories based on whether they require a local FreeCAD installation.
|
|
107
|
+
|
|
108
|
+
**1. Standard Integration Tests (No FreeCAD required)**
|
|
109
|
+
These tests check file extraction, CLI behavior, and ignore patterns without invoking FreeCAD.
|
|
110
|
+
```bash
|
|
111
|
+
npm run test:integration-no-freecad
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**2. Full Integration Tests (FreeCAD required)**
|
|
115
|
+
These tests involve the `--fit` flag and spawning the FreeCAD process. They require FreeCAD to be installed and available in the system PATH.
|
|
116
|
+
```bash
|
|
117
|
+
npm run test:integration
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Note for Developers:**
|
|
121
|
+
- Tests ending in `.test.js` do not require FreeCAD.
|
|
122
|
+
- Tests ending in `.test-freecad.js` require FreeCAD.
|
|
123
|
+
|
|
124
|
+
## Utilities
|
|
125
|
+
|
|
126
|
+
The project exports utility functions that can be used programmatically:
|
|
127
|
+
|
|
128
|
+
- `loadIgnoreConfig(filePath, cwd)` - Load ignore patterns from a file
|
|
129
|
+
- `filterIgnoredFiles(filePaths, rootDir, patterns, enabled)` - Filter files based on patterns
|
|
130
|
+
- `shouldIgnoreFile(filePath, rootDir, patterns, enabled)` - Check if a single file should be ignored
|
|
131
|
+
|
|
132
|
+
## Project Files
|
|
133
|
+
|
|
134
|
+
Key files of the project (not including all files):
|
|
135
|
+
|
|
136
|
+
- `src/index.js` - Main CLI entry point
|
|
137
|
+
- `src/extract-png-from-fcstd.js` - Thumbnail extraction logic
|
|
138
|
+
- `src/isofit.FCMacro` - FreeCAD macro for isometric view and fit (used by `--fit` option)
|
|
139
|
+
- `tests/` - Unit tests for ignore functionality
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "freecad-preview-extractor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Script extracting freecad previews",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"FreeCAD"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Andrew Kondratev",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "src/index.js",
|
|
12
|
+
"bin": {
|
|
13
|
+
"fcxtc": "src/index.js",
|
|
14
|
+
"freecad-preview-extractor": "src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test --test-concurrency=1 tests/*.js",
|
|
18
|
+
"test:integration": "node --test --test-concurrency=1 integration-tests/*.js",
|
|
19
|
+
"test:integration-no-freecad": "node --test --test-concurrency=1 integration-tests/*.test.js"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/andruhon/freecad-preview-extractor.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/andruhon/freecad-preview-extractor/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/andruhon/freecad-preview-extractor#readme",
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=24.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"glob": "^13.0.0",
|
|
39
|
+
"yauzl": "^3.2.0",
|
|
40
|
+
"minimatch": "^10.1.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yauzl from 'yauzl';
|
|
4
|
+
|
|
5
|
+
// Constants
|
|
6
|
+
const THUMBNAIL_PATH = 'thumbnails/Thumbnail.png';
|
|
7
|
+
|
|
8
|
+
// Helper to promisify yauzl's zip opening
|
|
9
|
+
function openZipPromise(filename) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
yauzl.open(filename, { lazyEntries: true }, (err, zipfile) => {
|
|
12
|
+
if (err) return reject(err);
|
|
13
|
+
resolve(zipfile);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract thumbnail from a FreeCAD file
|
|
20
|
+
* @param {string} zipFilePath - Path to the .FCStd file
|
|
21
|
+
* @param {string} outputPath - Path where to save the extracted PNG
|
|
22
|
+
* @returns {Promise<boolean>} True if thumbnail was found and extracted
|
|
23
|
+
*/
|
|
24
|
+
export async function extractThumbnailFromFCStd(zipFilePath, outputPath) {
|
|
25
|
+
const zipfile = await openZipPromise(zipFilePath);
|
|
26
|
+
let foundThumbnail = false;
|
|
27
|
+
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
zipfile.on('entry', (entry) => {
|
|
30
|
+
// Look specifically for thumbnails/Thumbnail.png
|
|
31
|
+
if (entry.fileName === THUMBNAIL_PATH) {
|
|
32
|
+
console.log(`✅ Found Thumbnail in ${path.basename(zipFilePath)}`);
|
|
33
|
+
foundThumbnail = true;
|
|
34
|
+
|
|
35
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
zipfile.close();
|
|
38
|
+
return reject(err);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create directory if it doesn't exist
|
|
42
|
+
const outputDir = path.dirname(outputPath);
|
|
43
|
+
if (!fs.existsSync(outputDir)) {
|
|
44
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
48
|
+
readStream.pipe(writeStream);
|
|
49
|
+
|
|
50
|
+
// Handle readStream errors to prevent resource leaks
|
|
51
|
+
readStream.on('error', (err) => {
|
|
52
|
+
zipfile.close();
|
|
53
|
+
reject(err);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
writeStream.on('finish', () => {
|
|
57
|
+
console.log(`✅ Extracted thumbnail to: ${outputPath}`);
|
|
58
|
+
zipfile.close();
|
|
59
|
+
resolve(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
writeStream.on('error', (err) => {
|
|
63
|
+
zipfile.close();
|
|
64
|
+
reject(err);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
zipfile.readEntry();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
zipfile.on('end', () => {
|
|
73
|
+
if (!foundThumbnail) {
|
|
74
|
+
console.log(`⚠️ No thumbnail found in ${path.basename(zipFilePath)}`);
|
|
75
|
+
resolve(false);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
zipfile.on('error', (err) => {
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
zipfile.readEntry();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Process a single .FCStd file to extract its preview
|
|
89
|
+
* @param {string} filePath - Path to the .FCStd file
|
|
90
|
+
* @returns {Promise<boolean>} True if processing was successful
|
|
91
|
+
*/
|
|
92
|
+
export async function processSingleFile(filePath) {
|
|
93
|
+
try {
|
|
94
|
+
// Verify file exists
|
|
95
|
+
if (!fs.existsSync(filePath)) {
|
|
96
|
+
console.log(`❌ File not found: ${filePath}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Verify it's a .FCStd file
|
|
101
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
102
|
+
if (ext !== '.fcstd') {
|
|
103
|
+
console.log(`❌ Not a .FCStd file: ${filePath}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Prepare output path - same directory, same name but with .png extension
|
|
108
|
+
const dir = path.dirname(filePath);
|
|
109
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
110
|
+
const pngPath = path.join(dir, `${baseName}-preview.png`);
|
|
111
|
+
|
|
112
|
+
// Try to extract thumbnail from the file
|
|
113
|
+
await extractThumbnailFromFCStd(filePath, pngPath);
|
|
114
|
+
return true;
|
|
115
|
+
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log(`❌ Error processing ${filePath}: ${err.message}`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load ignore patterns from a file
|
|
7
|
+
* @param {string} ignoreFilePath - Path to the ignore file
|
|
8
|
+
* @returns {string[]} Array of ignore patterns
|
|
9
|
+
*/
|
|
10
|
+
export function loadIgnorePatterns(ignoreFilePath) {
|
|
11
|
+
if (!existsSync(ignoreFilePath)) {
|
|
12
|
+
// Return empty array if file doesn't exist
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(ignoreFilePath, 'utf-8');
|
|
18
|
+
const patterns = content
|
|
19
|
+
.split('\n')
|
|
20
|
+
.map((line) => line.trim())
|
|
21
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
22
|
+
|
|
23
|
+
return patterns;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn(`⚠️ Warning: Could not read ignore file ${ignoreFilePath}: ${error.message}`);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a file path should be ignored based on patterns
|
|
32
|
+
* @param {string} filePath - The file path to check
|
|
33
|
+
* @param {string} rootDir - The root directory for relative pattern matching
|
|
34
|
+
* @param {string[] | undefined} customPatterns - Optional custom patterns to use
|
|
35
|
+
* @param {boolean} enabled - Whether ignore functionality is enabled
|
|
36
|
+
* @returns {boolean} True if the file should be ignored, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
export function shouldIgnoreFile(filePath, rootDir, customPatterns = undefined, enabled = true) {
|
|
39
|
+
if (!enabled) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const patterns = customPatterns ?? [];
|
|
44
|
+
|
|
45
|
+
if (patterns.length === 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Convert file path to relative path for pattern matching
|
|
50
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
51
|
+
|
|
52
|
+
// Check if any pattern matches
|
|
53
|
+
for (const pattern of patterns) {
|
|
54
|
+
try {
|
|
55
|
+
if (minimatch(relativePath, pattern, { dot: true })) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`⚠️ Warning: Error matching pattern '${pattern}': ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Filter an array of file paths based on ignore patterns
|
|
68
|
+
* @param {string[]} filePaths - Array of file paths to filter
|
|
69
|
+
* @param {string} rootDir - The root directory for relative pattern matching
|
|
70
|
+
* @param {string[] | undefined} customPatterns - Optional custom patterns to use
|
|
71
|
+
* @param {boolean} enabled - Whether ignore functionality is enabled
|
|
72
|
+
* @returns {string[]} Filtered array of file paths that should not be ignored
|
|
73
|
+
*/
|
|
74
|
+
export function filterIgnoredFiles(filePaths, rootDir, customPatterns = undefined, enabled = true) {
|
|
75
|
+
if (!enabled) {
|
|
76
|
+
return filePaths;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return filePaths.filter((filePath) => !shouldIgnoreFile(filePath, rootDir, customPatterns, enabled));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load ignore patterns and resolve to absolute paths if needed
|
|
84
|
+
* @param {string} ignoreFilePath - Path to the ignore config file
|
|
85
|
+
* @param {string} cwd - Current working directory for relative resolution
|
|
86
|
+
* @returns {string[]} Array of patterns
|
|
87
|
+
*/
|
|
88
|
+
export function loadIgnoreConfig(ignoreFilePath, cwd = process.cwd()) {
|
|
89
|
+
if (!existsSync(ignoreFilePath)) {
|
|
90
|
+
console.warn(`⚠️ Warning: Ignore config file not found: ${ignoreFilePath}`);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const patterns = loadIgnorePatterns(ignoreFilePath);
|
|
95
|
+
console.log(`🔍 Loaded ${patterns.length} ignore patterns from ${ignoreFilePath}`);
|
|
96
|
+
return patterns;
|
|
97
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
import {
|
|
7
|
+
processSingleFile,
|
|
8
|
+
extractThumbnailFromFCStd,
|
|
9
|
+
} from "./extract-png-from-fcstd.js";
|
|
10
|
+
import { loadIgnoreConfig, filterIgnoredFiles } from "./ignore-utils.js";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
// Get root directory for pattern matching
|
|
16
|
+
function getRootDir(cwd = process.cwd()) {
|
|
17
|
+
return cwd;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Run FreeCAD with isofit macro to generate preview
|
|
21
|
+
async function runFreeCADIsofit(fcstdFile) {
|
|
22
|
+
const macroPath = path.join(__dirname, "isofit.FCMacro");
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
console.log(
|
|
26
|
+
`🔧 Running FreeCAD with Isometric, Fit All macro on ${fcstdFile}...`,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const freecad = spawn("freecad", [fcstdFile, macroPath]);
|
|
30
|
+
|
|
31
|
+
freecad.stdout.on("data", (data) => {
|
|
32
|
+
// Log FreeCAD output if needed for debugging
|
|
33
|
+
// console.log(`FreeCAD: ${data}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
freecad.stderr.on("data", (data) => {
|
|
37
|
+
// Log errors but don't fail immediately
|
|
38
|
+
// console.error(`FreeCAD stderr: ${data}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
freecad.on("close", (code) => {
|
|
42
|
+
if (code === 0) {
|
|
43
|
+
console.log(`✅ FreeCAD processing complete for ${fcstdFile}`);
|
|
44
|
+
resolve();
|
|
45
|
+
} else {
|
|
46
|
+
reject(new Error(`FreeCAD exited with code ${code}`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
freecad.on("error", (err) => {
|
|
51
|
+
reject(
|
|
52
|
+
new Error(
|
|
53
|
+
`Failed to start FreeCAD: ${err.message}. Make sure FreeCAD is installed and available in PATH.`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Process all .FCStd files in current directory and subdirectories
|
|
61
|
+
async function processAllFiles(runFit = false, ignoreConfig = null) {
|
|
62
|
+
try {
|
|
63
|
+
// Find all .FCStd files in the current directory and subdirectories (case insensitive)
|
|
64
|
+
let fcstdFiles = await glob("**/*.FCStd", { nocase: true });
|
|
65
|
+
|
|
66
|
+
if (fcstdFiles.length === 0) {
|
|
67
|
+
console.log("❌ No .FCStd files found");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Apply ignore patterns if config is provided
|
|
72
|
+
let ignorePatterns = [];
|
|
73
|
+
if (ignoreConfig) {
|
|
74
|
+
const rootDir = getRootDir();
|
|
75
|
+
ignorePatterns = loadIgnoreConfig(ignoreConfig, rootDir);
|
|
76
|
+
if (ignorePatterns.length > 0) {
|
|
77
|
+
const originalCount = fcstdFiles.length;
|
|
78
|
+
fcstdFiles = filterIgnoredFiles(fcstdFiles, rootDir, ignorePatterns, true);
|
|
79
|
+
const ignoredCount = originalCount - fcstdFiles.length;
|
|
80
|
+
if (ignoredCount > 0) {
|
|
81
|
+
console.log(`🔍 Ignored ${ignoredCount} files based on ${ignoreConfig}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (fcstdFiles.length === 0) {
|
|
87
|
+
console.log("❌ All files were filtered out by ignore patterns");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`✅ Found ${fcstdFiles.length} .FCStd files to check`);
|
|
92
|
+
|
|
93
|
+
let processedCount = 0;
|
|
94
|
+
let successCount = 0;
|
|
95
|
+
let failureCount = 0;
|
|
96
|
+
|
|
97
|
+
for (const file of fcstdFiles) {
|
|
98
|
+
try {
|
|
99
|
+
// If --fit flag is set, run FreeCAD with isofit macro first
|
|
100
|
+
if (runFit) {
|
|
101
|
+
await runFreeCADIsofit(file);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Prepare output path - same directory, same name but with .png extension
|
|
105
|
+
const dir = path.dirname(file);
|
|
106
|
+
const baseName = path.basename(file, path.extname(file));
|
|
107
|
+
const pngPath = path.join(dir, `${baseName}-preview.png`);
|
|
108
|
+
|
|
109
|
+
// Try to extract thumbnail from the file
|
|
110
|
+
await extractThumbnailFromFCStd(file, pngPath);
|
|
111
|
+
successCount++;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(`❌ Error processing ${file}: ${err.message}`);
|
|
114
|
+
failureCount++;
|
|
115
|
+
}
|
|
116
|
+
processedCount++;
|
|
117
|
+
console.log(`📊 Progress: ${processedCount}/${fcstdFiles.length} (${successCount} succeeded, ${failureCount} failed)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (failureCount > 0) {
|
|
121
|
+
console.log(`⚠️ Processing completed with ${failureCount} failures out of ${processedCount} files`);
|
|
122
|
+
return false; // Indicate partial failure
|
|
123
|
+
} else {
|
|
124
|
+
console.log("✅ Processing complete - all files processed successfully");
|
|
125
|
+
return true; // Indicate success
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error("❌ Error:", err);
|
|
129
|
+
return false; // Indicate failure
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Main entry point
|
|
134
|
+
async function main() {
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
|
|
137
|
+
// Check for --version or --help flags
|
|
138
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
139
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
|
|
140
|
+
console.log(pkg.version);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
145
|
+
console.log(`FreeCAD Preview Extractor v0.0.2-alpha
|
|
146
|
+
|
|
147
|
+
Usage: fcxtc [options] [file]
|
|
148
|
+
|
|
149
|
+
Options:
|
|
150
|
+
--fit Run FreeCAD with isometric fit macro before extracting
|
|
151
|
+
--ignore-config Path to ignore config file (e.g., .fcignore)
|
|
152
|
+
--version, -v Show version number
|
|
153
|
+
--help, -h Show this help message
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
fcxtc Extract previews from all .FCStd files in current directory
|
|
157
|
+
fcxtc myfile.FCStd Extract preview from specific file
|
|
158
|
+
fcxtc --fit Run isometric fit macro on all files before extracting
|
|
159
|
+
fcxtc --ignore-config .fcignore Use ignore config file
|
|
160
|
+
fcxtc --fit --ignore-config .fcignore Combine flags`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check for --fit flag
|
|
165
|
+
const fitIndex = args.indexOf("--fit");
|
|
166
|
+
const runFit = fitIndex !== -1;
|
|
167
|
+
|
|
168
|
+
// Check for --ignore-config flag
|
|
169
|
+
let ignoreConfig = null;
|
|
170
|
+
const ignoreConfigIndex = args.indexOf("--ignore-config");
|
|
171
|
+
if (ignoreConfigIndex !== -1 && ignoreConfigIndex + 1 < args.length) {
|
|
172
|
+
ignoreConfig = args[ignoreConfigIndex + 1];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Filter out --fit, --ignore-config and its value to get file arguments
|
|
176
|
+
const fileArgs = args.filter((arg, index) => {
|
|
177
|
+
if (arg === "--fit") return false;
|
|
178
|
+
if (arg === "--ignore-config") return false;
|
|
179
|
+
if (ignoreConfigIndex !== -1 && index === ignoreConfigIndex + 1) return false;
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (fileArgs.length === 0) {
|
|
184
|
+
// No arguments - process all files
|
|
185
|
+
if (runFit) {
|
|
186
|
+
console.log(
|
|
187
|
+
"🔍 Running FreeCAD isofit and extracting images from all FreeCAD files in current directory...",
|
|
188
|
+
);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(
|
|
191
|
+
"🔍 Extracting images from all FreeCAD files in current directory...",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (ignoreConfig) {
|
|
195
|
+
console.log(`🔍 Using ignore config: ${ignoreConfig}`);
|
|
196
|
+
}
|
|
197
|
+
const success = await processAllFiles(runFit, ignoreConfig);
|
|
198
|
+
if (!success) {
|
|
199
|
+
process.exit(1); // Exit with error code if batch processing failed
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Process specific file(s)
|
|
203
|
+
const file = fileArgs[0];
|
|
204
|
+
|
|
205
|
+
// For single file mode, ignore config is not applied
|
|
206
|
+
if (runFit) {
|
|
207
|
+
console.log(`🔧 Running FreeCAD isofit on: ${file}`);
|
|
208
|
+
await runFreeCADIsofit(file);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`🔍 Extracting image from: ${file}`);
|
|
212
|
+
await processSingleFile(file);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { processAllFiles, runFreeCADIsofit };
|
|
217
|
+
export { loadIgnoreConfig, filterIgnoredFiles } from "./ignore-utils.js";
|
|
218
|
+
|
|
219
|
+
// Run the script
|
|
220
|
+
main().catch((err) => {
|
|
221
|
+
console.error("❌ Fatal error:", err);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Macro Begin: isofit.FCMacro +++++++++++++++++++++++++++++++++++++++++++++++++
|
|
4
|
+
import FreeCAD
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
view = Gui.activeDocument().activeView()
|
|
8
|
+
view.setAnimationEnabled(False)
|
|
9
|
+
view.viewIsometric()
|
|
10
|
+
Gui.SendMsgToActiveView("ViewFit")
|
|
11
|
+
Gui.updateGui()
|
|
12
|
+
Gui.SendMsgToActiveView("Save")
|
|
13
|
+
Gui.getMainWindow().close()
|
|
14
|
+
except Exception as e:
|
|
15
|
+
print("Error in isofit macro: {}".format(e))
|
|
16
|
+
Gui.getMainWindow().close()
|
|
17
|
+
|
|
18
|
+
# Macro End: isofit.FCMacro +++++++++++++++++++++++++++++++++++++++++++++++++
|