Skip to content

Commit d64553d

Browse files
feat: add actor_org_tools and advanced_alignment — 16 new tools
actor_org_tools (10 tools — Actor Organization): - actor_attach_to_parent: last selected becomes parent (Maya-style) - actor_detach: detach selection from parent, preserve world transform - actor_move_to_folder / actor_move_to_root: folder management - actor_rename_folder: re-path all actors from old → new folder - actor_select_by_folder / actor_select_same_folder / actor_select_by_class - actor_folder_list: full folder map with actor counts - actor_match_transform: copy loc/rot/scale from first to rest advanced_alignment (6 tools — Alignment): - align_to_reference: snap axis to first/last selected actor - distribute_with_gap: exact cm gap between bounding boxes - rotate_around_pivot: orbit selection around center or first actor - align_to_surface: snap_objects_to_floor with Z offset - match_spacing: even pivot spacing between first and last - align_to_grid_two_points: local grid from two anchor actors Dashboard: Advanced Alignment and Actor Organization groups added to Bulk Ops tab
1 parent f57e05d commit d64553d

4 files changed

Lines changed: 1252 additions & 0 deletions

File tree

Content/Python/UEFN_Toolbelt/dashboard_pyside6.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,95 @@ def _tab_bulk_ops(R) -> "QScrollArea":
657657
_btn(g5, "Stack Vertically", lambda: R("bulk_stack"))
658658
_btn(g5, "Normalize Scale →1", lambda: R("bulk_normalize_scale"))
659659

660+
# ── Advanced Alignment ─────────────────────────────────────────────────
661+
g_adv = _group(L, "Advanced Alignment")
662+
663+
adv_axis_combo = QComboBox(); adv_axis_combo.addItems(["X","Y","Z"]); adv_axis_combo.setCurrentIndex(2); adv_axis_combo.setFixedWidth(55)
664+
adv_ref_combo = QComboBox(); adv_ref_combo.addItems(["first","last"]); adv_ref_combo.setFixedWidth(60)
665+
row_adv1 = QWidget(); h_adv1 = QHBoxLayout(row_adv1); h_adv1.setContentsMargins(0,0,0,0); h_adv1.setSpacing(4)
666+
h_adv1.addWidget(QLabel("Axis:")); h_adv1.addWidget(adv_axis_combo)
667+
h_adv1.addWidget(QLabel("Ref:")); h_adv1.addWidget(adv_ref_combo); h_adv1.addStretch()
668+
g_adv.addWidget(row_adv1)
669+
_btn(g_adv, "Align to Reference Actor",
670+
lambda: R("align_to_reference", axis=adv_axis_combo.currentText(), reference=adv_ref_combo.currentText()))
671+
672+
gap_s = _spin(0, -10000, 10000, width=80)
673+
gap_axis_combo = QComboBox(); gap_axis_combo.addItems(["X","Y","Z"]); gap_axis_combo.setFixedWidth(55)
674+
row_adv2 = QWidget(); h_adv2 = QHBoxLayout(row_adv2); h_adv2.setContentsMargins(0,0,0,0); h_adv2.setSpacing(4)
675+
h_adv2.addWidget(QLabel("Gap cm:")); h_adv2.addWidget(gap_s)
676+
h_adv2.addWidget(QLabel("Axis:")); h_adv2.addWidget(gap_axis_combo); h_adv2.addStretch()
677+
g_adv.addWidget(row_adv2)
678+
_btn(g_adv, "Distribute with Exact Gap",
679+
lambda: R("distribute_with_gap", axis=gap_axis_combo.currentText(), gap=gap_s.value()))
680+
681+
pivot_angle_s = _spin(90, -360, 360, width=75)
682+
pivot_axis_combo = QComboBox(); pivot_axis_combo.addItems(["Z","X","Y"]); pivot_axis_combo.setFixedWidth(55)
683+
pivot_ref_combo = QComboBox(); pivot_ref_combo.addItems(["center","first"]); pivot_ref_combo.setFixedWidth(70)
684+
row_adv3 = QWidget(); h_adv3 = QHBoxLayout(row_adv3); h_adv3.setContentsMargins(0,0,0,0); h_adv3.setSpacing(4)
685+
h_adv3.addWidget(QLabel("°:")); h_adv3.addWidget(pivot_angle_s)
686+
h_adv3.addWidget(QLabel("Axis:")); h_adv3.addWidget(pivot_axis_combo)
687+
h_adv3.addWidget(QLabel("Pivot:")); h_adv3.addWidget(pivot_ref_combo); h_adv3.addStretch()
688+
g_adv.addWidget(row_adv3)
689+
_btn(g_adv, "Rotate Around Pivot",
690+
lambda: R("rotate_around_pivot", angle_deg=pivot_angle_s.value(),
691+
axis=pivot_axis_combo.currentText(), pivot=pivot_ref_combo.currentText()))
692+
693+
_row(g_adv,
694+
("Snap to Surface", lambda: R("align_to_surface")),
695+
("Match Spacing", lambda: R("match_spacing", axis=adv_axis_combo.currentText())),
696+
("Grid Two Points", lambda: R("align_to_grid_two_points")),
697+
)
698+
699+
# ── Actor Organization ─────────────────────────────────────────────────
700+
g_org = _group(L, "Actor Organization")
701+
702+
folder_inp = _inp("folder name", "MyGroup", width=160)
703+
_btn_inp(g_org, "Move Selection → Folder",
704+
lambda: R("actor_move_to_folder", folder_name=folder_inp.text() or "MyGroup"),
705+
folder_inp, tip="Moves all selected actors into a named World Outliner folder.")
706+
707+
_row(g_org,
708+
("Move to Root", lambda: R("actor_move_to_root")),
709+
("List All Folders", lambda: R("actor_folder_list")),
710+
)
711+
712+
old_folder_inp = _inp("old folder", "", width=130)
713+
new_folder_inp = _inp("new folder", "", width=130)
714+
row_ren = QWidget(); h_ren = QHBoxLayout(row_ren); h_ren.setContentsMargins(0,0,0,0); h_ren.setSpacing(4)
715+
h_ren.addWidget(QLabel("From:")); h_ren.addWidget(old_folder_inp)
716+
h_ren.addWidget(QLabel("To:")); h_ren.addWidget(new_folder_inp); h_ren.addStretch()
717+
g_org.addWidget(row_ren)
718+
_btn(g_org, "Rename Folder",
719+
lambda: R("actor_rename_folder", old_folder=old_folder_inp.text(), new_folder=new_folder_inp.text()))
720+
721+
sel_folder_inp = _inp("folder name", "", width=160)
722+
_btn_inp(g_org, "Select All in Folder",
723+
lambda: R("actor_select_by_folder", folder_name=sel_folder_inp.text()),
724+
sel_folder_inp)
725+
726+
class_inp = _inp("class name e.g. PlayerStart", "", width=200)
727+
_btn_inp(g_org, "Select All by Class",
728+
lambda: R("actor_select_by_class", class_filter=class_inp.text()),
729+
class_inp)
730+
731+
_row(g_org,
732+
("Select Same Folder", lambda: R("actor_select_same_folder")),
733+
("Attach to Parent", lambda: R("actor_attach_to_parent")),
734+
("Detach", lambda: R("actor_detach")),
735+
)
736+
737+
loc_chk = QCheckBox("Loc"); loc_chk.setChecked(True)
738+
rot_chk = QCheckBox("Rot"); rot_chk.setChecked(True)
739+
scl_chk2 = QCheckBox("Scale"); scl_chk2.setChecked(False)
740+
row_mt = QWidget(); h_mt = QHBoxLayout(row_mt); h_mt.setContentsMargins(0,0,0,0); h_mt.setSpacing(6)
741+
h_mt.addWidget(loc_chk); h_mt.addWidget(rot_chk); h_mt.addWidget(scl_chk2); h_mt.addStretch()
742+
g_org.addWidget(row_mt)
743+
_btn(g_org, "Match Transform (first → others)",
744+
lambda: R("actor_match_transform",
745+
copy_location=loc_chk.isChecked(),
746+
copy_rotation=rot_chk.isChecked(),
747+
copy_scale=scl_chk2.isChecked()))
748+
660749
# Verse (Schema-Driven)
661750
g6 = _group(L, "Verse Property Hardening")
662751
prop_inp = _inp("bIsEnabled", "bIsEnabled", width=140)

Content/Python/UEFN_Toolbelt/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,5 @@
5454
from . import verse_device_graph
5555
from . import project_setup
5656
from . import sign_tools
57+
from . import actor_org_tools
58+
from . import advanced_alignment

0 commit comments

Comments
 (0)