Skip to content

Commit 90d43c4

Browse files
authored
Merge pull request #3 from Danyals-code/standardize-panel-and-orient-products
Fix batch render failing on product names with trailing spaces, and two related bugs
2 parents fb6c8a1 + cf32ce9 commit 90d43c4

1 file changed

Lines changed: 111 additions & 16 deletions

File tree

‎multiview_product_renderer/__init__.py‎

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,23 @@ def ensure_collection(name, parent=None):
313313

314314

315315
def get_products_root():
316+
"""The MV_Products collection, created if it is not there yet.
317+
318+
Only for operators. Panels read the products with get_products(), which
319+
never creates anything: draw() runs on every redraw, so reaching for the
320+
root here linked an MV_Products collection into the scene merely because
321+
someone opened the sidebar, and left their file marked as modified for it.
322+
"""
316323
return ensure_collection(PRODUCT_COLL)
317324

318325

319326
def get_products():
320-
return list(get_products_root().children)
327+
"""Product collections, or [] when the root does not exist yet.
328+
329+
Read-only on purpose -- see get_products_root().
330+
"""
331+
root = bpy.data.collections.get(PRODUCT_COLL)
332+
return list(root.children) if root else []
321333

322334

323335
def bbox_of(objs):
@@ -675,6 +687,38 @@ def product_label(start_prefix, start_number, offset, per_letter):
675687
return f"{alpha_pair(base + bumps)}_{num:02d}"
676688

677689

690+
# Characters Windows refuses outright, plus the separators that would quietly
691+
# redirect a write into some other directory.
692+
BAD_PATH_CHARS = '<>:"/\\|?*'
693+
RESERVED_NAMES = (
694+
{"CON", "PRN", "AUX", "NUL"}
695+
| {f"COM{i}" for i in range(1, 10)}
696+
| {f"LPT{i}" for i in range(1, 10)}
697+
)
698+
699+
700+
def safe_name(name, fallback="unnamed"):
701+
"""A collection or view name reduced to one usable path component.
702+
703+
These names are free text and usually arrive from a STEP filename, so they
704+
carry whatever the CAD export happened to contain. A trailing space is the
705+
one that bites: asked for 'BICON ', Windows quietly creates 'BICON', then
706+
refuses to write 'BICON \\shot.png' because that directory really does not
707+
exist, and the render dies on the first product with 'cannot save'. The
708+
mismatch hides well, because os.path.isdir strips the same trailing space
709+
on the way in and cheerfully agrees the folder is there.
710+
"""
711+
cleaned = "".join(
712+
"_" if c in BAD_PATH_CHARS or ord(c) < 32 else c for c in str(name)
713+
)
714+
# The name we ask for has to match the name the filesystem will hand back,
715+
# or every later write into it misses.
716+
cleaned = cleaned.strip().rstrip(" .")
717+
if cleaned.split(".")[0].upper() in RESERVED_NAMES:
718+
cleaned = f"_{cleaned}"
719+
return cleaned or fallback
720+
721+
678722
STEPPER_OPERATORS = (
679723
"import_scene.occ_import_step",
680724
"import_scene.occ",
@@ -1563,10 +1607,29 @@ def _do_clay_render(scene, filepath, clay_color):
15631607
scene.display.shading.show_cavity = saved['show_cavity']
15641608

15651609

1610+
class _BatchAborted(Exception):
1611+
"""Stop the batch early without dumping a traceback on the user."""
1612+
1613+
15661614
class MV_OT_RenderAll(Operator):
15671615
bl_idname = "mv.render_all"
15681616
bl_label = "Render All"
15691617

1618+
def _fail(self, failures, done, path, exc):
1619+
"""Record one failed write, aborting the batch if nothing has landed.
1620+
1621+
Collected and reported at the end rather than raised, so a single
1622+
unwritable file cannot throw away a long batch that is otherwise fine.
1623+
Before the first success there is nothing to salvage and the cause is
1624+
almost always the setup, so that case stops immediately instead of
1625+
failing the same way for every remaining image.
1626+
"""
1627+
note = f"{os.path.basename(path)}: {exc}"
1628+
failures.append(note)
1629+
print(f"[MultiView] FAILED {note}")
1630+
if not done:
1631+
raise _BatchAborted from exc
1632+
15701633
def execute(self, context):
15711634
scene = context.scene
15721635
s = scene.mv_settings
@@ -1583,7 +1646,11 @@ def execute(self, context):
15831646
return {'CANCELLED'}
15841647

15851648
out_root = bpy.path.abspath(s.render_output_dir)
1586-
os.makedirs(out_root, exist_ok=True)
1649+
try:
1650+
os.makedirs(out_root, exist_ok=True)
1651+
except OSError as exc:
1652+
self.report({'ERROR'}, f"Cannot use output folder '{out_root}': {exc}")
1653+
return {'CANCELLED'}
15871654

15881655
saved_hide = {}
15891656
for pc in products:
@@ -1598,6 +1665,7 @@ def execute(self, context):
15981665
total_per_pair = (2 if both else 1) * len(cameras)
15991666
total = len(products) * total_per_pair
16001667
done = 0
1668+
failures = []
16011669

16021670
try:
16031671
for i, pc in enumerate(products):
@@ -1607,28 +1675,42 @@ def execute(self, context):
16071675
lc.exclude = (other.name != pc.name)
16081676

16091677
label = product_label(s.start_prefix, s.start_number, i, s.products_per_letter)
1610-
product_dir = os.path.join(out_root, f"{label}_{pc.name}")
1611-
os.makedirs(product_dir, exist_ok=True)
1678+
product_dir = os.path.join(out_root, safe_name(f"{label}_{pc.name}"))
1679+
try:
1680+
os.makedirs(product_dir, exist_ok=True)
1681+
except OSError as exc:
1682+
self._fail(failures, done, product_dir, exc)
1683+
continue
16121684

16131685
for cam in cameras:
16141686
scene.camera = cam
16151687
view_name = cam.name.replace("MV_Cam_", "")
1616-
base = f"{label}_{pc.name}_{view_name}"
1688+
base = safe_name(f"{label}_{pc.name}_{view_name}")
16171689

1690+
clay = (tuple(s.clay_color),)
16181691
if mode == 'FULL':
1619-
_do_full_render(scene, os.path.join(product_dir, base))
1620-
done += 1
1692+
attempts = ((_do_full_render, base, ()),)
16211693
elif mode == 'CLAY':
1622-
_do_clay_render(scene, os.path.join(product_dir, base + "_clay"),
1623-
tuple(s.clay_color))
1624-
done += 1
1694+
attempts = ((_do_clay_render, base + "_clay", clay),)
16251695
else:
1626-
_do_full_render(scene, os.path.join(product_dir, base + "_render"))
1696+
attempts = ((_do_full_render, base + "_render", ()),
1697+
(_do_clay_render, base + "_clay", clay))
1698+
1699+
for render, name, extra in attempts:
1700+
path = os.path.join(product_dir, name)
1701+
try:
1702+
render(scene, path, *extra)
1703+
except RuntimeError as exc:
1704+
# bpy.ops.render.render raises when the image cannot
1705+
# be written. Letting that escape put a Python
1706+
# traceback in front of the user instead of the name
1707+
# of the file that failed.
1708+
self._fail(failures, done, path, exc)
1709+
continue
16271710
done += 1
1628-
_do_clay_render(scene, os.path.join(product_dir, base + "_clay"),
1629-
tuple(s.clay_color))
1630-
done += 1
1631-
print(f"[MultiView] {done}/{total}: {scene.render.filepath}")
1711+
print(f"[MultiView] {done}/{total}: {scene.render.filepath}")
1712+
except _BatchAborted:
1713+
pass
16321714
finally:
16331715
for pc in products:
16341716
lc = find_layer_coll(context.view_layer.layer_collection, pc.name)
@@ -1637,7 +1719,19 @@ def execute(self, context):
16371719
scene.camera = saved_cam
16381720
scene.render.filepath = saved_path
16391721

1640-
self.report({'INFO'}, f"Wrote {done} image(s).")
1722+
if not failures:
1723+
self.report({'INFO'}, f"Wrote {done} image(s).")
1724+
return {'FINISHED'}
1725+
1726+
if not done:
1727+
# Nothing landed at all, so the batch stopped at the first failure
1728+
# rather than burning render time failing the same way every time.
1729+
self.report({'ERROR'}, f"Nothing rendered. {failures[0]}")
1730+
return {'CANCELLED'}
1731+
1732+
self.report({'WARNING'},
1733+
f"Wrote {done} of {total} image(s). {len(failures)} failed, "
1734+
f"first: {failures[0]}. Full list in the System Console.")
16411735
return {'FINISHED'}
16421736

16431737

@@ -1744,6 +1838,7 @@ def draw(self, context):
17441838
for pc in prods:
17451839
box.label(text=f"{pc.name} ({len(pc.all_objects)} objs)", icon='OUTLINER_COLLECTION')
17461840

1841+
17471842
class MV_PT_Standardize(Panel):
17481843
bl_label = "Standardize"
17491844
bl_space_type = 'VIEW_3D'

0 commit comments

Comments
 (0)