chore: remove legacy blender compression path
This commit is contained in:
@@ -2,7 +2,6 @@ UPLOAD_SECRET_KEY=your-secret-key-here
|
|||||||
GITHUB_TOKEN=ghp_your-github-personal-access-token
|
GITHUB_TOKEN=ghp_your-github-personal-access-token
|
||||||
GIT_BRANCH=main
|
GIT_BRANCH=main
|
||||||
GIT_REPO_URL=https://github.com/your-org/your-repo.git
|
GIT_REPO_URL=https://github.com/your-org/your-repo.git
|
||||||
BLENDER_PATH=/Applications/Blender.app/Contents/MacOS/Blender
|
|
||||||
|
|
||||||
# Nextcloud Drive (public share WebDAV)
|
# Nextcloud Drive (public share WebDAV)
|
||||||
NEXTCLOUD_URL=https://cloud.example.com
|
NEXTCLOUD_URL=https://cloud.example.com
|
||||||
|
|||||||
+3
-7
@@ -1,6 +1,6 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Upload GLTF — Dockerfile for Coolify
|
# Upload GLTF — Dockerfile for Coolify
|
||||||
# Node 20 Debian · Blender (headless) · Multi-stage build
|
# Node 20 Debian · Multi-stage build
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# --- Stage 1: Dependencies ---------------------------------------------------
|
# --- Stage 1: Dependencies ---------------------------------------------------
|
||||||
@@ -28,11 +28,10 @@ RUN npm run build
|
|||||||
FROM node:20-slim AS runner
|
FROM node:20-slim AS runner
|
||||||
|
|
||||||
LABEL maintainer="La Fabrik Durable"
|
LABEL maintainer="La Fabrik Durable"
|
||||||
LABEL description="Secure 3D asset upload interface with Draco compression and GitHub push"
|
LABEL description="Secure GLTF upload interface with texture compression and GitHub push"
|
||||||
|
|
||||||
# Install Blender (headless) + tini
|
# Install runtime helpers
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
blender \
|
|
||||||
tini \
|
tini \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -49,9 +48,6 @@ COPY --from=builder /app/.next/standalone ./
|
|||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
# Copy the Blender compression script
|
|
||||||
COPY --from=builder /app/scripts ./scripts
|
|
||||||
|
|
||||||
# Ensure tmp dir for uploads exists
|
# Ensure tmp dir for uploads exists
|
||||||
RUN mkdir -p /tmp/assets
|
RUN mkdir -p /tmp/assets
|
||||||
|
|
||||||
|
|||||||
@@ -237,13 +237,10 @@ lib/
|
|||||||
├── upload-staging.ts # Temporary server-side staging and prepared asset reuse
|
├── upload-staging.ts # Temporary server-side staging and prepared asset reuse
|
||||||
├── upload-lock.ts # Lightweight in-memory per-folder upload lock
|
├── upload-lock.ts # Lightweight in-memory per-folder upload lock
|
||||||
├── asset-classification.ts # Group assets by family for commit messages
|
├── asset-classification.ts # Group assets by family for commit messages
|
||||||
├── blender.ts # Legacy Blender compression helper
|
|
||||||
├── commit-message.ts # Commit message builder
|
├── commit-message.ts # Commit message builder
|
||||||
├── parse-upload.ts # FormData parser + validation
|
├── parse-upload.ts # FormData parser + validation
|
||||||
├── validate-folder.ts # Client-side folder validation (discriminated union)
|
├── validate-folder.ts # Client-side folder validation (discriminated union)
|
||||||
└── format-bytes.ts # Byte formatting utility
|
└── format-bytes.ts # Byte formatting utility
|
||||||
scripts/
|
|
||||||
└── compress.py # Legacy Blender compression script
|
|
||||||
Dockerfile # Multi-stage build: Node 20 slim + tini
|
Dockerfile # Multi-stage build: Node 20 slim + tini
|
||||||
docker-entrypoint.sh # Startup check + launch
|
docker-entrypoint.sh # Startup check + launch
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -62,11 +62,3 @@ export function FolderIcon({ className = 'w-6 h-6' }: IconProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoIcon({ className = 'w-5 h-5' }: IconProps) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ echo "[upload-gltf] Starting Upload GLTF..."
|
|||||||
# Ensure tmp directory for uploads exists
|
# Ensure tmp directory for uploads exists
|
||||||
mkdir -p /tmp/assets
|
mkdir -p /tmp/assets
|
||||||
|
|
||||||
# Check if Blender is available for Draco compression
|
|
||||||
if command -v blender > /dev/null 2>&1; then
|
|
||||||
BLENDER_VERSION=$(blender --version 2>/dev/null | head -n 1)
|
|
||||||
echo "[upload-gltf] Blender found: $BLENDER_VERSION"
|
|
||||||
echo "[upload-gltf] Draco compression is enabled."
|
|
||||||
else
|
|
||||||
echo "[upload-gltf] WARNING: Blender not found. Models will be pushed without compression."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[upload-gltf] Ready. Launching application..."
|
echo "[upload-gltf] Ready. Launching application..."
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { existsSync } from 'fs'
|
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compress a GLTF/GLB model using Blender's Draco compression.
|
|
||||||
* Returns { success: true } on success, or { success: false, error } on failure.
|
|
||||||
* Callers should fall back to the original file on failure.
|
|
||||||
*/
|
|
||||||
export async function compressWithBlender(
|
|
||||||
inputPath: string,
|
|
||||||
outputPath: string,
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
const blenderPath = process.env.BLENDER_PATH || 'blender'
|
|
||||||
const scriptPath = join(process.cwd(), 'scripts', 'compress.py')
|
|
||||||
|
|
||||||
if (!existsSync(scriptPath)) {
|
|
||||||
return { success: false, error: 'scripts/compress.py introuvable' }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await execFileAsync(
|
|
||||||
blenderPath,
|
|
||||||
[
|
|
||||||
'--background',
|
|
||||||
'--python', scriptPath,
|
|
||||||
'--',
|
|
||||||
'-i', inputPath,
|
|
||||||
'-o', outputPath,
|
|
||||||
'--draco-level', '7',
|
|
||||||
'--texture-size', '512',
|
|
||||||
'-q',
|
|
||||||
],
|
|
||||||
{ timeout: 120_000 },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!existsSync(outputPath)) {
|
|
||||||
return { success: false, error: "Blender n'a pas produit de fichier compresse" }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
return { success: false, error: `Compression Blender echouee: ${message}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,6 +46,7 @@ export async function prepareGitAssets({
|
|||||||
|
|
||||||
const textureResult = await compressTextureBuffer(pf.filename, pf.buffer)
|
const textureResult = await compressTextureBuffer(pf.filename, pf.buffer)
|
||||||
content = textureResult.buffer
|
content = textureResult.buffer
|
||||||
|
compressed ||= textureResult.compressed
|
||||||
|
|
||||||
if (textureResult.error && !compressionError) {
|
if (textureResult.error && !compressionError) {
|
||||||
compressionError = textureResult.error
|
compressionError = textureResult.error
|
||||||
|
|||||||
+1
-10
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { dirname, join } from 'path'
|
import { dirname, join } from 'path'
|
||||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'fs/promises'
|
import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { TMP_DIR } from '@/lib/constants'
|
import { TMP_DIR } from '@/lib/constants'
|
||||||
import { prepareGitAssets } from '@/lib/prepare-git-assets'
|
import { prepareGitAssets } from '@/lib/prepare-git-assets'
|
||||||
@@ -203,12 +203,3 @@ export async function readStagedOriginalFiles(stagingId: string): Promise<{ fold
|
|||||||
export async function cleanupStagingUpload(stagingId: string) {
|
export async function cleanupStagingUpload(stagingId: string) {
|
||||||
await rm(getStageDir(stagingId), { recursive: true, force: true })
|
await rm(getStageDir(stagingId), { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stagingExists(stagingId: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const info = await stat(getStageDir(stagingId))
|
|
||||||
return info.isDirectory()
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,422 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Blender Draco Compression Script
|
|
||||||
CLI tool to compress 3D meshes with Draco compression using Blender
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
blender --background --python compress.py -- [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-i, --input FILE Input file (required in advanced mode)
|
|
||||||
-o, --output FILE Output file (default: input_compressed.glb)
|
|
||||||
--draco-level LEVEL Draco compression level 0-10 (default: 7)
|
|
||||||
--resize-textures / --no-resize Enable/disable texture resizing (default: enabled)
|
|
||||||
--texture-size SIZE Max texture size in pixels (default: 512)
|
|
||||||
--batch Batch mode: input is a directory
|
|
||||||
--output-dir DIR Output directory for batch mode
|
|
||||||
--format FORMAT Output format: glb or gltf (default: glb)
|
|
||||||
-q, --quiet Quiet mode (less output)
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Simple mode (all defaults)
|
|
||||||
blender --background --python compress.py -- input.glb
|
|
||||||
|
|
||||||
# Advanced mode
|
|
||||||
blender --background --python compress.py -- -i input.glb -o output.glb --draco-level 10
|
|
||||||
|
|
||||||
# Batch mode
|
|
||||||
blender --background --python compress.py -- --batch ./models/ --output-dir ./compressed/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import io
|
|
||||||
import argparse
|
|
||||||
from contextlib import redirect_stdout
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
try:
|
|
||||||
import bpy_types
|
|
||||||
except ImportError:
|
|
||||||
bpy_types = None
|
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_IMPORT_FORMATS = {
|
|
||||||
'.glb': 'gltf',
|
|
||||||
'.gltf': 'gltf',
|
|
||||||
'.obj': 'obj',
|
|
||||||
'.ply': 'ply',
|
|
||||||
'.stl': 'stl',
|
|
||||||
'.x3d': 'x3d',
|
|
||||||
'.wrl': 'x3d',
|
|
||||||
'.3ds': '3ds',
|
|
||||||
'.fbx': 'fbx',
|
|
||||||
'.dae': 'dae',
|
|
||||||
}
|
|
||||||
|
|
||||||
SUPPORTED_OUTPUT_FORMATS = ['glb', 'gltf']
|
|
||||||
|
|
||||||
|
|
||||||
def file_name(filepath):
|
|
||||||
return os.path.split(filepath)[1]
|
|
||||||
|
|
||||||
|
|
||||||
def file_suffix(filepath):
|
|
||||||
return os.path.splitext(file_name(filepath))[1].lower()
|
|
||||||
|
|
||||||
|
|
||||||
def dir_path(filepath):
|
|
||||||
return os.path.split(filepath)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def get_import_operator(suffix):
|
|
||||||
operators = {
|
|
||||||
'gltf': bpy.ops.import_scene.gltf,
|
|
||||||
'obj': bpy.ops.import_scene.obj,
|
|
||||||
'ply': bpy.ops.import_mesh.ply,
|
|
||||||
'stl': bpy.ops.import_mesh.stl,
|
|
||||||
'x3d': bpy.ops.import_scene.x3d,
|
|
||||||
'3ds': bpy.ops.import_scene.fbx,
|
|
||||||
'fbx': bpy.ops.import_scene.fbx,
|
|
||||||
'dae': bpy.ops.import_scene.dae,
|
|
||||||
}
|
|
||||||
return operators.get(suffix)
|
|
||||||
|
|
||||||
|
|
||||||
def get_output_extension(format_type):
|
|
||||||
return '.glb' if format_type == 'glb' else '.gltf'
|
|
||||||
|
|
||||||
|
|
||||||
def import_mesh(filepath):
|
|
||||||
suffix = file_suffix(filepath)
|
|
||||||
if suffix not in SUPPORTED_IMPORT_FORMATS:
|
|
||||||
raise ValueError(f"Unsupported input format: {suffix}")
|
|
||||||
|
|
||||||
format_type = SUPPORTED_IMPORT_FORMATS[suffix]
|
|
||||||
import_op = get_import_operator(format_type)
|
|
||||||
|
|
||||||
if import_op is None:
|
|
||||||
raise ValueError(f"Cannot import {suffix} format")
|
|
||||||
|
|
||||||
stdout_buffer = io.StringIO()
|
|
||||||
with redirect_stdout(stdout_buffer):
|
|
||||||
import_op(filepath=str(filepath))
|
|
||||||
|
|
||||||
output = stdout_buffer.getvalue()
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def clear_scene():
|
|
||||||
bpy.ops.object.select_all(action='SELECT')
|
|
||||||
bpy.ops.object.delete()
|
|
||||||
return len(bpy.data.objects) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def resize_textures(target_size):
|
|
||||||
resized_count = 0
|
|
||||||
|
|
||||||
for image in bpy.data.images:
|
|
||||||
if image.size[0] > target_size or image.size[1] > target_size:
|
|
||||||
old_width = image.size[0]
|
|
||||||
old_height = image.size[1]
|
|
||||||
|
|
||||||
scale = min(target_size / old_width, target_size / old_height)
|
|
||||||
new_width = int(old_width * scale)
|
|
||||||
new_height = int(old_height * scale)
|
|
||||||
|
|
||||||
image.scale(new_width, new_height)
|
|
||||||
resized_count += 1
|
|
||||||
print(f" Resized '{image.name}': {old_width}x{old_height} -> {new_width}x{new_height}")
|
|
||||||
|
|
||||||
return resized_count
|
|
||||||
|
|
||||||
|
|
||||||
def export_mesh(filepath, draco_level=7, format_type='glb'):
|
|
||||||
export_kwargs = {
|
|
||||||
'filepath': str(filepath),
|
|
||||||
'export_draco_mesh_compression_enable': True,
|
|
||||||
'export_draco_mesh_compression_level': draco_level,
|
|
||||||
'export_format': 'GLB' if format_type == 'glb' else 'GLTF_SEPARATE',
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout_buffer = io.StringIO()
|
|
||||||
with redirect_stdout(stdout_buffer):
|
|
||||||
bpy.ops.export_scene.gltf(**export_kwargs)
|
|
||||||
|
|
||||||
return stdout_buffer.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_output(input_path, format_type='glb'):
|
|
||||||
input_file = Path(input_path)
|
|
||||||
suffix = get_output_extension(format_type)
|
|
||||||
return str(input_file.parent / f"{input_file.stem}_compressed{suffix}")
|
|
||||||
|
|
||||||
|
|
||||||
def process_file(input_path, output_path=None, draco_level=7,
|
|
||||||
resize_textures_flag=True, texture_size=512,
|
|
||||||
format_type='glb', quiet=False):
|
|
||||||
if not quiet:
|
|
||||||
print(f"\n{'='*50}")
|
|
||||||
print(f"Processing: {input_path}")
|
|
||||||
|
|
||||||
if not os.path.exists(input_path):
|
|
||||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
original_size = os.path.getsize(input_path)
|
|
||||||
print(f"Original size: {original_size / 1024:.2f} KB")
|
|
||||||
|
|
||||||
if not clear_scene():
|
|
||||||
raise RuntimeError("Failed to clear Blender scene")
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
print("Importing mesh...")
|
|
||||||
|
|
||||||
import_mesh(input_path)
|
|
||||||
|
|
||||||
if len(bpy.data.objects) == 0:
|
|
||||||
raise RuntimeError(f"No objects imported from {input_path}")
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
mesh_count = sum(1 for obj in bpy.data.objects if isinstance(obj.data, bpy.types.Mesh))
|
|
||||||
print(f"Imported {mesh_count} mesh(es)")
|
|
||||||
|
|
||||||
if resize_textures_flag:
|
|
||||||
if not quiet:
|
|
||||||
print(f"Resizing textures (max: {texture_size}px)...")
|
|
||||||
resized = resize_textures(texture_size)
|
|
||||||
if not quiet and resized > 0:
|
|
||||||
print(f"Resized {resized} texture(s)")
|
|
||||||
|
|
||||||
if output_path is None:
|
|
||||||
output_path = get_default_output(input_path, format_type)
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
print(f"Exporting with Draco compression (level={draco_level})...")
|
|
||||||
|
|
||||||
export_mesh(output_path, draco_level, format_type)
|
|
||||||
|
|
||||||
if not os.path.exists(output_path):
|
|
||||||
raise RuntimeError(f"Export failed: {output_path} not created")
|
|
||||||
|
|
||||||
final_size = os.path.getsize(output_path)
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
original_size = os.path.getsize(input_path) if os.path.exists(input_path) else 0
|
|
||||||
reduction = ((original_size - final_size) / original_size * 100) if original_size > 0 else 0
|
|
||||||
print(f"\nOutput: {output_path}")
|
|
||||||
print(f"Final size: {final_size / 1024:.2f} KB")
|
|
||||||
if original_size > 0:
|
|
||||||
print(f"Reduction: {reduction:.1f}%")
|
|
||||||
print("Compression complete!")
|
|
||||||
|
|
||||||
return output_path, final_size
|
|
||||||
|
|
||||||
|
|
||||||
def process_batch(input_dir, output_dir=None, draco_level=7,
|
|
||||||
resize_textures_flag=True, texture_size=512,
|
|
||||||
format_type='glb', quiet=False):
|
|
||||||
if not os.path.exists(input_dir):
|
|
||||||
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
|
||||||
|
|
||||||
if output_dir:
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
files_found = []
|
|
||||||
for ext in SUPPORTED_IMPORT_FORMATS.keys():
|
|
||||||
files_found.extend(Path(input_dir).glob(f"*{ext}"))
|
|
||||||
files_found.extend(Path(input_dir).glob(f"*{ext.upper()}"))
|
|
||||||
|
|
||||||
files_found = sorted(set(files_found))
|
|
||||||
|
|
||||||
if not files_found:
|
|
||||||
print(f"No supported files found in {input_dir}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
print(f"\n{'='*50}")
|
|
||||||
print(f"BATCH MODE")
|
|
||||||
print(f"Input directory: {input_dir}")
|
|
||||||
print(f"Files found: {len(files_found)}")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for i, file_path in enumerate(files_found, 1):
|
|
||||||
if output_dir:
|
|
||||||
input_file = Path(file_path)
|
|
||||||
suffix = get_output_extension(format_type)
|
|
||||||
output_path = os.path.join(output_dir, f"{input_file.stem}_compressed{suffix}")
|
|
||||||
else:
|
|
||||||
output_path = None
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
print(f"\n[{i}/{len(files_found)}]")
|
|
||||||
|
|
||||||
try:
|
|
||||||
result_path, _ = process_file(
|
|
||||||
str(file_path),
|
|
||||||
output_path=output_path,
|
|
||||||
draco_level=draco_level,
|
|
||||||
resize_textures_flag=resize_textures_flag,
|
|
||||||
texture_size=texture_size,
|
|
||||||
format_type=format_type,
|
|
||||||
quiet=quiet
|
|
||||||
)
|
|
||||||
results.append((str(file_path), result_path, True, None))
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
if not quiet:
|
|
||||||
print(f"ERROR: {error_msg}")
|
|
||||||
results.append((str(file_path), None, False, error_msg))
|
|
||||||
|
|
||||||
success_count = sum(1 for _, _, success, _ in results if success)
|
|
||||||
fail_count = len(results) - success_count
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
print(f"\n{'='*50}")
|
|
||||||
print(f"BATCH COMPLETE")
|
|
||||||
print(f"Total files: {len(results)}")
|
|
||||||
print(f"Success: {success_count}")
|
|
||||||
print(f"Failed: {fail_count}")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
argv = sys.argv
|
|
||||||
if "--" not in argv:
|
|
||||||
argv = []
|
|
||||||
else:
|
|
||||||
argv = argv[argv.index("--") + 1:]
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Compress 3D meshes with Draco compression using Blender',
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
epilog="""
|
|
||||||
Examples:
|
|
||||||
# Simple mode (all defaults)
|
|
||||||
blender --background --python compress.py -- input.glb
|
|
||||||
|
|
||||||
# With options
|
|
||||||
blender --background --python compress.py -- -i input.glb -o output.glb --draco-level 10
|
|
||||||
|
|
||||||
# Batch mode
|
|
||||||
blender --background --python compress.py -- --batch ./models/ --output-dir ./compressed/
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'input',
|
|
||||||
nargs='?',
|
|
||||||
help='Input file or directory (for batch mode)'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'-i', '--input',
|
|
||||||
dest='input_file',
|
|
||||||
help='Input file (alternative to positional argument)'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'-o', '--output',
|
|
||||||
dest='output',
|
|
||||||
help='Output file (default: input_compressed.glb)'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--draco-level',
|
|
||||||
type=int,
|
|
||||||
default=7,
|
|
||||||
choices=range(0, 11),
|
|
||||||
help='Draco compression level 0-10 (default: 7)'
|
|
||||||
)
|
|
||||||
|
|
||||||
resize_group = parser.add_mutually_exclusive_group()
|
|
||||||
resize_group.add_argument(
|
|
||||||
'--resize-textures',
|
|
||||||
action='store_true',
|
|
||||||
default=True,
|
|
||||||
help='Enable texture resizing (default: enabled)'
|
|
||||||
)
|
|
||||||
resize_group.add_argument(
|
|
||||||
'--no-resize',
|
|
||||||
action='store_false',
|
|
||||||
dest='resize_textures',
|
|
||||||
help='Disable texture resizing'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--texture-size',
|
|
||||||
type=int,
|
|
||||||
default=512,
|
|
||||||
help='Max texture size in pixels (default: 512)'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--batch',
|
|
||||||
action='store_true',
|
|
||||||
help='Batch mode: process all files in input directory'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--output-dir', '-d',
|
|
||||||
dest='output_dir',
|
|
||||||
help='Output directory for batch mode'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--format', '-f',
|
|
||||||
choices=SUPPORTED_OUTPUT_FORMATS,
|
|
||||||
default='glb',
|
|
||||||
help='Output format (default: glb)'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'-q', '--quiet',
|
|
||||||
action='store_true',
|
|
||||||
help='Quiet mode (less output)'
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
input_path = args.input or args.input_file
|
|
||||||
|
|
||||||
if not input_path:
|
|
||||||
parser.print_help()
|
|
||||||
print("\nError: Input file or directory is required")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.batch or os.path.isdir(input_path):
|
|
||||||
results = process_batch(
|
|
||||||
input_path,
|
|
||||||
output_dir=args.output_dir,
|
|
||||||
draco_level=args.draco_level,
|
|
||||||
resize_textures_flag=args.resize_textures,
|
|
||||||
texture_size=args.texture_size,
|
|
||||||
format_type=args.format,
|
|
||||||
quiet=args.quiet
|
|
||||||
)
|
|
||||||
failed = [r for r in results if not r[2]]
|
|
||||||
if failed:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
process_file(
|
|
||||||
input_path,
|
|
||||||
output_path=args.output,
|
|
||||||
draco_level=args.draco_level,
|
|
||||||
resize_textures_flag=args.resize_textures,
|
|
||||||
texture_size=args.texture_size,
|
|
||||||
format_type=args.format,
|
|
||||||
quiet=args.quiet
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user