Skip to content

Commit 7b4f15a

Browse files
authored
Merge branch 'main' into ITEP-83030/renovate-config
2 parents 63a9f31 + 5b573ea commit 7b4f15a

9 files changed

Lines changed: 718 additions & 271 deletions

File tree

manager/src/django/mesh_generator.py

Lines changed: 243 additions & 84 deletions
Large diffs are not rendered by default.

manager/src/django/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ class Scene(models.Model):
113113
name = models.CharField(max_length=200, unique=True)
114114
map_type = models.CharField("Map Type", max_length=20, choices=MAP_TYPE_CHOICES, default='map_upload', null=True)
115115
thumbnail = models.ImageField(default=None, null=True, editable=False)
116-
map = models.FileField("Scene map as .glb or .ply or image or .zip", default=None, null=True, blank=True,
117-
validators=[FileExtensionValidator(["glb","png","jpeg","jpg","zip","ply"]),
116+
map = models.FileField("Scene map as .glb or .ply or image or .zip or video", default=None, null=True, blank=True,
117+
validators=[FileExtensionValidator(["glb","png","jpeg","jpg","zip","ply","mp4",
118+
"mov", "mkv", "webm", "avi"]),
118119
validate_map_file])
119120
scale = models.FloatField("Pixels per meter", default=None, null=True, blank=True,
120121
validators=[MinValueValidator(np.nextafter(0, 1))])

manager/src/django/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
path('scene/update/<uuid:pk>/', views.SceneUpdateView.as_view(), name='scene_update'),
3030
path('scene/delete/<uuid:pk>/', views.SceneDeleteView.as_view(), name='scene_delete'),
3131
path('scene/generate-mesh/<uuid:pk>/', views.generate_mesh, name='generate_mesh'),
32+
path('scene/generate-mesh-status/<uuid:pk>/',views.generate_mesh_status, name='generate_mesh_status'),
3233
path('mapping-service/status/', views.check_mapping_service_status, name='mapping_service_status'),
3334
path('cam/list/', views.CamListView.as_view(), name='cam_list'),
3435
path('cam/create/', views.CamCreateView.as_view(), name='cam_create'),

manager/src/django/views.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import traceback
99
import uuid
1010
from collections import namedtuple
11+
import tempfile
12+
import subprocess
13+
from pathlib import Path
1114

1215
from django.conf import settings
1316
from django.contrib.admin.views.decorators import user_passes_test
@@ -19,7 +22,7 @@
1922
from django.contrib.auth import user_logged_in, user_login_failed
2023
from django.contrib.sessions.models import Session
2124
from rest_framework.authtoken.models import Token
22-
from django.db import IntegrityError
25+
from django.db import IntegrityError, transaction
2326
from django.dispatch.dispatcher import receiver
2427
from django.http import FileResponse, HttpResponse, HttpResponseNotFound, HttpResponseRedirect, JsonResponse
2528
from django.shortcuts import get_object_or_404, redirect, render
@@ -890,6 +893,57 @@ def generate_camera_pipeline(request, sensor_id):
890893
log.error(f"Traceback: {traceback.format_exc()}")
891894
return JsonResponse({"error": "Error generating pipeline"}, status=500)
892895

896+
@superuser_required
897+
def generate_mesh_status(request, pk):
898+
scene = get_object_or_404(Scene, pk=pk)
899+
request_id = request.GET.get("request_id")
900+
if not request_id:
901+
return JsonResponse({"success": False, "error": "missing request_id"}, status=400)
902+
903+
try:
904+
from .mesh_generator import MeshGenerator
905+
mesh_generator = MeshGenerator()
906+
907+
status_data = mesh_generator.mapping_client.getReconstructionStatus(request_id)
908+
909+
# If mapping service couldn't find it / errored, just return it
910+
if not status_data.get("success"):
911+
return JsonResponse(status_data, status=200)
912+
913+
state = status_data.get("state")
914+
915+
if state != "complete":
916+
return JsonResponse(status_data, status=200)
917+
918+
with transaction.atomic():
919+
scene = Scene.objects.select_for_update().get(pk=scene.pk)
920+
921+
if hasattr(scene, "mesh_state") and scene.mesh_state == "complete":
922+
status_data["finalized"] = True
923+
return JsonResponse(status_data, status=200)
924+
finalize_result = mesh_generator.finalizeMeshFromStatus(scene, request_id)
925+
926+
if not finalize_result.get("success"):
927+
if hasattr(scene, "mesh_state"):
928+
scene.mesh_state = "failed"
929+
scene.save(update_fields=["mesh_state"])
930+
return JsonResponse(finalize_result, status=500)
931+
932+
if hasattr(scene, "mesh_state"):
933+
scene.mesh_state = "complete"
934+
scene.save(update_fields=["mesh_state"])
935+
936+
status_data["finalized"] = True
937+
return JsonResponse(status_data, status=200)
938+
939+
except Exception as e:
940+
log.error(f"Mesh status error: {e}")
941+
log.error(f"Traceback: {traceback.format_exc()}")
942+
return JsonResponse({
943+
"success": False,
944+
"error": "An internal error occurred while getting mesh status",
945+
}, status=500)
946+
893947
@superuser_required
894948
def generate_mesh(request, pk):
895949
"""Generate 3D mesh from scene cameras using mapping service."""
@@ -899,39 +953,37 @@ def generate_mesh(request, pk):
899953
try:
900954
from .mesh_generator import MeshGenerator
901955

902-
# Get request parameters
903-
request_data = json.loads(request.body.decode('utf-8'))
904-
mesh_type = request_data.get('mesh_type', 'mesh')
905-
906956
# Get scene object
907957
scene = get_object_or_404(Scene, pk=pk)
908958

909959
# Initialize mesh generator
960+
mesh_type = request.POST.get("mesh_type", "mesh")
961+
uploaded_map = request.FILES.get("map", None)
910962
mesh_generator = MeshGenerator()
911963

912964
# Generate mesh
913-
result = mesh_generator.generateMeshFromScene(scene, mesh_type)
914-
915-
if result['success']:
965+
result = mesh_generator.startMeshGeneration(scene, mesh_type, uploaded_map=uploaded_map)
966+
if result.get("success"):
916967
return JsonResponse({
917968
"success": True,
918969
"message": "Mesh generated successfully",
919-
"processing_time": result.get('processing_time', 0)
970+
"request_id": result["request_id"],
971+
"processing_time": result.get("processing_time", 0),
920972
})
921-
else:
922-
return JsonResponse({
923-
"success": False,
924-
"error": result.get('error', 'Unknown error occurred while generating mesh'),
925-
"processing_time": result.get('processing_time', 0)
926-
}, status=400)
973+
974+
return JsonResponse({
975+
"success": False,
976+
"error": result.get("error", "Unknown error occurred while generating mesh"),
977+
"processing_time": result.get("processing_time", 0),
978+
}, status=400)
927979

928980
except Exception as e:
929981
log.error(f"Mesh generation error: {e}")
930982
import traceback
931983
log.error(f"Traceback: {traceback.format_exc()}")
932984
return JsonResponse({
933985
"success": False,
934-
"error": f"An internal error occurred while generating mesh"
986+
"error": "An internal error occurred while generating mesh",
935987
}, status=500)
936988

937989
@superuser_required

manager/src/static/js/sscape.js

Lines changed: 127 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,88 +1466,158 @@ function setupSceneRotationTranslationFields(event = null) {
14661466

14671467
function setupGenerateMesh() {
14681468
const generateMeshButton = document.getElementById("generate_mesh");
1469+
const saveButton = document.getElementById("save");
1470+
const mapInput = document.getElementById("id_map");
1471+
1472+
saveButton?.addEventListener("click", async (e) => {
1473+
const allowedExtensions = ["mp4", "mov", "avi", "webm", "mkv"];
1474+
1475+
const file = mapInput?.files?.[0];
1476+
if (!file) {
1477+
// No file selected; nothing to validate here.
1478+
return;
1479+
}
1480+
const extension = file.name.split(".").pop().toLowerCase();
1481+
1482+
const isVideoMime = file.type.startsWith("video/");
1483+
const isVideoExt = allowedExtensions.includes(extension);
1484+
1485+
if (isVideoMime && isVideoExt) {
1486+
e.preventDefault();
1487+
alert("Please click generate mesh when uploading video file.");
1488+
return;
1489+
}
1490+
});
1491+
14691492
if (!generateMeshButton) return;
14701493

14711494
// Start monitoring mapping service status
14721495
startMappingServiceStatusMonitoring();
14731496

1474-
generateMeshButton.addEventListener("click", async function () {
1497+
generateMeshButton?.addEventListener("click", async (e) => {
1498+
e.preventDefault();
1499+
14751500
const sceneId = document.getElementById("sceneUID")?.value;
1476-
if (!sceneId) {
1477-
alert("Scene ID not found");
1478-
return;
1479-
}
1501+
const form = document.getElementById("scene_update_form");
1502+
1503+
if (!sceneId) return alert("Scene ID not found");
1504+
if (!form) return alert("Form not found");
14801505

14811506
// Show loading state
14821507
const spinner = document.getElementById("mesh_spinner");
1483-
spinner.classList.remove("d-none");
1508+
1509+
spinner?.classList.remove("d-none");
1510+
generateMeshButton.dataset.meshRunning = "1";
14841511
generateMeshButton.disabled = true;
14851512

14861513
try {
1487-
await generateMeshFromCameras(sceneId);
1488-
} catch (error) {
1489-
console.error("Mesh generation failed:", error);
1490-
alert("Mesh generation failed: " + error.message);
1514+
const startResult = await generateMeshFromCameras(sceneId, form);
1515+
1516+
const requestId = startResult.request_id;
1517+
if (!requestId) {
1518+
throw new Error("Backend did not return request_id");
1519+
}
1520+
1521+
await pollMeshStatus(sceneId, requestId);
1522+
1523+
alert("Mesh generated successfully! The scene map has been updated.");
1524+
1525+
$("#id_rotation_x").val(0);
1526+
$("#id_rotation_y").val(0);
1527+
$("#id_rotation_z").val(0);
1528+
$("#id_translation_x").val(0);
1529+
$("#id_translation_y").val(0);
1530+
$("#id_translation_z").val(0);
1531+
window.location.reload();
1532+
} catch (err) {
1533+
console.error(err);
1534+
alert("Mesh generation failed: " + (err?.message ?? String(err)));
14911535
} finally {
14921536
// Hide loading state
1493-
spinner.classList.add("d-none");
1537+
spinner?.classList.add("d-none");
1538+
generateMeshButton.dataset.meshRunning = "0";
14941539
generateMeshButton.disabled = false;
14951540
}
14961541
});
14971542
}
14981543

1499-
async function generateMeshFromCameras(sceneId) {
1500-
const tokenElement = document.getElementById("auth-token");
1501-
if (!tokenElement) {
1502-
throw new Error("Authentication token not found");
1503-
}
1544+
async function pollMeshStatus(sceneId, requestId) {
1545+
const timeout = 10 * 60 * 1000; // 10 minutes
1546+
const start = Date.now();
15041547

1505-
const authToken = `Token ${tokenElement.value}`;
1506-
try {
1507-
const response = await fetch(`/scene/generate-mesh/${sceneId}/`, {
1508-
method: "POST",
1509-
headers: {
1510-
"Content-Type": "application/json",
1511-
Authorization: authToken,
1512-
"X-CSRFToken": document.querySelector("[name=csrfmiddlewaretoken]")
1513-
?.value,
1514-
},
1515-
body: JSON.stringify({
1516-
mesh_type: "mesh",
1517-
}),
1518-
});
1548+
while (true) {
1549+
if (Date.now() - start > timeout) {
1550+
throw new Error("Timed out waiting for mesh generation.");
1551+
}
15191552

1520-
// Log response for debugging
1521-
console.log("Response status:", response.status);
1522-
console.log("Response headers:", response.headers);
1553+
const resp = await fetch(
1554+
`/scene/generate-mesh-status/${sceneId}/?request_id=${encodeURIComponent(requestId)}`,
1555+
);
15231556

1524-
if (!response.ok) {
1525-
const errorText = await response.text();
1526-
console.log("Error response text:", errorText);
1527-
throw new Error(`HTTP ${response.status}: ${errorText}`);
1557+
const data = await resp.json();
1558+
1559+
if (!resp.ok) {
1560+
throw new Error(data?.error || "Status check failed");
15281561
}
15291562

1530-
const result = await response.json();
1563+
if (data.success === false) {
1564+
throw new Error(data?.error || "Mesh generation failed");
1565+
}
15311566

1532-
if (result.success) {
1533-
alert("Mesh generated successfully! The scene map has been updated.");
1534-
// Optionally reload the page to show the updated map
1535-
// window.location.reload();
1536-
// Set rotation and translation fields to zero
1537-
$("#id_rotation_x").val(0);
1538-
$("#id_rotation_y").val(0);
1539-
$("#id_rotation_z").val(0);
1540-
$("#id_translation_x").val(0);
1541-
$("#id_translation_y").val(0);
1542-
$("#id_translation_z").val(0);
1543-
} else {
1544-
throw new Error(result.message || "Mesh generation failed");
1567+
if (data.state === "complete") {
1568+
return data;
15451569
}
1546-
} catch (error) {
1547-
throw error;
1570+
1571+
if (data.state === "failed") {
1572+
throw new Error(data.error || "Mesh generation failed");
1573+
}
1574+
1575+
// Wait before next poll
1576+
await new Promise((r) => setTimeout(r, 1500));
15481577
}
15491578
}
15501579

1580+
async function generateMeshFromCameras(sceneId, form) {
1581+
const url = `/scene/generate-mesh/${sceneId}/`;
1582+
1583+
const formData = new FormData(form);
1584+
1585+
// Make sure CSRF is sent (Django)
1586+
const csrfToken =
1587+
document.querySelector('input[name="csrfmiddlewaretoken"]')?.value ||
1588+
getCookie("csrftoken");
1589+
1590+
const resp = await fetch(url, {
1591+
method: "POST",
1592+
headers: {
1593+
"X-CSRFToken": csrfToken,
1594+
Accept: "application/json",
1595+
},
1596+
body: formData,
1597+
});
1598+
1599+
const data = await resp.json().catch(() => ({}));
1600+
1601+
if (!resp.ok || data.success === false) {
1602+
throw new Error(
1603+
data?.error || `Generate mesh failed (HTTP ${resp.status})`,
1604+
);
1605+
}
1606+
1607+
// expects { success: true, request_id: "..." }
1608+
if (!data.request_id) {
1609+
throw new Error("Generate mesh response missing request_id");
1610+
}
1611+
1612+
return data;
1613+
}
1614+
1615+
// Optional cookie helper if you don't already have one:
1616+
function getCookie(name) {
1617+
const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
1618+
return match ? decodeURIComponent(match[2]) : null;
1619+
}
1620+
15511621
async function checkMappingServiceStatus() {
15521622
const generateMeshButton = document.getElementById("generate_mesh");
15531623
if (!generateMeshButton) return;
@@ -1577,7 +1647,10 @@ async function checkMappingServiceStatus() {
15771647
if (status.available) {
15781648
// Service is available, show the button
15791649
generateMeshButton.style.display = "inline-block";
1580-
generateMeshButton.disabled = false;
1650+
const running = generateMeshButton.dataset.meshRunning === "1";
1651+
if (!running) {
1652+
generateMeshButton.disabled = false;
1653+
}
15811654
generateMeshButton.title =
15821655
"Generate 3D mesh from camera images using mapping service";
15831656

0 commit comments

Comments
 (0)