From 3ae55bc8a7a6d432ee3309a72e0bfe8dbc034dc9 Mon Sep 17 00:00:00 2001
From: Pablo Silva
- Created by {{ exhibit.owner.user.username }}
- {{ exhibit.creation_date }}
- {{ exhibit.artworks_count }} Artwork(s)
-
+ {{ _("Created by ") }}{{ exhibit.owner.user.username }}
+ {{ exhibit.date }}
+ {{ exhibit.artworks_count }} {{ _("Artwork(s)") }}
+ ' + escapeHtml(message) + '
- {{ _("Welcome, ") }}{{ request.user.username }}
- {{ _("Choose from your repository") }} {{ artwork.description }} {{ artwork.description }}
{{ "{} {}{} {}".format(_("Created by") , artwork.author.user.username,_(", in"), artwork.created.strftime("%d/%m/%Y")) }}
{{ artwork.used_in_html_string() | safe }}
{{ "{} {}{} {}".format(_("Uploaded by") , marker.owner.user.username,_(", in"), marker.created.strftime("%d/%m/%Y")) }}
{{ marker.used_in_html_string() | safe }} Download file:
{{ "{} ({})".format(_("Download File") , marker.file_size | filesizeformat)}}
{{ "{} {}{} {}".format(_("Uploaded by") , ar_object.owner.user.username,_(", in"), ar_object.created.strftime("%d/%m/%Y")) }}
@@ -33,7 +33,6 @@
{% endif %}
- Download file:
{{ "{} {}{} {}".format(_("Uploaded by") , sound.owner.user.username,_(", in"), sound.created.strftime("%d/%m/%Y")) }}
{{ sound.used_in_html_string() | safe }} Download file:
{{ "{} ({})".format(_("Download File") , sound.file_size | filesizeformat)}}
{{ "{} {}{} {}".format(_("Uploaded by") , ar_object.owner.user.username,_(", in"), ar_object.created.strftime("%d/%m/%Y")) }}
{{ ar_object.used_in_html_string() | safe }} {{ _("Sound:") }} {{ _("To see the Artworks you need to grant camera access to the app.") }} {{ _("No vídeo abaixo, apresentamos o que é a Jandig, o conceito da plataforma e suas possibilidades.") }}
+ {{ _("Se você se identificou com a Jandig e acredita que a plataforma é útil para você criar seu projeto com realidade aumentada (seja ele uma exposição, cartaz, adesivo, mural, livro ou o que mais sua imaginação puder criar), deixamos para você um passo a passo de como utilizá-la. Ele explica o processo para subir seu conteúdo ou remixar o conteúdo já existente na plataforma.") }}
+
+ {{ _("Para acompanhar o passo a passo e colocar em prática,") }}
+ {{ _("faça seu cadastro") }}
+ {{ _("antes de começar a assistir. Depois, volte aqui e dê play no vídeo.
+ Se você preferir, também pode acessar o") }}
+ {{ _("passo a passo por texto.") }}
+ {{ _("Além disso, no site da Jandig, na área de") }}
+ {{ _("Ajuda") }},
+ {{ _("há outros tutoriais que podem ser úteis. Não deixe de consultá-los se sentir que é necessário.") }}
+
+ {{ _("Esperamos ver suas obras e exposições na Jandig! Se você criar algo, não deixe de compartilhar conosco. Temos um") }}
+ {{ _("canal no Telegram") }}
+ {{ _("voltado para a comunidade, onde é possível tirar dúvidas e compartilhar suas criações e experiências.") }}
+
-
-
{{ exhibit.name }}
-
- {{ exhibit.name }}
+
+
-
-
+{% if editable %}
+ {% set can_edit = not marker.is_used_by_other_user() %}
+ {% set can_delete = not marker.in_use %}
+ {% set edit_reason = _("This marker is used by other users and cannot be edited.") %}
+ {% set delete_reason = _("This marker is used in artworks and cannot be deleted.") %}
+ {% with element=marker, element_type="marker", has_preview=true, can_edit=can_edit, can_delete=can_delete, edit_reason=edit_reason, delete_reason=delete_reason %}
+ {% include "core/components/action-menu.jinja2" %}
+ {% endwith %}
+{% endif %}
diff --git a/src/core/jinja2/core/templates/object_thumbnail.jinja2 b/src/core/jinja2/core/templates/object_thumbnail.jinja2
index 3d2a29ffa..576e80804 100644
--- a/src/core/jinja2/core/templates/object_thumbnail.jinja2
+++ b/src/core/jinja2/core/templates/object_thumbnail.jinja2
@@ -1,17 +1,40 @@
-
- {% if editable %}edit{% endif %}
- {% if deletable %}
- delete
+{% if object.is_video %}
+
+ {% else %}
+
+ {% endif %}
+ {% else %}
+
+ {% endif %}
+ {% if editable %}
+ {% set can_edit = not object.is_used_by_other_user() %}
+ {% set can_delete = not object.in_use %}
+ {% set edit_reason = _("This object is used by other users and cannot be edited.") %}
+ {% set delete_reason = _("This object is used in artworks and cannot be deleted.") %}
+ {% with element=object, element_type="object", has_preview=false, can_edit=can_edit, can_delete=can_delete, edit_reason=edit_reason, delete_reason=delete_reason %}
+ {% include "core/components/action-menu.jinja2" %}
+ {% endwith %}
{% endif %}
-
`OekSvVRc=W1j;EtPpmi
z5r&2asI*#$V`5m{&&hxnEiJI#zCA-Al8XocPfyRsu_Do5Uw3zx2d@2EQu5nqd;9)K
zj
S~6t_|aRO-_U5}
zz`L&S6FogW5&LWBeZ0NJ>d(LWLd`2Ez>{a6#^c7{zLmf9OcsGabcXYeu5xvXh8&5+
zUX9eWG;uz90(W|Ue?L7pH?6*2lkXv=WZIi1rEJTubtrp2R%PSYG@bhJp~1%dv)qLR
z52yKp-&}MJ4DbpdR#i}_#7NQc2j_x=gKZg395awvwsv+X#xRIF&`PI*xYQ)!c+uFk
zsmAEtWq9P~`#=8e@8{Pi*yU+HAi><++|*8-=;v_m7WyAoQo8406BZIeZEQTr;cz1E
z)h7x}xf2uldKd{9>_uOa_=oj%+RV&F7##i=y4lFs_{zw=)1A@Vs&C40;x;xm
-
+
+
Date: Fri, 3 Jul 2026 12:37:15 +0100
Subject: [PATCH 02/11] Refactor profile page and modal pages CSS for new
thumbnails
---
.../jinja2/core/components/action-menu.jinja2 | 2 +-
src/core/jinja2/core/exhibit_create_ar.jinja2 | 1 +
src/core/jinja2/core/exhibit_create_mr.jinja2 | 1 +
.../core/templates/artwork_thumbnail.jinja2 | 23 ++--
.../core/templates/marker_thumbnail.jinja2 | 5 +-
.../core/templates/object_thumbnail.jinja2 | 19 +--
.../core/templates/sound_thumbnail.jinja2 | 2 +-
src/core/jinja2/core/upload-artwork.jinja2 | 1 +
src/core/static/css/action-menu.css | 10 +-
src/core/static/css/generator.css | 2 +-
src/core/static/css/marker-creation.css | 6 +
src/core/static/css/repository-list.css | 115 ++++++++++++++++--
src/core/static/css/reset.css | 62 +++++-----
src/users/static/css/profile.css | 24 ++--
14 files changed, 192 insertions(+), 81 deletions(-)
diff --git a/src/core/jinja2/core/components/action-menu.jinja2 b/src/core/jinja2/core/components/action-menu.jinja2
index c2912350c..c608eb873 100644
--- a/src/core/jinja2/core/components/action-menu.jinja2
+++ b/src/core/jinja2/core/components/action-menu.jinja2
@@ -12,7 +12,7 @@
diff --git a/src/core/jinja2/core/exhibit_create_ar.jinja2 b/src/core/jinja2/core/exhibit_create_ar.jinja2
index fc3b0a3c1..cdbc872ab 100644
--- a/src/core/jinja2/core/exhibit_create_ar.jinja2
+++ b/src/core/jinja2/core/exhibit_create_ar.jinja2
@@ -3,6 +3,7 @@
+ height="80"
+ width="80">
{% if artwork.augmented.is_video %}
+ height="80"
+ width="80">
{% else %}
+ src="data:image/svg+xml;utf8,
+ height="80"
+ width="80">
{% endif %}
{% if editable %}
{% set can_edit = true %}
diff --git a/src/core/jinja2/core/templates/marker_thumbnail.jinja2 b/src/core/jinja2/core/templates/marker_thumbnail.jinja2
index a697ff627..245bf0de6 100644
--- a/src/core/jinja2/core/templates/marker_thumbnail.jinja2
+++ b/src/core/jinja2/core/templates/marker_thumbnail.jinja2
@@ -1,8 +1,9 @@
+{{ marker.title }}
+ height="128"
+ width="128">
{% if editable %}
{% set can_edit = not marker.is_used_by_other_user() %}
{% set can_delete = not marker.in_use %}
diff --git a/src/core/jinja2/core/templates/object_thumbnail.jinja2 b/src/core/jinja2/core/templates/object_thumbnail.jinja2
index 576e80804..f34656d40 100644
--- a/src/core/jinja2/core/templates/object_thumbnail.jinja2
+++ b/src/core/jinja2/core/templates/object_thumbnail.jinja2
@@ -1,3 +1,4 @@
+{{ object.title }}
{% if object.is_video %}
+ height="128"
+ width="128">
{% else %}
+ src="data:image/svg+xml;utf8,
+ height="128"
+ width="128">
{% endif %}
{% if editable %}
{% set can_edit = not object.is_used_by_other_user() %}
diff --git a/src/core/jinja2/core/templates/sound_thumbnail.jinja2 b/src/core/jinja2/core/templates/sound_thumbnail.jinja2
index 2cb881009..023ca0f62 100644
--- a/src/core/jinja2/core/templates/sound_thumbnail.jinja2
+++ b/src/core/jinja2/core/templates/sound_thumbnail.jinja2
@@ -1,4 +1,4 @@
-{{ sound.title }}
+{{ sound.title }}
{{ artworks_qty }} {{ _(" Artworks") }}
{% else %}
-
{{ exhibits_qty }} {{ _(" Exhibits") }}
{% else %}
- {{ _("Select Artworks") }}
{% if artworks %}
- {{ _("Select Objects") }}
-
{% with repository_list = objects, element_type="object", htmx="false" %}
{% include "core/components/item-list.jinja2" %}
@@ -74,14 +68,8 @@
onclick="validateSubmit()">{{ _("Okay") }}
{{ _("Select Sounds") }}
-
{% with repository_list = sounds, element_type="sound", htmx="false" %}
{% include "core/components/item-list.jinja2" %}
diff --git a/src/core/jinja2/core/templates/artwork_modal.jinja2 b/src/core/jinja2/core/templates/artwork_modal.jinja2
index 712a4c137..1696cd4c3 100644
--- a/src/core/jinja2/core/templates/artwork_modal.jinja2
+++ b/src/core/jinja2/core/templates/artwork_modal.jinja2
@@ -3,9 +3,11 @@
{{ artwork.title }}
{{ artwork.title }}
- {{ marker.title }}
{{ marker.title }}
{{ ar_object.title }}
{{ ar_object.title }}
{{ sound.title }}
{{ sound.title }}
{{ _("Select Marker") }}
-
{% with repository_list = marker_list, element_type = 'marker', htmx="false" %}
{% include "core/components/item-list.jinja2" %}
@@ -88,14 +82,8 @@
onclick="validateSubmit()">{{ _("Okay") }}
{{ _("Select Object") }}
-
{% with repository_list = object_list, element_type = 'object', htmx="false" %}
{% include "core/components/item-list.jinja2" %}
diff --git a/src/core/static/css/generic-modal.css b/src/core/static/css/generic-modal.css
index 339f079df..d338fa582 100644
--- a/src/core/static/css/generic-modal.css
+++ b/src/core/static/css/generic-modal.css
@@ -26,6 +26,7 @@
margin: 0;
text-align: center;
background: #FFFFFF00;
+ font-size: 1.5em;
}
.modal-content {
@@ -72,22 +73,33 @@
}
#modal-header {
- display:flex;
- flex-direction: row-reverse;
+ display: flex;
+ align-items: center;
+ justify-content: center;
padding: 10px;
- justify-content: space-between;
+ position: relative;
+}
+
+#modal-header #modal-title {
+ margin: 0;
+ text-align: center;
+ flex: 1;
}
#back-modal{
font-size: larger;
margin: 0;
width: 2.5em;
+ position: absolute;
+ left: 10px;
}
#close-modal{
font-size: larger;
width: 2.5em;
margin: 0;
+ position: absolute;
+ right: 10px;
}
#artwork-description{
diff --git a/src/core/static/css/marker-creation.css b/src/core/static/css/marker-creation.css
index dbfa4e187..d8fbee69a 100644
--- a/src/core/static/css/marker-creation.css
+++ b/src/core/static/css/marker-creation.css
@@ -11,6 +11,10 @@
flex: 0 1 calc(20% - 16px);
}
+.modal.modal #repo-artwork .repository-item {
+ flex: 0 1 calc(50% - 10px);
+}
+
.form-options{
font-size: 12px;
text-align: left;
@@ -29,4 +33,27 @@
.tab {
display: none;
+}
+
+.modal-nav-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ padding: 10px 0;
+}
+
+.modal-nav-header .modal-title {
+ flex: 1;
+ text-align: center;
+ margin: 0;
+ font-size: 1.4em;
+}
+
+.modal-nav-btn {
+ width: 2.5em;
+ height: 2.5em;
+ font-size: larger;
+ margin: 0;
+ flex-shrink: 0;
}
\ No newline at end of file
diff --git a/src/core/tests/test_get_elements.py b/src/core/tests/test_get_elements.py
index 50a87b643..0d5ac82a5 100644
--- a/src/core/tests/test_get_elements.py
+++ b/src/core/tests/test_get_elements.py
@@ -133,7 +133,7 @@ def test_object_pagination_first_page(self):
"""Test object pagination on first page"""
self.client.login(username="testuser", password="testpass123")
- with patch.object(settings, "MODAL_PAGE_SIZE", 8):
+ with patch.object(settings, "OBJECT_MODAL_PAGE_SIZE", 8):
response = self.client.get(
reverse("get-element"),
{"element_type": "object", "page": "1"},
From 245790ed5385e21811b65ecfa8bfdbe5975d91ea Mon Sep 17 00:00:00 2001
From: Pablo Silva
{{ _("Edit object") }}
{% else %}
@@ -290,119 +290,27 @@
controlsInfo.style.cssText = "margin: 10px 0; padding: 10px; background: #e8f4fd; border-radius: 5px; font-size: 14px;";
controlsInfo.innerHTML = "3D Controls: Left click + drag to rotate, Right click + drag to pan, Scroll to zoom";
document.getElementById("content-box").appendChild(controlsInfo);
-
- // THREE.js is already loaded globally, just import GLTFLoader and OrbitControls
- Promise.all([
- import('https://unpkg.com/three@0.127.0/examples/jsm/loaders/GLTFLoader.js'),
- import('https://unpkg.com/three@0.127.0/examples/jsm/controls/OrbitControls.js')
- ]).then(([{ GLTFLoader }, { OrbitControls }]) => {
- const scene = new THREE.Scene();
- scene.background = new THREE.Color(0xf0f0f0); // Light gray background
-
- const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
- camera.position.set(0, 0, 5);
-
- const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
- renderer.setSize(600, 600);
- renderer.shadowMap.enabled = false;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
- document.getElementById("content-box").appendChild(renderer.domElement);
- document.getElementById("content-box").appendChild(blockquote);
-
- // Add OrbitControls for camera interaction
- const controls = new OrbitControls(camera, renderer.domElement);
- controls.enableDamping = true; // Smooth camera movement
- controls.dampingFactor = 0.05;
- controls.enableZoom = true;
- controls.enablePan = true;
- controls.enableRotate = true;
- controls.autoRotate = false;
-
- // Add lighting
- const ambientLight = new THREE.AmbientLight(0xFFFDD0, 3.5); // Brighter ambient light
- scene.add(ambientLight);
-
-
- // Animation variables
- let mixer = null;
- const clock = new THREE.Clock();
-
- // Animation loop for smooth controls and animations
- function animate() {
- requestAnimationFrame(animate);
-
- const delta = clock.getDelta();
- if (mixer) mixer.update(delta);
-
- controls.update(); // Update controls for damping
- renderer.render(scene, camera);
- }
-
- // Load the GLB file
- const loader = new GLTFLoader();
- loader.load(
- URL.createObjectURL(file),
- function (gltf) {
- scene.add(gltf.scene);
-
- // Handle animations if they exist
- if (gltf.animations && gltf.animations.length > 0) {
- mixer = new THREE.AnimationMixer(gltf.scene);
-
- // Play all animations
- gltf.animations.forEach((clip) => {
- const action = mixer.clipAction(clip);
- action.play();
- });
-
- // Add animation info
- const animationInfo = document.createElement("div");
- animationInfo.style.cssText = "margin: 10px 0; padding: 10px; background: #d4edda; border-radius: 5px; font-size: 14px;";
- animationInfo.innerHTML = `Animations: ${gltf.animations.length} animation(s) found and playing`;
- document.getElementById("content-box").appendChild(animationInfo);
- }
-
- // Auto-scale and center the model
- const box = new THREE.Box3().setFromObject(gltf.scene);
- const center = box.getCenter(new THREE.Vector3());
- const size = box.getSize(new THREE.Vector3());
-
- // Center the model
- gltf.scene.position.sub(center);
-
- // Scale to fit in view (make it bigger)
- const maxDim = Math.max(size.x, size.y, size.z);
- const scale = 5 / maxDim; // Make it take up more of the view
- gltf.scene.scale.setScalar(scale);
-
- // Position camera at optimal distance
- const distance = Math.max(3, maxDim * 2);
- camera.position.set(distance, distance * 0.5, distance);
- camera.lookAt(0, 0, 0);
-
- // Update controls target to model center
- controls.target.set(0, 0, 0);
- controls.update();
-
- // Start animation loop
- animate();
-
- // Auto-generate thumbnail after model is loaded and positioned
- setTimeout(() => {
- generateThumbnail(renderer, scene, camera);
- }, 1000); // Wait 1 second for the model to be properly positioned
- },
- function (progress) {
- console.log('Loading progress:', progress);
- },
- function (error) {
- console.error('Error loading GLB file:', error);
- }
- );
- })
- .catch(error => {
- console.error('Error loading GLTFLoader or OrbitControls:', error);
- });
+
+ // Create container for 3D viewer
+ var viewerContainer = document.createElement("div");
+ viewerContainer.style.cssText = "width: 600px; height: 600px; max-width: 100%; margin: 0 auto;";
+ document.getElementById("content-box").appendChild(viewerContainer);
+ document.getElementById("content-box").appendChild(blockquote);
+
+ GLBViewer.init(viewerContainer, URL.createObjectURL(file), {
+ onAnimationsFound: function(count) {
+ const animationInfo = document.createElement("div");
+ animationInfo.style.cssText = "margin: 10px 0; padding: 10px; background: #d4edda; border-radius: 5px; font-size: 14px;";
+ animationInfo.innerHTML = `Animations: ${count} animation(s) found and playing`;
+ document.getElementById("content-box").appendChild(animationInfo);
+ },
+ onThumbnailReady: function(renderer, scene, camera) {
+ generateThumbnail(renderer, scene, camera);
+ },
+ onError: function(error) {
+ console.error('Error loading GLB file:', error);
+ }
+ });
} else {
console.log("Invalid file type: " + file.type);
alert("{{ _("Invalid file type! Only image, video or GLB files are accepted.") }}")
diff --git a/src/core/static/js/glb-viewer.js b/src/core/static/js/glb-viewer.js
new file mode 100644
index 000000000..49cc134c9
--- /dev/null
+++ b/src/core/static/js/glb-viewer.js
@@ -0,0 +1,159 @@
+/**
+ * Shared GLB 3D model viewer using Three.js.
+ *
+ * Usage:
+ * var viewer = GLBViewer.init(container, sourceUrl, options);
+ * // Later, to clean up:
+ * viewer.dispose();
+ *
+ * Options:
+ * - onThumbnailReady(blob, renderer, scene, camera): called after model loads (for thumbnail generation)
+ * - onAnimationsFound(count): called when animations are detected
+ * - onError(error): called on load failure
+ */
+var GLBViewer = (function () {
+ var THREE_VERSION = '0.127.0';
+ var THREE_BASE = 'https://unpkg.com/three@' + THREE_VERSION;
+
+ function loadThree() {
+ if (window.THREE) return Promise.resolve();
+ return new Promise(function (resolve, reject) {
+ var script = document.createElement('script');
+ script.src = THREE_BASE + '/build/three.min.js';
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+
+ function init(container, sourceUrl, options) {
+ options = options || {};
+ var renderer = null;
+ var animationFrameId = null;
+ var disposed = false;
+
+ function dispose() {
+ if (disposed) return;
+ disposed = true;
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ if (renderer) {
+ renderer.dispose();
+ renderer.forceContextLoss();
+ renderer = null;
+ }
+ }
+
+ loadThree().then(function () {
+ if (disposed) return;
+ return Promise.all([
+ import(THREE_BASE + '/examples/jsm/loaders/GLTFLoader.js'),
+ import(THREE_BASE + '/examples/jsm/controls/OrbitControls.js')
+ ]);
+ }).then(function (modules) {
+ if (disposed) return;
+ var GLTFLoader = modules[0].GLTFLoader;
+ var OrbitControls = modules[1].OrbitControls;
+
+ var width = container.clientWidth || 600;
+ var height = container.clientHeight || width;
+
+ var scene = new THREE.Scene();
+ scene.background = new THREE.Color(0xf0f0f0);
+
+ var camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
+ camera.position.set(0, 0, 5);
+
+ renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(width, height);
+ container.appendChild(renderer.domElement);
+
+ var controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.05;
+ controls.enableZoom = true;
+ controls.enablePan = true;
+ controls.enableRotate = true;
+
+ var ambientLight = new THREE.AmbientLight(0xFFFDD0, 3.5);
+ scene.add(ambientLight);
+
+ var mixer = null;
+ var clock = new THREE.Clock();
+
+ function animate() {
+ if (disposed) return;
+ animationFrameId = requestAnimationFrame(animate);
+ var delta = clock.getDelta();
+ if (mixer) mixer.update(delta);
+ controls.update();
+ renderer.render(scene, camera);
+ }
+
+ var loader = new GLTFLoader();
+ loader.load(
+ sourceUrl,
+ function (gltf) {
+ if (disposed) return;
+ scene.add(gltf.scene);
+
+ if (gltf.animations && gltf.animations.length > 0) {
+ mixer = new THREE.AnimationMixer(gltf.scene);
+ gltf.animations.forEach(function (clip) {
+ mixer.clipAction(clip).play();
+ });
+ if (options.onAnimationsFound) {
+ options.onAnimationsFound(gltf.animations.length);
+ }
+ }
+
+ // Auto-scale and center the model
+ var box = new THREE.Box3().setFromObject(gltf.scene);
+ var center = box.getCenter(new THREE.Vector3());
+ var size = box.getSize(new THREE.Vector3());
+
+ gltf.scene.position.sub(center);
+
+ var maxDim = Math.max(size.x, size.y, size.z);
+ var scale = 5 / maxDim;
+ gltf.scene.scale.setScalar(scale);
+
+ var distance = Math.max(3, maxDim * 2);
+ camera.position.set(distance, distance * 0.5, distance);
+ camera.lookAt(0, 0, 0);
+
+ controls.target.set(0, 0, 0);
+ controls.update();
+
+ animate();
+
+ if (options.onThumbnailReady) {
+ setTimeout(function () {
+ if (disposed) return;
+ renderer.render(scene, camera);
+ options.onThumbnailReady(renderer, scene, camera);
+ }, 1000);
+ }
+ },
+ undefined,
+ function (error) {
+ console.error('Error loading GLB file:', error);
+ if (options.onError) {
+ options.onError(error);
+ }
+ }
+ );
+ }).catch(function (error) {
+ console.error('Error initializing GLB viewer:', error);
+ if (options.onError) {
+ options.onError(error);
+ }
+ });
+
+ return { dispose: dispose };
+ }
+
+ return { init: init };
+})();
From 6e936e576c53d5529fb719dc31cba8c8563a5c69 Mon Sep 17 00:00:00 2001
From: Pablo Silva {{ _("Edit object") }}
{% else %}
@@ -297,19 +295,24 @@
document.getElementById("content-box").appendChild(viewerContainer);
document.getElementById("content-box").appendChild(blockquote);
- GLBViewer.init(viewerContainer, URL.createObjectURL(file), {
- onAnimationsFound: function(count) {
- const animationInfo = document.createElement("div");
- animationInfo.style.cssText = "margin: 10px 0; padding: 10px; background: #d4edda; border-radius: 5px; font-size: 14px;";
- animationInfo.innerHTML = `Animations: ${count} animation(s) found and playing`;
- document.getElementById("content-box").appendChild(animationInfo);
- },
- onThumbnailReady: function(renderer, scene, camera) {
- generateThumbnail(renderer, scene, camera);
- },
- onError: function(error) {
- console.error('Error loading GLB file:', error);
- }
+ (window.GLBViewer
+ ? Promise.resolve(window.GLBViewer)
+ : import("{{ static('js/glb-viewer.js') }}").then(function(m) { window.GLBViewer = m.GLBViewer; return m.GLBViewer; })
+ ).then(function(GLBViewer) {
+ GLBViewer.init(viewerContainer, URL.createObjectURL(file), {
+ onAnimationsFound: function(count) {
+ const animationInfo = document.createElement("div");
+ animationInfo.style.cssText = "margin: 10px 0; padding: 10px; background: #d4edda; border-radius: 5px; font-size: 14px;";
+ animationInfo.innerHTML = `Animations: ${count} animation(s) found and playing`;
+ document.getElementById("content-box").appendChild(animationInfo);
+ },
+ onThumbnailReady: function(renderer, scene, camera) {
+ generateThumbnail(renderer, scene, camera);
+ },
+ onError: function(error) {
+ console.error('Error loading GLB file:', error);
+ }
+ });
});
} else {
console.log("Invalid file type: " + file.type);
diff --git a/src/core/static/js/glb-viewer.js b/src/core/static/js/glb-viewer.js
index 49cc134c9..3819ffc21 100644
--- a/src/core/static/js/glb-viewer.js
+++ b/src/core/static/js/glb-viewer.js
@@ -1,159 +1,132 @@
/**
- * Shared GLB 3D model viewer using Three.js.
+ * Shared GLB 3D model viewer using Three.js (ESM, r184).
*
* Usage:
+ * import { GLBViewer } from './glb-viewer.js';
* var viewer = GLBViewer.init(container, sourceUrl, options);
* // Later, to clean up:
* viewer.dispose();
*
* Options:
- * - onThumbnailReady(blob, renderer, scene, camera): called after model loads (for thumbnail generation)
+ * - onThumbnailReady(renderer, scene, camera): called after model loads (for thumbnail generation)
* - onAnimationsFound(count): called when animations are detected
* - onError(error): called on load failure
*/
-var GLBViewer = (function () {
- var THREE_VERSION = '0.127.0';
- var THREE_BASE = 'https://unpkg.com/three@' + THREE_VERSION;
-
- function loadThree() {
- if (window.THREE) return Promise.resolve();
- return new Promise(function (resolve, reject) {
- var script = document.createElement('script');
- script.src = THREE_BASE + '/build/three.min.js';
- script.onload = resolve;
- script.onerror = reject;
- document.head.appendChild(script);
- });
+import * as THREE from 'three';
+import { GLTFLoader } from 'three/addons/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/OrbitControls.js';
+
+function init(container, sourceUrl, options) {
+ options = options || {};
+ var renderer = null;
+ var animationFrameId = null;
+ var disposed = false;
+
+ function dispose() {
+ if (disposed) return;
+ disposed = true;
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ if (renderer) {
+ renderer.dispose();
+ renderer.forceContextLoss();
+ renderer = null;
+ }
}
- function init(container, sourceUrl, options) {
- options = options || {};
- var renderer = null;
- var animationFrameId = null;
- var disposed = false;
+ var width = container.clientWidth || 600;
+ var height = container.clientHeight || width;
- function dispose() {
- if (disposed) return;
- disposed = true;
- if (animationFrameId) {
- cancelAnimationFrame(animationFrameId);
- animationFrameId = null;
- }
- if (renderer) {
- renderer.dispose();
- renderer.forceContextLoss();
- renderer = null;
- }
- }
+ var scene = new THREE.Scene();
+ scene.background = new THREE.Color(0xf0f0f0);
- loadThree().then(function () {
- if (disposed) return;
- return Promise.all([
- import(THREE_BASE + '/examples/jsm/loaders/GLTFLoader.js'),
- import(THREE_BASE + '/examples/jsm/controls/OrbitControls.js')
- ]);
- }).then(function (modules) {
+ var camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
+ camera.position.set(0, 0, 5);
+
+ renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(width, height);
+ container.appendChild(renderer.domElement);
+
+ var controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.05;
+ controls.enableZoom = true;
+ controls.enablePan = true;
+ controls.enableRotate = true;
+
+ var ambientLight = new THREE.AmbientLight(0xFFFDD0, 3.5);
+ scene.add(ambientLight);
+
+ var mixer = null;
+ var timer = new THREE.Timer();
+
+ function animate() {
+ if (disposed) return;
+ animationFrameId = requestAnimationFrame(animate);
+ timer.update();
+ var delta = timer.getDelta();
+ if (mixer) mixer.update(delta);
+ controls.update();
+ renderer.render(scene, camera);
+ }
+
+ var loader = new GLTFLoader();
+ loader.load(
+ sourceUrl,
+ function (gltf) {
if (disposed) return;
- var GLTFLoader = modules[0].GLTFLoader;
- var OrbitControls = modules[1].OrbitControls;
-
- var width = container.clientWidth || 600;
- var height = container.clientHeight || width;
-
- var scene = new THREE.Scene();
- scene.background = new THREE.Color(0xf0f0f0);
-
- var camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
- camera.position.set(0, 0, 5);
-
- renderer = new THREE.WebGLRenderer({ antialias: true });
- renderer.setSize(width, height);
- container.appendChild(renderer.domElement);
-
- var controls = new OrbitControls(camera, renderer.domElement);
- controls.enableDamping = true;
- controls.dampingFactor = 0.05;
- controls.enableZoom = true;
- controls.enablePan = true;
- controls.enableRotate = true;
-
- var ambientLight = new THREE.AmbientLight(0xFFFDD0, 3.5);
- scene.add(ambientLight);
-
- var mixer = null;
- var clock = new THREE.Clock();
-
- function animate() {
- if (disposed) return;
- animationFrameId = requestAnimationFrame(animate);
- var delta = clock.getDelta();
- if (mixer) mixer.update(delta);
- controls.update();
- renderer.render(scene, camera);
+ scene.add(gltf.scene);
+
+ if (gltf.animations && gltf.animations.length > 0) {
+ mixer = new THREE.AnimationMixer(gltf.scene);
+ gltf.animations.forEach(function (clip) {
+ mixer.clipAction(clip).play();
+ });
+ if (options.onAnimationsFound) {
+ options.onAnimationsFound(gltf.animations.length);
+ }
}
- var loader = new GLTFLoader();
- loader.load(
- sourceUrl,
- function (gltf) {
+ // Auto-scale and center the model
+ var box = new THREE.Box3().setFromObject(gltf.scene);
+ var center = box.getCenter(new THREE.Vector3());
+ var size = box.getSize(new THREE.Vector3());
+
+ gltf.scene.position.sub(center);
+
+ var maxDim = Math.max(size.x, size.y, size.z);
+ var scale = 5 / maxDim;
+ gltf.scene.scale.setScalar(scale);
+
+ var distance = Math.max(3, maxDim * 2);
+ camera.position.set(distance, distance * 0.5, distance);
+ camera.lookAt(0, 0, 0);
+
+ controls.target.set(0, 0, 0);
+ controls.update();
+
+ animate();
+
+ if (options.onThumbnailReady) {
+ setTimeout(function () {
if (disposed) return;
- scene.add(gltf.scene);
-
- if (gltf.animations && gltf.animations.length > 0) {
- mixer = new THREE.AnimationMixer(gltf.scene);
- gltf.animations.forEach(function (clip) {
- mixer.clipAction(clip).play();
- });
- if (options.onAnimationsFound) {
- options.onAnimationsFound(gltf.animations.length);
- }
- }
-
- // Auto-scale and center the model
- var box = new THREE.Box3().setFromObject(gltf.scene);
- var center = box.getCenter(new THREE.Vector3());
- var size = box.getSize(new THREE.Vector3());
-
- gltf.scene.position.sub(center);
-
- var maxDim = Math.max(size.x, size.y, size.z);
- var scale = 5 / maxDim;
- gltf.scene.scale.setScalar(scale);
-
- var distance = Math.max(3, maxDim * 2);
- camera.position.set(distance, distance * 0.5, distance);
- camera.lookAt(0, 0, 0);
-
- controls.target.set(0, 0, 0);
- controls.update();
-
- animate();
-
- if (options.onThumbnailReady) {
- setTimeout(function () {
- if (disposed) return;
- renderer.render(scene, camera);
- options.onThumbnailReady(renderer, scene, camera);
- }, 1000);
- }
- },
- undefined,
- function (error) {
- console.error('Error loading GLB file:', error);
- if (options.onError) {
- options.onError(error);
- }
- }
- );
- }).catch(function (error) {
- console.error('Error initializing GLB viewer:', error);
+ renderer.render(scene, camera);
+ options.onThumbnailReady(renderer, scene, camera);
+ }, 1000);
+ }
+ },
+ undefined,
+ function (error) {
+ console.error('Error loading GLB file:', error);
if (options.onError) {
options.onError(error);
}
- });
+ }
+ );
- return { dispose: dispose };
- }
+ return { dispose: dispose };
+}
- return { init: init };
-})();
+export var GLBViewer = { init: init };
diff --git a/src/core/static/libs/BufferGeometryUtils-0.184.0.js b/src/core/static/libs/BufferGeometryUtils-0.184.0.js
new file mode 100644
index 000000000..6e996616e
--- /dev/null
+++ b/src/core/static/libs/BufferGeometryUtils-0.184.0.js
@@ -0,0 +1,1434 @@
+import {
+ BufferAttribute,
+ BufferGeometry,
+ Float32BufferAttribute,
+ InstancedBufferAttribute,
+ InterleavedBuffer,
+ InterleavedBufferAttribute,
+ TriangleFanDrawMode,
+ TriangleStripDrawMode,
+ TrianglesDrawMode,
+ Vector3,
+} from 'three';
+
+/**
+ * @module BufferGeometryUtils
+ * @three_import import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+ */
+
+/**
+ * Computes vertex tangents using the MikkTSpace algorithm. MikkTSpace generates the same tangents consistently,
+ * and is used in most modelling tools and normal map bakers. Use MikkTSpace for materials with normal maps,
+ * because inconsistent tangents may lead to subtle visual issues in the normal map, particularly around mirrored
+ * UV seams.
+ *
+ * In comparison to this method, {@link BufferGeometry#computeTangents} (a custom algorithm) generates tangents that
+ * probably will not match the tangents in other software. The custom algorithm is sufficient for general use with a
+ * custom material, and may be faster than MikkTSpace.
+ *
+ * Returns the original BufferGeometry. Indexed geometries will be de-indexed. Requires position, normal, and uv attributes.
+ *
+ * @param {BufferGeometry} geometry - The geometry to compute tangents for.
+ * @param {Object} MikkTSpace - Instance of `examples/jsm/libs/mikktspace.module.js`, or `mikktspace` npm package.
+ * Await `MikkTSpace.ready` before use.
+ * @param {boolean} [negateSign=true] - Whether to negate the sign component (.w) of each tangent.
+ * Required for normal map conventions in some formats, including glTF.
+ * @return {BufferGeometry} The updated geometry.
+ */
+function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
+
+ if ( ! MikkTSpace || ! MikkTSpace.isReady ) {
+
+ throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );
+
+ }
+
+ if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {
+
+ throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );
+
+ }
+
+ function getAttributeArray( attribute ) {
+
+ if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) {
+
+ const dstArray = new Float32Array( attribute.count * attribute.itemSize );
+
+ for ( let i = 0, j = 0; i < attribute.count; i ++ ) {
+
+ dstArray[ j ++ ] = attribute.getX( i );
+ dstArray[ j ++ ] = attribute.getY( i );
+
+ if ( attribute.itemSize > 2 ) {
+
+ dstArray[ j ++ ] = attribute.getZ( i );
+
+ }
+
+ }
+
+ return dstArray;
+
+ }
+
+ if ( attribute.array instanceof Float32Array ) {
+
+ return attribute.array;
+
+ }
+
+ return new Float32Array( attribute.array );
+
+ }
+
+ // MikkTSpace algorithm requires non-indexed input.
+
+ const _geometry = geometry.index ? geometry.toNonIndexed() : geometry;
+
+ // Compute vertex tangents.
+
+ const tangents = MikkTSpace.generateTangents(
+
+ getAttributeArray( _geometry.attributes.position ),
+ getAttributeArray( _geometry.attributes.normal ),
+ getAttributeArray( _geometry.attributes.uv )
+
+ );
+
+ // Texture coordinate convention of glTF differs from the apparent
+ // default of the MikkTSpace library; .w component must be flipped.
+
+ if ( negateSign ) {
+
+ for ( let i = 3; i < tangents.length; i += 4 ) {
+
+ tangents[ i ] *= - 1;
+
+ }
+
+ }
+
+ //
+
+ _geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) );
+
+ if ( geometry !== _geometry ) {
+
+ geometry.copy( _geometry );
+
+ }
+
+ return geometry;
+
+}
+
+/**
+ * Merges a set of geometries into a single instance. All geometries must have compatible attributes.
+ *
+ * @param {Array{{ _("Welcome to Jandig") }}
-
- {{ _("An Open Source Augmented Reality art community.") }}
-
- {{ _("Welcome to Jandig") }}
+
+ {{ _("Uma comunidade de código aberto de arte em Realidade Estendida, que viabiliza a produção de exposições e obras unindo arte e tecnologia.") }}
+
+
- {{ _("Uma comunidade de código aberto de arte em Realidade Estendida, que viabiliza a produção de exposições e obras unindo arte e tecnologia.") }} --
{{ _("No vídeo abaixo, apresentamos o que é a Jandig, o conceito da plataforma e suas possibilidades.") }}
-- {{ _("Se você se identificou com a Jandig e acredita que a plataforma é útil para você criar seu projeto com realidade aumentada (seja ele uma exposição, cartaz, adesivo, mural, livro ou o que mais sua imaginação puder criar), deixamos para você um passo a passo de como utilizá-la. Ele explica o processo para subir seu conteúdo ou remixar o conteúdo já existente na plataforma.") }} -
-- {{ _("Para acompanhar o passo a passo e colocar em prática,") }} - {{ _("faça seu cadastro") }} - {{ _("antes de começar a assistir. Depois, volte aqui e dê play no vídeo. - Se você preferir, também pode acessar o") }} - {{ _("passo a passo por texto.") }} - {{ _("Além disso, no site da Jandig, na área de") }} - {{ _("Ajuda") }}, - {{ _("há outros tutoriais que podem ser úteis. Não deixe de consultá-los se sentir que é necessário.") }} -
-- {{ _("Esperamos ver suas obras e exposições na Jandig! Se você criar algo, não deixe de compartilhar conosco. Temos um") }} - {{ _("canal no Telegram") }} - {{ _("voltado para a comunidade, onde é possível tirar dúvidas e compartilhar suas criações e experiências.") }} -
-diff --git a/src/core/jinja2/core/home_v2.jinja2 b/src/core/jinja2/core/home_v2.jinja2 deleted file mode 100644 index a27c7915c..000000000 --- a/src/core/jinja2/core/home_v2.jinja2 +++ /dev/null @@ -1,91 +0,0 @@ - - -
- - - - - {# #} - - - - -
- - - - - {% block extra_css %}{% endblock %} - - -
-