CarolineM5 commited on
Commit
973f3eb
·
verified ·
1 Parent(s): 214a81e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +305 -206
app.py CHANGED
@@ -83,219 +83,318 @@ class UNetNoCondWrapper(nn.Module):
83
  # b = base64.b64encode(buf.getvalue()).decode("utf-8")
84
  # return f"data:image/png;base64,{b}"
85
 
86
- def build_textured_cube(pil_imgs, face_rotations=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  """
88
- Crée un parallélépipède texturé (OBJ + MTL + textures).
89
- - pil_imgs: liste/tuple de 4 PIL.Image dans l'ordre [front, right, back, left]
90
- - Retour: (chemin_absolu_obj, tmpdir)
91
- - Defaults (comme demandé) :
92
- default_rots = {"front": 0, "right": 270, "back": 180, "left": 90, "top": 0, "bottom": 0}
93
- face_order = ["top","right","bottom","left","front","back"]
94
- Notes:
95
- - Les textures sont écrites dans un dossier temporaire (préférentiellement sous /tmp/gradio).
96
- - Les faces front/back utiliseront la largeur/hauteur de pil_imgs[0].
97
- - Les faces right/left utiliseront la largeur de pil_imgs[1] et pil_imgs[3] (moyenne si différentes).
98
  """
99
- import os
100
- import tempfile
101
- from PIL import Image
102
-
103
- if not (isinstance(pil_imgs, (list, tuple)) and len(pil_imgs) >= 4):
104
- raise ValueError("build_textured_cube attend une liste/tuple de 4 images PIL (front, right, back, left).")
105
-
106
- # rotations & ordre demandés
107
- default_rots = {"front": 0, "right": 270, "back": 180, "left": 90, "top": 0, "bottom": 0}
108
- if face_rotations is None:
109
- face_rotations = default_rots
110
- else:
111
- for k, v in default_rots.items():
112
- face_rotations.setdefault(k, v)
113
-
114
- # --- creer dossier temporaire preferentiellement sous /tmp/gradio (HF Spaces) ---
115
- base_dir = "/tmp/gradio"
116
- if os.path.isdir(base_dir) and os.access(base_dir, os.W_OK):
117
- tmpdir = tempfile.mkdtemp(prefix="parallelep_", dir=base_dir)
118
- else:
119
- tmpdir = tempfile.mkdtemp(prefix="parallelep_")
120
-
121
- # noms de fichiers textures (relatifs)
122
- tex_names = {
123
- "front": "tex_front.png",
124
- "right": "tex_right.png",
125
- "back": "tex_back.png",
126
- "left": "tex_left.png",
127
- "top": "tex_top.png",
128
- "bottom": "tex_bottom.png",
129
  }
130
 
131
- # récupérer tailles (on accepte que les images aient déjà été redimensionnées
132
- # (ex: left/right résized par ton code d'inference à new_width x original_height))
133
- front_w, front_h = pil_imgs[0].size
134
- right_w, right_h = pil_imgs[1].size
135
- back_w, back_h = pil_imgs[2].size
136
- left_w, left_h = pil_imgs[3].size
137
-
138
- # On définit les dimensions principales du box :
139
- # width (X) = front_w
140
- # depth (Y) = moyenne des largeurs de left/right (i.e. largeur le long Y)
141
- # height (Z) = front_h
142
- width_px = float(front_w)
143
- height_px = float(front_h)
144
- # depth: moyenne des largeurs horizontales des textures latérales (right & left)
145
- depth_px = float((right_w + left_w) / 2.0)
146
-
147
- # Normaliser pour garder la boîte à des tailles raisonnables (max dimension -> 1.0)
148
- max_dim = max(width_px, depth_px, height_px, 1.0)
149
- scale = 1.0 / max_dim
150
- half_x = (width_px * 0.5) * scale
151
- half_y = (depth_px * 0.5) * scale
152
- half_z = (height_px * 0.5) * scale
153
-
154
- # Mapping attendu pour pil_imgs
155
- mapping_order = ["front", "right", "back", "left"]
156
-
157
- # Sauvegarder les textures avec rotation demandée (et permissions)
158
- for img, face_name in zip(pil_imgs[:4], mapping_order):
159
- im = img.convert("RGB")
160
- # si la taille diffère (sécurité), on la respecte : on n'uniformise pas ici
161
- angle = face_rotations.get(face_name, 0)
162
- if angle % 360 != 0:
163
- # PIL rotate: angle en degrés, positif = CCW
164
- im = im.rotate(angle, resample=Image.BICUBIC, expand=False)
165
- path = os.path.join(tmpdir, tex_names[face_name])
166
- im.save(path, format="PNG")
167
- try:
168
- os.chmod(path, 0o644)
169
- except Exception:
170
- pass
171
-
172
- # top / bottom textures noires (même hauteur que front pour homogénéité)
173
- black = Image.new("RGB", (front_w, front_h), (0, 0, 0))
174
- for face_name in ("top", "bottom"):
175
- im = black
176
- angle = face_rotations.get(face_name, 0)
177
- if angle % 360 != 0:
178
- im = im.rotate(angle, resample=Image.BICUBIC, expand=False)
179
- p = os.path.join(tmpdir, tex_names[face_name])
180
- im.save(p, format="PNG")
181
- try:
182
- os.chmod(p, 0o644)
183
- except Exception:
184
- pass
185
-
186
- # --- écrire le .mtl (références relatives) ---
187
- mtl_path = os.path.join(tmpdir, "parallelep.mtl")
188
- with open(mtl_path, "w", encoding="utf-8") as f:
189
- f.write("# Material file for parallelepiped\n")
190
- for mat_name, tex_file in tex_names.items():
191
- f.write(f"newmtl m_{mat_name}\n")
192
- f.write("Ka 1.000 1.000 1.000\n")
193
- f.write("Kd 1.000 1.000 1.000\n")
194
- f.write("Ks 0.000 0.000 0.000\n")
195
- f.write("Ns 10.000\n")
196
- f.write("illum 2\n")
197
- f.write(f"map_Kd {tex_file}\n\n")
198
- try:
199
- os.chmod(mtl_path, 0o644)
200
- except Exception:
201
- pass
202
-
203
- # --- construire la géométrie : on écrit 24 vertices (4 par face) dans l'ordre
204
- # On utilise l'ordre des faces demandé par toi :
205
- face_order = ["top", "right", "bottom", "left", "front", "back"]
206
-
207
- # Pour éviter toute confusion d'indices, on définit pour chaque face les 4 sommets (en coordonnées)
208
- # Convention de coordonnées : +X = right, +Y = front, +Z = up
209
- # coins du parallélépipède (en coordonnées globales) si on utilisait 8 sommets :
210
- # but ici on énumère 4-verts par face afin d'avoir des UVs distincts
211
- # Définition des quads (ordre: v0, v1, v2, v3) orientés CCW quand on regarde la face (face avant)
212
  quads = {
213
- # top (+Z) : côté supérieur, couvrira width x depth
214
- "top": [
215
- (-half_x, -half_y, half_z),
216
- ( half_x, -half_y, half_z),
217
- ( half_x, half_y, half_z),
218
- (-half_x, half_y, half_z),
219
- ],
220
- # right (+X) : face latérale droite, couvrira depth x height (u=0..1 mappe largeur depth)
221
- "right": [
222
- ( half_x, -half_y, -half_z),
223
- ( half_x, half_y, -half_z),
224
- ( half_x, half_y, half_z),
225
- ( half_x, -half_y, half_z),
226
- ],
227
- # bottom (-Z)
228
- "bottom": [
229
- (-half_x, half_y, -half_z),
230
- ( half_x, half_y, -half_z),
231
- ( half_x, -half_y, -half_z),
232
- (-half_x, -half_y, -half_z),
233
- ],
234
- # left (-X)
235
- "left": [
236
- (-half_x, -half_y, half_z),
237
- (-half_x, half_y, half_z),
238
- (-half_x, half_y, -half_z),
239
- (-half_x, -half_y, -half_z),
240
- ],
241
- # front (+Y)
242
- "front": [
243
- (-half_x, half_y, -half_z),
244
- (-half_x, half_y, half_z),
245
- ( half_x, half_y, half_z),
246
- ( half_x, half_y, -half_z),
247
- ],
248
- # back (-Y)
249
- "back": [
250
- ( half_x, -half_y, -half_z),
251
- ( half_x, -half_y, half_z),
252
- (-half_x, -half_y, half_z),
253
- (-half_x, -half_y, -half_z),
254
- ],
255
  }
256
 
257
- # écrire l'OBJ (triangulé, 24 vertices, 24 vt)
258
- obj_path = os.path.join(tmpdir, "parallelep.obj")
259
- with open(obj_path, "w", encoding="utf-8") as f:
260
- f.write("# Parallelepiped OBJ generated by build_textured_cube\n")
261
- f.write("mtllib parallelep.mtl\n\n")
262
-
263
- # écrire vertices (4 par face dans l'ordre face_order)
264
- for face_name in face_order:
265
- for v in quads[face_name]:
266
- f.write("v {:.6f} {:.6f} {:.6f}\n".format(*v))
267
- f.write("\n")
268
-
269
- # écrire UVs : pour chaque face on écrit 4 UVs (0..1)
270
- # note : si la texture est rectangulaire, UVs restent 0..1 et mapperont l'image entière
271
- uvs = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
272
- for _ in range(6):
273
- for (u, v) in uvs:
274
- f.write("vt {:.6f} {:.6f}\n".format(u, v))
275
- f.write("\n")
276
-
277
- # faces (2 triangles par face), indices 1-based
278
- for i, face_name in enumerate(face_order):
279
- f.write(f"usemtl m_{face_name}\n")
280
- v_base = i * 4 + 1
281
- t_base = i * 4 + 1
282
- v1, v2, v3, v4 = v_base, v_base + 1, v_base + 2, v_base + 3
283
- t1, t2, t3, t4 = t_base, t_base + 1, t_base + 2, t_base + 3
284
- # écrire deux triangles (v/vt)
285
- f.write(f"f {v1}/{t1} {v2}/{t2} {v3}/{t3}\n")
286
- f.write(f"f {v1}/{t1} {v3}/{t3} {v4}/{t4}\n\n")
287
- try:
288
- os.chmod(obj_path, 0o644)
289
- except Exception:
290
- pass
291
-
292
- # vérifications rapides
293
- for fname in ["parallelep.obj", "parallelep.mtl"] + list(tex_names.values()):
294
- p = os.path.join(tmpdir, fname)
295
- if not os.path.exists(p):
296
- raise FileNotFoundError(f"Fichier attendu introuvable : {p}")
297
-
298
- return (os.path.abspath(obj_path), tmpdir)
299
 
300
  # def build_textured_cube(pil_imgs, face_rotations=None):
301
  # """
 
83
  # b = base64.b64encode(buf.getvalue()).decode("utf-8")
84
  # return f"data:image/png;base64,{b}"
85
 
86
+ # def build_textured_cube(pil_imgs, face_rotations=None):
87
+ # """
88
+ # Crée un parallélépipède texturé (OBJ + MTL + textures).
89
+ # - pil_imgs: liste/tuple de 4 PIL.Image dans l'ordre [front, right, back, left]
90
+ # - Retour: (chemin_absolu_obj, tmpdir)
91
+ # - Defaults (comme demandé) :
92
+ # default_rots = {"front": 0, "right": 270, "back": 180, "left": 90, "top": 0, "bottom": 0}
93
+ # face_order = ["top","right","bottom","left","front","back"]
94
+ # Notes:
95
+ # - Les textures sont écrites dans un dossier temporaire (préférentiellement sous /tmp/gradio).
96
+ # - Les faces front/back utiliseront la largeur/hauteur de pil_imgs[0].
97
+ # - Les faces right/left utiliseront la largeur de pil_imgs[1] et pil_imgs[3] (moyenne si différentes).
98
+ # """
99
+ # import os
100
+ # import tempfile
101
+ # from PIL import Image
102
+
103
+ # if not (isinstance(pil_imgs, (list, tuple)) and len(pil_imgs) >= 4):
104
+ # raise ValueError("build_textured_cube attend une liste/tuple de 4 images PIL (front, right, back, left).")
105
+
106
+ # # rotations & ordre demandés
107
+ # default_rots = {"front": 0, "right": 270, "back": 180, "left": 90, "top": 0, "bottom": 0}
108
+ # if face_rotations is None:
109
+ # face_rotations = default_rots
110
+ # else:
111
+ # for k, v in default_rots.items():
112
+ # face_rotations.setdefault(k, v)
113
+
114
+ # # --- creer dossier temporaire preferentiellement sous /tmp/gradio (HF Spaces) ---
115
+ # base_dir = "/tmp/gradio"
116
+ # if os.path.isdir(base_dir) and os.access(base_dir, os.W_OK):
117
+ # tmpdir = tempfile.mkdtemp(prefix="parallelep_", dir=base_dir)
118
+ # else:
119
+ # tmpdir = tempfile.mkdtemp(prefix="parallelep_")
120
+
121
+ # # noms de fichiers textures (relatifs)
122
+ # tex_names = {
123
+ # "front": "tex_front.png",
124
+ # "right": "tex_right.png",
125
+ # "back": "tex_back.png",
126
+ # "left": "tex_left.png",
127
+ # "top": "tex_top.png",
128
+ # "bottom": "tex_bottom.png",
129
+ # }
130
+
131
+ # # récupérer tailles (on accepte que les images aient déjà été redimensionnées
132
+ # # (ex: left/right résized par ton code d'inference à new_width x original_height))
133
+ # front_w, front_h = pil_imgs[0].size
134
+ # right_w, right_h = pil_imgs[1].size
135
+ # back_w, back_h = pil_imgs[2].size
136
+ # left_w, left_h = pil_imgs[3].size
137
+
138
+ # # On définit les dimensions principales du box :
139
+ # # width (X) = front_w
140
+ # # depth (Y) = moyenne des largeurs de left/right (i.e. largeur le long Y)
141
+ # # height (Z) = front_h
142
+ # width_px = float(front_w)
143
+ # height_px = float(front_h)
144
+ # # depth: moyenne des largeurs horizontales des textures latérales (right & left)
145
+ # depth_px = float((right_w + left_w) / 2.0)
146
+
147
+ # # Normaliser pour garder la boîte à des tailles raisonnables (max dimension -> 1.0)
148
+ # max_dim = max(width_px, depth_px, height_px, 1.0)
149
+ # scale = 1.0 / max_dim
150
+ # half_x = (width_px * 0.5) * scale
151
+ # half_y = (depth_px * 0.5) * scale
152
+ # half_z = (height_px * 0.5) * scale
153
+
154
+ # # Mapping attendu pour pil_imgs
155
+ # mapping_order = ["front", "right", "back", "left"]
156
+
157
+ # # Sauvegarder les textures avec rotation demandée (et permissions)
158
+ # for img, face_name in zip(pil_imgs[:4], mapping_order):
159
+ # im = img.convert("RGB")
160
+ # # si la taille diffère (sécurité), on la respecte : on n'uniformise pas ici
161
+ # angle = face_rotations.get(face_name, 0)
162
+ # if angle % 360 != 0:
163
+ # # PIL rotate: angle en degrés, positif = CCW
164
+ # im = im.rotate(angle, resample=Image.BICUBIC, expand=False)
165
+ # path = os.path.join(tmpdir, tex_names[face_name])
166
+ # im.save(path, format="PNG")
167
+ # try:
168
+ # os.chmod(path, 0o644)
169
+ # except Exception:
170
+ # pass
171
+
172
+ # # top / bottom textures noires (même hauteur que front pour homogénéité)
173
+ # black = Image.new("RGB", (front_w, front_h), (0, 0, 0))
174
+ # for face_name in ("top", "bottom"):
175
+ # im = black
176
+ # angle = face_rotations.get(face_name, 0)
177
+ # if angle % 360 != 0:
178
+ # im = im.rotate(angle, resample=Image.BICUBIC, expand=False)
179
+ # p = os.path.join(tmpdir, tex_names[face_name])
180
+ # im.save(p, format="PNG")
181
+ # try:
182
+ # os.chmod(p, 0o644)
183
+ # except Exception:
184
+ # pass
185
+
186
+ # # --- écrire le .mtl (références relatives) ---
187
+ # mtl_path = os.path.join(tmpdir, "parallelep.mtl")
188
+ # with open(mtl_path, "w", encoding="utf-8") as f:
189
+ # f.write("# Material file for parallelepiped\n")
190
+ # for mat_name, tex_file in tex_names.items():
191
+ # f.write(f"newmtl m_{mat_name}\n")
192
+ # f.write("Ka 1.000 1.000 1.000\n")
193
+ # f.write("Kd 1.000 1.000 1.000\n")
194
+ # f.write("Ks 0.000 0.000 0.000\n")
195
+ # f.write("Ns 10.000\n")
196
+ # f.write("illum 2\n")
197
+ # f.write(f"map_Kd {tex_file}\n\n")
198
+ # try:
199
+ # os.chmod(mtl_path, 0o644)
200
+ # except Exception:
201
+ # pass
202
+
203
+ # # --- construire la géométrie : on écrit 24 vertices (4 par face) dans l'ordre
204
+ # # On utilise l'ordre des faces demandé par toi :
205
+ # face_order = ["top", "right", "bottom", "left", "front", "back"]
206
+
207
+ # # Pour éviter toute confusion d'indices, on définit pour chaque face les 4 sommets (en coordonnées)
208
+ # # Convention de coordonnées : +X = right, +Y = front, +Z = up
209
+ # # coins du parallélépipède (en coordonnées globales) si on utilisait 8 sommets :
210
+ # # but ici on énumère 4-verts par face afin d'avoir des UVs distincts
211
+ # # Définition des quads (ordre: v0, v1, v2, v3) orientés CCW quand on regarde la face (face avant)
212
+ # quads = {
213
+ # # top (+Z) : côté supérieur, couvrira width x depth
214
+ # "top": [
215
+ # (-half_x, -half_y, half_z),
216
+ # ( half_x, -half_y, half_z),
217
+ # ( half_x, half_y, half_z),
218
+ # (-half_x, half_y, half_z),
219
+ # ],
220
+ # # right (+X) : face latérale droite, couvrira depth x height (u=0..1 mappe largeur depth)
221
+ # "right": [
222
+ # ( half_x, -half_y, -half_z),
223
+ # ( half_x, half_y, -half_z),
224
+ # ( half_x, half_y, half_z),
225
+ # ( half_x, -half_y, half_z),
226
+ # ],
227
+ # # bottom (-Z)
228
+ # "bottom": [
229
+ # (-half_x, half_y, -half_z),
230
+ # ( half_x, half_y, -half_z),
231
+ # ( half_x, -half_y, -half_z),
232
+ # (-half_x, -half_y, -half_z),
233
+ # ],
234
+ # # left (-X)
235
+ # "left": [
236
+ # (-half_x, -half_y, half_z),
237
+ # (-half_x, half_y, half_z),
238
+ # (-half_x, half_y, -half_z),
239
+ # (-half_x, -half_y, -half_z),
240
+ # ],
241
+ # # front (+Y)
242
+ # "front": [
243
+ # (-half_x, half_y, -half_z),
244
+ # (-half_x, half_y, half_z),
245
+ # ( half_x, half_y, half_z),
246
+ # ( half_x, half_y, -half_z),
247
+ # ],
248
+ # # back (-Y)
249
+ # "back": [
250
+ # ( half_x, -half_y, -half_z),
251
+ # ( half_x, -half_y, half_z),
252
+ # (-half_x, -half_y, half_z),
253
+ # (-half_x, -half_y, -half_z),
254
+ # ],
255
+ # }
256
+
257
+ # # écrire l'OBJ (triangulé, 24 vertices, 24 vt)
258
+ # obj_path = os.path.join(tmpdir, "parallelep.obj")
259
+ # with open(obj_path, "w", encoding="utf-8") as f:
260
+ # f.write("# Parallelepiped OBJ generated by build_textured_cube\n")
261
+ # f.write("mtllib parallelep.mtl\n\n")
262
+
263
+ # # écrire vertices (4 par face dans l'ordre face_order)
264
+ # for face_name in face_order:
265
+ # for v in quads[face_name]:
266
+ # f.write("v {:.6f} {:.6f} {:.6f}\n".format(*v))
267
+ # f.write("\n")
268
+
269
+ # # écrire UVs : pour chaque face on écrit 4 UVs (0..1)
270
+ # # note : si la texture est rectangulaire, UVs restent 0..1 et mapperont l'image entière
271
+ # uvs = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
272
+ # for _ in range(6):
273
+ # for (u, v) in uvs:
274
+ # f.write("vt {:.6f} {:.6f}\n".format(u, v))
275
+ # f.write("\n")
276
+
277
+ # # faces (2 triangles par face), indices 1-based
278
+ # for i, face_name in enumerate(face_order):
279
+ # f.write(f"usemtl m_{face_name}\n")
280
+ # v_base = i * 4 + 1
281
+ # t_base = i * 4 + 1
282
+ # v1, v2, v3, v4 = v_base, v_base + 1, v_base + 2, v_base + 3
283
+ # t1, t2, t3, t4 = t_base, t_base + 1, t_base + 2, t_base + 3
284
+ # # écrire deux triangles (v/vt)
285
+ # f.write(f"f {v1}/{t1} {v2}/{t2} {v3}/{t3}\n")
286
+ # f.write(f"f {v1}/{t1} {v3}/{t3} {v4}/{t4}\n\n")
287
+ # try:
288
+ # os.chmod(obj_path, 0o644)
289
+ # except Exception:
290
+ # pass
291
+
292
+ # # vérifications rapides
293
+ # for fname in ["parallelep.obj", "parallelep.mtl"] + list(tex_names.values()):
294
+ # p = os.path.join(tmpdir, fname)
295
+ # if not os.path.exists(p):
296
+ # raise FileNotFoundError(f"Fichier attendu introuvable : {p}")
297
+
298
+ # return (os.path.abspath(obj_path), tmpdir)
299
+
300
+
301
+ def build_textured_cube(pil_imgs):
302
  """
303
+ pil_imgs: [front, right, back, left]
304
+ Renvoie : path absolu vers cube.obj
 
 
 
 
 
 
 
 
305
  """
306
+
307
+ # -------------------------
308
+ # 1. Création du répertoire temporaire
309
+ # -------------------------
310
+ tmpdir = tempfile.mkdtemp()
311
+
312
+ # Créer les images noires pour top et bottom
313
+ size_square = pil_imgs[0].size[0] # front est carré
314
+ black = Image.new("RGB", (size_square, size_square), (0, 0, 0))
315
+ textures = {
316
+ "front": pil_imgs[0],
317
+ "right": pil_imgs[1],
318
+ "back": pil_imgs[2],
319
+ "left": pil_imgs[3],
320
+ "top": black,
321
+ "bottom": black
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
+ # -------------------------
325
+ # 2. Rotation des textures si nécessaire
326
+ # -------------------------
327
+ # Toutes les faces +90° pour être dans le même sens
328
+ rotations = {"front": 0, "right": 270, "back": 180, "left": 90, "top": 0, "bottom": 0}
329
+ for face, img in textures.items():
330
+ if rotations[face] != 0:
331
+ textures[face] = img.rotate(rotations[face], expand=True)
332
+ # sauvegarder l'image
333
+ textures[face].save(os.path.join(tmpdir, f"{face}.png"))
334
+
335
+ # -------------------------
336
+ # 3. Dimensions du parallélépipède
337
+ # -------------------------
338
+ # front/back = carré
339
+ w = pil_imgs[0].width
340
+ h = pil_imgs[0].height
341
+ # left/right = rectangles
342
+ d = pil_imgs[1].width # largeur du rectangle (X) correspond à profondeur
343
+ # hauteur Z = front.height
344
+ half_x = w / 2
345
+ half_y = d / 2
346
+ half_z = h / 2
347
+
348
+ # -------------------------
349
+ # 4. Définition des sommets
350
+ # -------------------------
351
+ verts = [
352
+ (-half_x, -half_y, -half_z),
353
+ ( half_x, -half_y, -half_z),
354
+ ( half_x, half_y, -half_z),
355
+ (-half_x, half_y, -half_z),
356
+ (-half_x, -half_y, half_z),
357
+ ( half_x, -half_y, half_z),
358
+ ( half_x, half_y, half_z),
359
+ (-half_x, half_y, half_z),
360
+ ]
361
+
362
+ # -------------------------
363
+ # 5. Faces et textures
364
+ # -------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  quads = {
366
+ "front": [4,5,6,7],
367
+ "back": [0,1,2,3],
368
+ "left": [0,4,7,3],
369
+ "right": [1,5,6,2],
370
+ "top": [3,7,6,2],
371
+ "bottom": [0,4,5,1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  }
373
 
374
+ # -------------------------
375
+ # 6. Création du fichier OBJ et MTL
376
+ # -------------------------
377
+ obj_path = os.path.join(tmpdir, "cube.obj")
378
+ mtl_path = os.path.join(tmpdir, "cube.mtl")
379
+
380
+ with open(mtl_path, "w") as f:
381
+ for face in quads:
382
+ f.write(f"newmtl {face}\n")
383
+ f.write(f"Ka 1.0 1.0 1.0\nKd 1.0 1.0 1.0\nKs 0.0 0.0 0.0\n")
384
+ f.write(f"map_Kd {face}.png\n\n")
385
+
386
+ with open(obj_path, "w") as f:
387
+ f.write(f"mtllib cube.mtl\n")
388
+ for v in verts:
389
+ f.write(f"v {v[0]} {v[1]} {v[2]}\n")
390
+ # coordonnées UV simples (coin-bas-gauche à coin-haut-droit)
391
+ f.write("vt 0 0\nvt 1 0\nvt 1 1\nvt 0 1\n")
392
+ f.write("vn 0 0 1\n")
393
+ for face, idxs in quads.items():
394
+ f.write(f"usemtl {face}\n")
395
+ f.write(f"f {idxs[0]+1}/1 {idxs[1]+1}/2 {idxs[2]+1}/3 {idxs[3]+1}/4\n")
396
+
397
+ return obj_path, tmpdir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
  # def build_textured_cube(pil_imgs, face_rotations=None):
400
  # """