Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Issue Tracker is found here: www.github.com/xLightsSequencer/xLights/issues
XLIGHTS/NUTCRACKER RELEASE NOTES
---------------------------------
2026.07 April ??, 2026
-enh (heffneil) Layout tab: Bulk Edit menu now includes Rotate X / Rotate Y / Rotate Z items so a
rotation can be applied to every selected model in one operation, with the tree
selection preserved after the change.
-enh (dkulp) Shape effect: Effect panel now organises its 27 properties into Shape / Size / Motion /
Triggers tabs instead of a flat scroll, matching the grouped layout other large effects use.
-enh (PB) Value curve Exponential, Logarithmic, and Parabolic types now support Start/End.
Expand Down
98 changes: 98 additions & 0 deletions src-ui-wx/layout/LayoutPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ const long LayoutPanel::ID_PREVIEW_BULKEDIT_CONTROLLERCONNECTIONINCREMENT = wxNe
const long LayoutPanel::ID_PREVIEW_BULKEDIT_SMARTREMOTETYPE = wxNewId();
const long LayoutPanel::ID_PREVIEW_BULKEDIT_PREVIEW = wxNewId();
const long LayoutPanel::ID_PREVIEW_BULKEDIT_DIMMINGCURVES = wxNewId();
const long LayoutPanel::ID_PREVIEW_BULKEDIT_ROTATEX = wxNewId();
const long LayoutPanel::ID_PREVIEW_BULKEDIT_ROTATEY = wxNewId();
const long LayoutPanel::ID_PREVIEW_BULKEDIT_ROTATEZ = wxNewId();
const long LayoutPanel::ID_PREVIEW_ALIGN_TOP = wxNewId();
const long LayoutPanel::ID_PREVIEW_ALIGN_GROUND = wxNewId();
const long LayoutPanel::ID_PREVIEW_ALIGN_BOTTOM = wxNewId();
Expand Down Expand Up @@ -2265,6 +2268,84 @@ void LayoutPanel::BulkEditPixelSize() {
}
}

void LayoutPanel::BulkEditRotateX() { BulkEditRotateAxis('X'); }
void LayoutPanel::BulkEditRotateY() { BulkEditRotateAxis('Y'); }
void LayoutPanel::BulkEditRotateZ() { BulkEditRotateAxis('Z'); }

void LayoutPanel::BulkEditRotateAxis(char axis) {
std::vector<Model*> modelsToEdit = GetSelectedModelsForEdit();
// remember the selected models so we can restore the selection after the reload
std::vector<std::list<std::string>> selectedModelPaths = GetSelectedTreeModelPaths();

// Pre-fill the dialog with the first unlocked model's current rotation so
// the user only has to type a value if they want to change it.
float initial = 0.0f;
for (Model* model : modelsToEdit) {
if (model != nullptr && !model->GetBaseObjectScreenLocation().IsLocked()) {
switch (axis) {
case 'X': initial = model->GetBaseObjectScreenLocation().GetRotateX(); break;
case 'Y': initial = model->GetBaseObjectScreenLocation().GetRotateY(); break;
case 'Z': initial = model->GetBaseObjectScreenLocation().GetRotateZ(); break;
}
break;
}
}

wxString title = wxString::Format("Bulk Edit Rotate %c", axis);
wxString prompt = wxString::Format("Rotate %c (degrees):", axis);
wxTextEntryDialog dlg(this, prompt, title, wxString::Format("%g", initial));
OptimiseDialogPosition(&dlg);
if (dlg.ShowModal() != wxID_OK) {
return;
}

// Parse the entered angle. Use strtod (not std::stod) because xLights has
// effectively no exception handling - see CLAUDE.md "Prefer std::* Over wx*".
std::string entered = dlg.GetValue().ToStdString();
char* endp = nullptr;
double angle = std::strtod(entered.c_str(), &endp);
if (endp == entered.c_str()) {
// Nothing parsed - abort silently rather than setting 0.
return;
}
Comment thread
heffneil marked this conversation as resolved.
float newAngle = static_cast<float>(angle);

// Snapshot the full models XML before any changes so a single Ctrl-Z
// rolls back the entire bulk rotation as one undo step. "All" type
// captures the complete state - this is the same pattern used by
// multi-model delete / group operations elsewhere in LayoutPanel.
// We defer taking the snapshot until after the dialog is accepted and
// the value parsed, so Cancel / invalid input doesn't leave a no-op
// entry on the undo stack.
CreateUndoPoint("All", wxString::Format("BulkRotate%c", axis).ToStdString(), entered);
Comment thread
heffneil marked this conversation as resolved.
Outdated

int changed = 0;
for (Model* model : modelsToEdit) {
if (model == nullptr) continue;
Comment thread
heffneil marked this conversation as resolved.
Outdated
auto& loc = model->GetBaseObjectScreenLocation();
if (loc.IsLocked()) continue;
switch (axis) {
case 'X': loc.SetRotateX(newAngle); break;
case 'Y': loc.SetRotateY(newAngle); break;
case 'Z': loc.SetRotateZ(newAngle); break;
}
++changed;
}

if (changed == 0) {
return;
}

// Match the persistence + selection-restore pattern used by BulkEditPixelSize
// and the other bulk-edit commands: clear, reload, then reselect the tree
// rows so the user sees the same models still selected after the redraw.
xlights->GetOutputModelManager()->ClearSelectedModel();
xlights->GetOutputModelManager()->AddImmediateWork(
OutputModelManager::WORK_RELOAD_ALLMODELS,
wxString::Format("BulkEditRotate%c", axis).ToStdString());
ReselectTreeModels(selectedModelPaths);
}

void LayoutPanel::BulkEditPixelStyle() {
std::vector<Model*> modelsToEdit = GetSelectedModelsForEdit();
// remember the selected models
Expand Down Expand Up @@ -5386,6 +5467,11 @@ void LayoutPanel::AddBulkEditOptionsToMenu(wxMenu* mnuBulkEdit) {
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_BLACKTRANSPARENCY, "Black Transparency");
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_SHADOWMODELFOR, "Shadow Model For");

mnuBulkEdit->AppendSeparator();
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_ROTATEX, "Rotate X");
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_ROTATEY, "Rotate Y");
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_ROTATEZ, "Rotate Z");

mnuBulkEdit->AppendSeparator();
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_CONTROLLERCONNECTION, "Controller Port");
mnuBulkEdit->Append(ID_PREVIEW_BULKEDIT_CONTROLLERCONNECTIONINCREMENT, "Controller Port and Increment");
Expand Down Expand Up @@ -5607,6 +5693,12 @@ void LayoutPanel::OnPreviewModelPopup(wxCommandEvent& event)
BulkEditTagColour();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_PIXELSIZE) {
BulkEditPixelSize();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEX) {
BulkEditRotateX();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEY) {
BulkEditRotateY();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEZ) {
BulkEditRotateZ();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_PIXELSTYLE) {
BulkEditPixelStyle();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_TRANSPARENCY) {
Expand Down Expand Up @@ -8427,6 +8519,12 @@ void LayoutPanel::OnModelsPopup(wxCommandEvent& event) {
BulkEditTagColour();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_PIXELSIZE) {
BulkEditPixelSize();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEX) {
BulkEditRotateX();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEY) {
BulkEditRotateY();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_ROTATEZ) {
BulkEditRotateZ();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_PIXELSTYLE) {
BulkEditPixelStyle();
} else if (event.GetId() == ID_PREVIEW_BULKEDIT_TRANSPARENCY) {
Expand Down
7 changes: 7 additions & 0 deletions src-ui-wx/layout/LayoutPanel.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ class LayoutPanel: public wxPanel
static const long ID_PREVIEW_BULKEDIT_SMARTREMOTETYPE;
static const long ID_PREVIEW_BULKEDIT_PREVIEW;
static const long ID_PREVIEW_BULKEDIT_DIMMINGCURVES;
static const long ID_PREVIEW_BULKEDIT_ROTATEX;
static const long ID_PREVIEW_BULKEDIT_ROTATEY;
static const long ID_PREVIEW_BULKEDIT_ROTATEZ;
static const long ID_PREVIEW_ALIGN_TOP;
static const long ID_PREVIEW_ALIGN_BOTTOM;
static const long ID_PREVIEW_ALIGN_GROUND;
Expand Down Expand Up @@ -403,6 +406,10 @@ class LayoutPanel: public wxPanel
void BulkEditControllerPreview();
void BulkEditGroupControllerPreview();
void BulkEditDimmingCurves();
void BulkEditRotateX();
void BulkEditRotateY();
void BulkEditRotateZ();
void BulkEditRotateAxis(char axis);
void ReplaceModel();
void EditSubModelAlias();
void ShowNodeLayout();
Expand Down
Loading