Skip to content
Open
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
58 changes: 45 additions & 13 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _check_dependencies():
import json
import uuid

from flask import Flask, request, render_template, jsonify, send_from_directory, Response, stream_with_context
from flask import Flask, request, render_template, jsonify, send_from_directory, send_file, Response, stream_with_context

from model_loader import load_model, build_filtered_mesh, ModelLoadError, SUPPORTED_EXTENSIONS, load_model_from_zip, remap_part_face_ranges
from renderer import get_model_scale, detect_front_axis, render_view, render_rib_sections, AxisConfig, compute_ambient_occlusion, rotate_mesh_around_up_axis, compute_part_centroids, project_part_labels, AOPerformanceError, finalize_ssao_views, compute_directional_shading
Expand Down Expand Up @@ -175,42 +175,62 @@ def upload():
}), 400

keep_uploads = request.form.get("keep_uploads", "0") == "1"
session_id = str(uuid.uuid4())

if keep_uploads:
save_path = os.path.join(UPLOAD_DIR, f.filename)
else:
# Default behavior: don't leave a permanent copy in UPLOAD_DIR.
# Save into a dedicated tmp folder and delete it right after the
# mesh is parsed (in `finally`, so a failed parse doesn't leave
# orphaned temp files either).
tmp_name = f"{uuid.uuid4().hex}{ext}"
save_path = os.path.join(UPLOAD_TMP_DIR, tmp_name)
save_path = os.path.join(UPLOAD_TMP_DIR, f"{session_id}{ext}")
f.save(save_path)

# Determine the path Three.js will load for the 3D preview.
# KN5 files are converted to OBJ internally; we save a copy of that OBJ.
# ZIP uploads don't produce a single loadable file, so preview is skipped.
if is_zip:
preview_path = None
preview_ext = None
elif ext == ".kn5":
preview_path = os.path.join(UPLOAD_TMP_DIR, f"{session_id}.obj")
preview_ext = ".obj"
else:
preview_path = save_path
preview_ext = ext

chosen_filename = f.filename
zip_warning = None
parse_ok = False
try:
try:
if is_zip:
mesh, part_face_ranges, uv, chosen_filename, zip_warning = load_model_from_zip(save_path)
elif ext == ".kn5":
mesh, part_face_ranges, uv = load_model(save_path, kn5_preview_out=preview_path)
else:
mesh, part_face_ranges, uv = load_model(save_path)
parse_ok = True
except ModelLoadError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
return jsonify({"error": f"Unexpected error loading file: {e}"}), 500
finally:
if not keep_uploads:
try:
os.remove(save_path)
except Exception:
pass
# On parse failure, clean up temp files. On success, keep them so the
# /preview/<session_id> endpoint can serve the file for the 3D viewer.
# UPLOAD_TMP_DIR is wiped on next app startup, so no permanent accumulation.
if not parse_ok and not keep_uploads:
for _p in (save_path, preview_path):
if _p and os.path.exists(_p):
try:
os.remove(_p)
except Exception:
pass

session_id = str(uuid.uuid4())
_MODEL_CACHE[session_id] = {
"mesh": mesh,
"part_face_ranges": part_face_ranges,
"filename": chosen_filename,
"uv": uv,
"preview_path": preview_path,
"preview_ext": preview_ext,
}

# Load saved selection state for this filename, if any.
Expand All @@ -233,11 +253,23 @@ def upload():
"face_count": len(mesh.faces),
}
response["has_uv"] = uv is not None
response["preview_ext"] = preview_ext
if zip_warning:
response["warning"] = zip_warning
return jsonify(response)


@app.route("/preview/<sid>")
def preview(sid):
"""Serves the uploaded model file for the Three.js 3D preview panel."""
if sid not in _MODEL_CACHE:
return "Session not found", 404
p = _MODEL_CACHE[sid].get("preview_path")
if not p or not os.path.exists(p):
return "Preview file not available", 404
return send_file(p)


@app.route("/generate", methods=["POST"])
def generate():
"""Runs the render and streams Server-Sent Events (SSE) back to the
Expand Down
8 changes: 7 additions & 1 deletion model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import os
import re
import shutil
import tempfile
import numpy as np
import trimesh
Expand Down Expand Up @@ -117,14 +118,17 @@ def _parse_obj_groups(path):



def load_model(path):
def load_model(path, kn5_preview_out=None):
"""Loads any supported 3D model file and returns (mesh, part_face_ranges, uv)
where part_face_ranges maps {part_name: (face_start_index, face_end_index)}
against mesh.faces, so the caller can map checkbox selections back to
actual faces regardless of input format. uv is an (n_vertices, 2) array
aligned with mesh.vertices, or None if no usable UV data was found —
only the KN5/OBJ path currently extracts it (see _load_obj_with_real_groups);
other formats (.dae/.stl/.gltf/.glb) always return None here for now.

kn5_preview_out: if given, the converted OBJ is copied there before the
temp directory is cleaned up, so callers can serve it for preview purposes.
"""
ext = os.path.splitext(path)[1].lower()
if ext not in SUPPORTED_EXTENSIONS:
Expand All @@ -140,6 +144,8 @@ def load_model(path):
kn5_convert(path, tmp_obj)
except Exception as e:
raise ModelLoadError(f"Failed to convert KN5 file: {e}") from e
if kn5_preview_out:
shutil.copy2(tmp_obj, kn5_preview_out)
return _load_obj_with_real_groups(tmp_obj)

if ext == ".obj":
Expand Down
6 changes: 6 additions & 0 deletions static/vendor/three/build/three.module.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/controls/OrbitControls.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/environments/RoomEnvironment.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/loaders/ColladaLoader.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/loaders/GLTFLoader.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/loaders/OBJLoader.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions static/vendor/three/jsm/loaders/STLLoader.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading