Skip to content

Commit 3070b1f

Browse files
committed
Add a Standardize panel and per-product orientation controls
Splits the Import panel in two. Import now covers only the importer and the file list; everything that makes products comparable -- recenter, rescale, target size, auto smooth, and Fit on Import -- moves to a Standardize panel of its own, between Import and Cameras. Sidebar order follows registration order, so the classes tuple is ordered to match. Adds Orient Products under Build Cameras, for the case Front Axis cannot fix: Front Axis rotates the whole batch, so it is no help when only a few products were modelled facing the wrong way. Previous and Next walk the batch one product at a time, showing the current one and excluding the others through the same view layer mechanism Render All uses, so the preview matches what will render. The list wraps, and Show All restores everything afterwards. Left and Right spin the visible product about the vertical axis, the turntable move that brings a sideways product round to the front. Up and Down tip it about whatever axis is horizontal in the current view, so they read the same through any camera. Rotation is about the product's own bounding box centre, so it cannot drift out of frame, and is applied to the collection roots so a multi-part assembly turns rigidly. Rotate Step defaults to 90 degrees, making four presses a full turn. Rotating changes the bounding box the camera framing was measured from, so the panel and the README both say to rebuild cameras afterwards.
1 parent 3168143 commit 3070b1f

2 files changed

Lines changed: 258 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ to see the full list.
113113
You can also click **Add Empty Product Collection** and drag existing meshes into it, which
114114
is how you use MultiView with geometry that didn't come from STEP.
115115

116-
**Fit on Import** (on by default) does this automatically: as each STEP file lands, the
116+
### 3. Standardize
117+
118+
Everything that makes products comparable to each other lives in this panel.
119+
120+
**Fit on Import** (on by default) does it automatically: as each STEP file lands, the
117121
product is uniformly scaled so its largest dimension equals **Target Size** (default
118122
`1.0 m`, i.e. it fits inside a 1x1x1 m cube) and its bounding box is centred on the world
119123
origin. Turn it off if you want to place and scale products yourself.
@@ -145,7 +149,7 @@ on the world origin, so the product rotates about its own centre and the N panel
145149
Geometry does not move during that step. Objects on multi-user or linked data cannot be
146150
edited by Blender, so those keep their transform and are counted in the status message.
147151

148-
### 3. Cameras / Views
152+
### 4. Cameras / Views
149153

150154
Choose a view set and click **Build Cameras**. Cameras land in `MV_Cameras`, named
151155
`MV_Cam_<View>`.
@@ -171,7 +175,34 @@ product reads at the same scale across the whole sheet rather than being re-fitt
171175
Need an angle that isn't in the presets? Add a **Custom View** with an X/Y/Z direction
172176
vector and an ortho/perspective toggle, and it gets built alongside the rest.
173177

174-
### 4. Batch Render
178+
#### Orient Products
179+
180+
**Front Axis** fixes the whole batch at once, which only helps when every product was
181+
modelled the same way. In practice a few arrive facing the wrong way, and this is where you
182+
turn them by hand.
183+
184+
**Previous** and **Next** step through the batch one product at a time, showing the current
185+
one and excluding the rest. That is the same isolation **Render All** uses, so what you see
186+
is what that product's render will contain. The list wraps, so you can keep pressing
187+
**Next** all the way round. **Show All** brings everything back when you are done.
188+
189+
**Left**, **Right**, **Up** and **Down** turn the product that is currently showing:
190+
191+
| Button | What it does |
192+
| --- | --- |
193+
| **Left** / **Right** | Spins the product about the vertical axis — the turntable move that swings a sideways product round to face front. |
194+
| **Up** / **Down** | Tips the product about whatever axis is horizontal on your screen, for something lying on its back or standing on its nose. |
195+
196+
Up and Down follow the camera, so they mean the same thing whichever view you are looking
197+
through. Rotation is about the product's own centre, so it never drifts out of frame, and a
198+
multi-part assembly turns as one rigid piece. **Rotate Step** sets how far one press turns
199+
it — 90° by default, so four presses of the same button return to where you started.
200+
201+
Turning a product changes its bounding box, so **click Build Cameras again once you are
202+
done orienting**. Framing is measured across the whole batch, and it is measured before you
203+
start rotating.
204+
205+
### 5. Batch Render
175206

176207
Set an **Output Directory**, choose a **Render Mode**, and hit **Render All**.
177208

@@ -246,11 +277,18 @@ collection are not seen as products.
246277
`freecadcmd`, not the GUI `freecad` binary. Open `Window → Toggle System Console` (Windows)
247278
to see the subprocess output.
248279

249-
**Products are all different sizes in the renders**: run *Standardize Products* with
250-
**Rescale** enabled.
280+
**Products are all different sizes in the renders**: run *Standardize Products* in the
281+
Standardize panel with **Rescale** enabled.
251282

252283
**The "Front" view isn't the front**: change **Front Axis** in Cameras / Views and rebuild
253-
the cameras.
284+
the cameras. If only some products are wrong, Front Axis will not help — it moves the whole
285+
batch. Use *Orient Products* to turn the offenders individually, then rebuild.
286+
287+
**Only one product is visible in the viewport**: you left a *Previous* / *Next* review pass
288+
part way through, which excludes the others. Click **Show All** in Cameras / Views.
289+
290+
**Renders are cropped after rotating products**: rotating changes the bounding box the
291+
framing was measured from. Click *Build Cameras* again.
254292

255293
**Imported STEP geometry is blocky**: lower **Tessellation Deflection** in preferences
256294
(try `0.02`) and re-import.

‎multiview_product_renderer/__init__.py‎

Lines changed: 214 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@
9090
"1. Scene Setup: pick a lighting preset and background, then Apply.",
9191
"2. Import: point to FreeCAD or STEPper, add STEP files, click Import.",
9292
" Each file becomes a collection under MV_Products.",
93-
" Fit on Import scales each one into a Target Size cube at the origin.",
94-
" You can also create empty product collections and drop meshes in,",
95-
" then click Standardize Products to fit them the same way.",
96-
"3. Cameras: pick a view set and front axis, click Build.",
97-
"4. Render: set output folder, mode (full/clay/both), starting index,",
93+
" You can also create empty product collections and drop meshes in.",
94+
"3. Standardize: centre and scale every product to one size, and fix",
95+
" the faceted shading a CAD tessellation arrives with. Fit on Import",
96+
" does the same automatically as each file lands.",
97+
"4. Cameras: pick a view set and front axis, click Build.",
98+
" Then use Orient Products to walk the batch one at a time and turn",
99+
" anything that faces the wrong way, before rebuilding the cameras.",
100+
"5. Render: set output folder, mode (full/clay/both), starting index,",
98101
" then Render All. Files are named PREFIX_NN_Product_View.",
99102
"",
100103
"Render engine, samples, resolution and file format stay under your",
@@ -268,6 +271,19 @@ class MV_Settings(PropertyGroup):
268271

269272
show_products_list: BoolProperty(default=False)
270273

274+
active_product_index: IntProperty(
275+
name="Active Product",
276+
description="Which product the orientation buttons act on",
277+
default=0, min=0,
278+
)
279+
rotate_step: FloatProperty(
280+
name="Rotate Step",
281+
description="How far one press of Left/Right/Up/Down turns the product",
282+
default=math.radians(90.0),
283+
min=math.radians(1.0), max=math.radians(180.0),
284+
subtype='ANGLE',
285+
)
286+
271287

272288
def ensure_collection(name, parent=None):
273289
coll = bpy.data.collections.get(name)
@@ -540,6 +556,78 @@ def find_layer_coll(layer_coll, name):
540556
return None
541557

542558

559+
def isolate_product(context, name):
560+
"""Show one product and exclude the rest from the view layer.
561+
562+
Deliberately the same mechanism Render All uses rather than a viewport-only
563+
hide, so stepping through products previews exactly what a batch will
564+
render. Pass name=None to bring everything back.
565+
"""
566+
for pc in get_products():
567+
lc = find_layer_coll(context.view_layer.layer_collection, pc.name)
568+
if lc:
569+
lc.exclude = name is not None and pc.name != name
570+
571+
572+
def active_product(context):
573+
"""The product the orientation buttons act on, or None if there are none.
574+
575+
The stored index is clamped rather than trusted: products get deleted from
576+
the outliner behind the add-on's back, and a stale index would otherwise
577+
raise the moment someone pressed Rotate.
578+
"""
579+
products = get_products()
580+
if not products:
581+
return None, -1
582+
idx = min(max(context.scene.mv_settings.active_product_index, 0), len(products) - 1)
583+
return products[idx], idx
584+
585+
586+
def view_right_axis(context):
587+
"""World-space axis that reads as horizontal in the camera being used.
588+
589+
Up and Down tip the product about whatever is horizontal on screen, so the
590+
buttons behave the same whether you are looking through Front or Top. Falls
591+
back to world Y when no camera exists to ask.
592+
"""
593+
cam = context.scene.camera
594+
if cam is None or cam.type != 'CAMERA':
595+
cam_coll = bpy.data.collections.get(CAMERA_COLL)
596+
cams = [o for o in cam_coll.objects if o.type == 'CAMERA'] if cam_coll else []
597+
cam = cams[0] if cams else None
598+
if cam is None:
599+
return Vector((0.0, 1.0, 0.0))
600+
axis = cam.matrix_world.col[0].xyz
601+
return axis.normalized() if axis.length > 1e-9 else Vector((0.0, 1.0, 0.0))
602+
603+
604+
def rotate_product(context, coll, direction, step):
605+
"""Turn one product in place, as seen from the current view.
606+
607+
Left and Right spin about world Z, which is the turntable move that fixes a
608+
product facing the wrong way. Up and Down tip about the view's horizontal
609+
axis. Rotation happens around the product's own bounding box centre so it
610+
does not drift out of frame, and is applied to the collection roots so a
611+
multi-part assembly turns as one rigid body.
612+
"""
613+
objs = collection_meshes(coll)
614+
if not objs:
615+
return False
616+
center, _ = bbox_of(objs)
617+
if direction in {'LEFT', 'RIGHT'}:
618+
axis = Vector((0.0, 0.0, 1.0))
619+
angle = -step if direction == 'LEFT' else step
620+
else:
621+
axis = view_right_axis(context)
622+
angle = -step if direction == 'UP' else step
623+
xform = (Matrix.Translation(center)
624+
@ Matrix.Rotation(angle, 4, axis)
625+
@ Matrix.Translation(-center))
626+
for obj in collection_roots(coll):
627+
obj.matrix_world = xform @ obj.matrix_world
628+
return True
629+
630+
543631
def alpha_pair(idx):
544632
"""Repeated-letter group label: AA..ZZ, then AAA..ZZZ, then AAAA..
545633
@@ -1323,6 +1411,74 @@ def execute(self, context):
13231411
return {'FINISHED'}
13241412

13251413

1414+
class MV_OT_RotateProduct(Operator):
1415+
"""Turn the active product 90 degrees so it faces the way the view set expects"""
1416+
bl_idname = "mv.rotate_product"
1417+
bl_label = "Rotate Product"
1418+
bl_options = {'REGISTER', 'UNDO'}
1419+
1420+
direction: EnumProperty(
1421+
items=[('LEFT', "Left", "Spin left about the vertical axis"),
1422+
('RIGHT', "Right", "Spin right about the vertical axis"),
1423+
('UP', "Up", "Tip the front face up"),
1424+
('DOWN', "Down", "Tip the front face down")],
1425+
default='LEFT',
1426+
)
1427+
1428+
def execute(self, context):
1429+
coll, _ = active_product(context)
1430+
if coll is None:
1431+
self.report({'ERROR'}, f"No product collections under '{PRODUCT_COLL}'.")
1432+
return {'CANCELLED'}
1433+
step = context.scene.mv_settings.rotate_step
1434+
if not rotate_product(context, coll, self.direction, step):
1435+
self.report({'WARNING'}, f"'{coll.name}' has no geometry to rotate.")
1436+
return {'CANCELLED'}
1437+
self.report({'INFO'},
1438+
f"Rotated {coll.name} {self.direction.lower()} "
1439+
f"{math.degrees(step):.0f} degrees.")
1440+
return {'FINISHED'}
1441+
1442+
1443+
class MV_OT_ProductStep(Operator):
1444+
"""Show the next or previous product on its own, hiding the others"""
1445+
bl_idname = "mv.product_step"
1446+
bl_label = "Step Product"
1447+
bl_options = {'REGISTER', 'UNDO'}
1448+
1449+
delta: IntProperty(default=1)
1450+
1451+
def execute(self, context):
1452+
products = get_products()
1453+
if not products:
1454+
self.report({'ERROR'}, f"No product collections under '{PRODUCT_COLL}'.")
1455+
return {'CANCELLED'}
1456+
s = context.scene.mv_settings
1457+
_, idx = active_product(context)
1458+
# Wraps, so a review pass round a batch never dead-ends on the last one.
1459+
idx = (idx + self.delta) % len(products)
1460+
s.active_product_index = idx
1461+
isolate_product(context, products[idx].name)
1462+
self.report({'INFO'}, f"{products[idx].name} ({idx + 1}/{len(products)})")
1463+
return {'FINISHED'}
1464+
1465+
1466+
class MV_OT_ShowAllProducts(Operator):
1467+
"""Bring every product back into the view layer"""
1468+
bl_idname = "mv.show_all_products"
1469+
bl_label = "Show All"
1470+
bl_options = {'REGISTER', 'UNDO'}
1471+
1472+
def execute(self, context):
1473+
products = get_products()
1474+
if not products:
1475+
self.report({'ERROR'}, f"No product collections under '{PRODUCT_COLL}'.")
1476+
return {'CANCELLED'}
1477+
isolate_product(context, None)
1478+
self.report({'INFO'}, f"Showing all {len(products)} product(s).")
1479+
return {'FINISHED'}
1480+
1481+
13261482
class MV_OT_AddProductCollection(Operator):
13271483
bl_idname = "mv.add_product_collection"
13281484
bl_label = "Add Empty Product Collection"
@@ -1558,22 +1714,35 @@ def draw(self, context):
15581714
for pc in prods:
15591715
box.label(text=f"{pc.name} ({len(pc.all_objects)} objs)", icon='OUTLINER_COLLECTION')
15601716

1717+
class MV_PT_Standardize(Panel):
1718+
bl_label = "Standardize"
1719+
bl_space_type = 'VIEW_3D'
1720+
bl_region_type = 'UI'
1721+
bl_category = "MultiView"
1722+
1723+
def draw(self, context):
1724+
layout = self.layout
1725+
s = context.scene.mv_settings
1726+
1727+
layout.prop(s, "fit_on_import")
1728+
layout.label(text="Runs as each STEP file lands.", icon='INFO')
1729+
15611730
layout.separator()
1562-
box = layout.box()
1563-
box.label(text="Standardization:")
1564-
box.prop(s, "fit_on_import")
1565-
row = box.row(align=True)
1731+
row = layout.row(align=True)
15661732
row.prop(s, "recenter", toggle=True)
15671733
row.prop(s, "rescale", toggle=True)
1568-
sub = box.row()
1734+
sub = layout.row()
15691735
sub.enabled = s.rescale or s.fit_on_import
15701736
sub.prop(s, "target_size")
1571-
row = box.row(align=True)
1737+
1738+
row = layout.row(align=True)
15721739
row.prop(s, "auto_smooth", toggle=True)
15731740
sub = row.row()
15741741
sub.enabled = s.auto_smooth
15751742
sub.prop(s, "smooth_angle", text="")
1576-
box.operator("mv.standardize", icon='FULLSCREEN_EXIT')
1743+
1744+
layout.separator()
1745+
layout.operator("mv.standardize", icon='FULLSCREEN_EXIT')
15771746

15781747

15791748
class MV_PT_Cameras(Panel):
@@ -1596,6 +1765,34 @@ def draw(self, context):
15961765

15971766
layout.operator("mv.build_cameras", icon='CAMERA_DATA')
15981767

1768+
layout.separator()
1769+
box = layout.box()
1770+
box.label(text="Orient Products:")
1771+
1772+
products = get_products()
1773+
if not products:
1774+
box.label(text="No products to orient.", icon='INFO')
1775+
return
1776+
1777+
coll, idx = active_product(context)
1778+
box.label(text=f"{coll.name} ({idx + 1}/{len(products)})",
1779+
icon='OUTLINER_COLLECTION')
1780+
box.prop(s, "rotate_step")
1781+
1782+
col = box.column(align=True)
1783+
row = col.row(align=True)
1784+
row.operator("mv.rotate_product", text="Left", icon='TRIA_LEFT').direction = 'LEFT'
1785+
row.operator("mv.rotate_product", text="Right", icon='TRIA_RIGHT').direction = 'RIGHT'
1786+
row = col.row(align=True)
1787+
row.operator("mv.rotate_product", text="Up", icon='TRIA_UP').direction = 'UP'
1788+
row.operator("mv.rotate_product", text="Down", icon='TRIA_DOWN').direction = 'DOWN'
1789+
1790+
row = box.row(align=True)
1791+
row.operator("mv.product_step", text="Previous", icon='FRAME_PREV').delta = -1
1792+
row.operator("mv.product_step", text="Next", icon='FRAME_NEXT').delta = 1
1793+
box.operator("mv.show_all_products", icon='HIDE_OFF')
1794+
box.label(text="Rebuild cameras after rotating to reframe.", icon='INFO')
1795+
15991796

16001797
class MV_PT_Render(Panel):
16011798
bl_label = "Batch Render"
@@ -1651,12 +1848,17 @@ def draw(self, context):
16511848
MV_OT_RemoveCustomView,
16521849
MV_OT_AddProductCollection,
16531850
MV_OT_Standardize,
1851+
MV_OT_RotateProduct,
1852+
MV_OT_ProductStep,
1853+
MV_OT_ShowAllProducts,
16541854
MV_OT_RenderAll,
16551855
MV_UL_StepFiles,
16561856
MV_UL_CustomViews,
1857+
# Panels draw in the sidebar in the order they are registered.
16571858
MV_PT_Guide,
16581859
MV_PT_SceneSetup,
16591860
MV_PT_Import,
1861+
MV_PT_Standardize,
16601862
MV_PT_Cameras,
16611863
MV_PT_Render,
16621864
)

0 commit comments

Comments
 (0)