Skip to content

Commit cee6efb

Browse files
Model Definitions fixes
1 parent cb3f515 commit cee6efb

13 files changed

Lines changed: 126 additions & 6 deletions

README.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ XLIGHTS/NUTCRACKER RELEASE NOTES
283283
-bug (cybercop23) Fix Face and State effect definition choices when switching between different
284284
model groups.
285285
-bug (derwin12) Fixed locked effects duplicating when dragged across rows/layers (#6697)
286+
-bug (scott) Fix a crash clicking in the Model Definitions preview (Faces/States)
287+
with nothing selected in the name list
288+
-enh (scott) Model Definitions: the preview's pencil tool and click-drag node
289+
selection are disabled when nothing is selected, a SubModel is in
290+
SubBuffer mode, or a Face is in Matrix mode; the pencil is also disabled
291+
in Single Node mode, though click-drag there still works and picks the
292+
first node touched
286293

287294
2026.15 August 4, 2026
288295
-change (dkulp) Faces: the automatic eye blink is now computed independently per frame, so

src-iPad/App/LayoutEditorView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7516,7 +7516,8 @@ private struct SubModelDetailEditor: View {
75167516
highlightedNodes: highlightedNodes,
75177517
onToggleNode: toggleNode,
75187518
onAddNodes: addNodes,
7519-
controller: previewController)
7519+
controller: previewController,
7520+
nodesEditable: entry.isRanges)
75207521
.background(Color.black)
75217522
.clipShape(RoundedRectangle(cornerRadius: 8))
75227523
Text(entry.isRanges

src-iPad/App/SubmodelPreviewPane.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ struct SubmodelPreviewPane: UIViewRepresentable {
4949
let onToggleNode: (Int) -> Void
5050
let onAddNodes: ([Int]) -> Void
5151
var controller: SubmodelPreviewController? = nil
52+
// Tap-to-toggle and marquee only make sense for a Ranges/Lines submodel;
53+
// a Sub-buffer submodel isn't defined by a node list, so there's nothing
54+
// for either gesture to edit (mirrors desktop's SubModelsPanel::CanEditPreviewNodes).
55+
var nodesEditable: Bool = true
5256

5357
func makeCoordinator() -> Coordinator {
5458
Coordinator(parent: parentModelName)
@@ -78,6 +82,7 @@ struct SubmodelPreviewPane: UIViewRepresentable {
7882
context.coordinator.document = document
7983
context.coordinator.onToggleNode = onToggleNode
8084
context.coordinator.onAddNodes = onAddNodes
85+
context.coordinator.nodesEditable = nodesEditable
8186
controller?.coordinator = context.coordinator
8287

8388
let pinch = UIPinchGestureRecognizer(
@@ -135,6 +140,7 @@ struct SubmodelPreviewPane: UIViewRepresentable {
135140
context.coordinator.document = document
136141
context.coordinator.onToggleNode = onToggleNode
137142
context.coordinator.onAddNodes = onAddNodes
143+
context.coordinator.nodesEditable = nodesEditable
138144
controller?.coordinator = context.coordinator
139145
if context.coordinator.lastHighlight != highlightedNodes {
140146
context.coordinator.applyHighlights(highlightedNodes)
@@ -158,6 +164,7 @@ struct SubmodelPreviewPane: UIViewRepresentable {
158164
var document: XLSequenceDocument?
159165
var onToggleNode: ((Int) -> Void)?
160166
var onAddNodes: (([Int]) -> Void)?
167+
var nodesEditable: Bool = true
161168
var lastHighlight: [Int] = []
162169
private let parent: String
163170
private var pinchStartZoom: Float = 1.0
@@ -250,6 +257,7 @@ struct SubmodelPreviewPane: UIViewRepresentable {
250257
}
251258

252259
@objc func handleTap(_ g: UITapGestureRecognizer) {
260+
guard nodesEditable else { return }
253261
guard let bridge, let view = g.view, let document else { return }
254262
let pt = g.location(in: view)
255263
let n = bridge.nodeNearPoint(pt,
@@ -267,6 +275,7 @@ struct SubmodelPreviewPane: UIViewRepresentable {
267275
}
268276

269277
@objc func handleMarquee(_ g: UILongPressGestureRecognizer) {
278+
guard nodesEditable else { return }
270279
guard let view = g.view else { return }
271280
switch g.state {
272281
case .began:

src-ui-wx/layout/ModelPreview.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void ModelPreview::ResetPencilSize() {
8686
}
8787

8888
bool ModelPreview::IsPencilActive() const {
89-
return _supportsPencil && s_pencilSizeIndex > 0;
89+
return _supportsPencil && _pencilEnabled && s_pencilSizeIndex > 0;
9090
}
9191

9292
void ModelPreview::StartPaintPath(std::vector<xlPoint>& path, int x, int y, bool freeform) {
@@ -146,7 +146,7 @@ std::vector<float> ModelPreview::GetPencilStrokeOffsets() const {
146146
}
147147

148148
bool ModelPreview::HitTestPencilIcon(int x, int y) const {
149-
if (!_supportsPencil) return false;
149+
if (!_supportsPencil || !_pencilEnabled) return false;
150150
int w = mWindowWidth;
151151
if (w < 60) return false;
152152
return (x >= w - 44 && x <= w - 4 && y >= 4 && y <= 44);
@@ -185,7 +185,7 @@ void ModelPreview::AddPencilIconToAccumulator() {
185185
if (!_supportsPencil || solidProgram == nullptr) return;
186186
auto acc = solidProgram->getAccumulator();
187187
int start = acc->getCount();
188-
const xlColor pencilColor = IsPencilActive() ? xlColor(255, 140, 0) : xlColor(170, 170, 170);
188+
const xlColor pencilColor = !_pencilEnabled ? xlColor(90, 90, 90) : (IsPencilActive() ? xlColor(255, 140, 0) : xlColor(170, 170, 170));
189189

190190
int w = mWindowWidth;
191191
int h = mWindowHeight;

src-ui-wx/layout/ModelPreview.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ class ModelPreview : public GRAPHICS_BASE_CLASS, public IModelPreview
194194
static void ResetPencilSize();
195195
// Only previews used for freeform node painting (faces/states/submodels) show the pencil
196196
void SetSupportsPencil(bool b) { _supportsPencil = b; }
197+
// Whether the pencil can be used right now (e.g. a submodel is selected and
198+
// it isn't in SubBuffer mode) -- distinct from _supportsPencil, which just
199+
// says this preview hosts the feature at all.
200+
void SetPencilEnabled(bool b) { _pencilEnabled = b; }
197201
bool IsPencilActive() const;
198202
float GetPencilCatchRadiusMultiplier() const override;
199203
std::vector<float> GetPencilStrokeOffsets() const;
@@ -255,6 +259,7 @@ class ModelPreview : public GRAPHICS_BASE_CLASS, public IModelPreview
255259
bool _center2D0 = false;
256260
bool scaleImage = false;
257261
bool _supportsPencil = false;
262+
bool _pencilEnabled = true;
258263
bool allowSelected;
259264
bool allowPreviewChange;
260265
ControllerObjectContext _controllerObjectContext = ControllerObjectContext::None;

src-ui-wx/model/ModelFacesPanel.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,19 @@ void ModelFacesPanel::SelectFaceModel(const std::string& name)
731731
void ModelFacesPanel::OnMatrixNameChoiceSelect(wxCommandEvent& event)
732732
{
733733
SelectFaceModel(NameChoice->GetString(NameChoice->GetSelection()).ToStdString());
734+
if (_modelPreview) {
735+
_modelPreview->SetPencilEnabled(CanUsePencil());
736+
}
737+
}
738+
739+
bool ModelFacesPanel::CanEditPreviewNodes() const
740+
{
741+
return NameChoice->GetSelection() != wxNOT_FOUND && FaceTypeChoice->GetSelection() != MATRIX_FACE;
742+
}
743+
744+
bool ModelFacesPanel::CanUsePencil() const
745+
{
746+
return CanEditPreviewNodes() && FaceTypeChoice->GetSelection() != SINGLE_NODE_FACE;
734747
}
735748

736749
void ModelFacesPanel::OnButtonMatrixAddClicked(wxCommandEvent& event)
@@ -1294,6 +1307,9 @@ void ModelFacesPanel::OnFaceTypeChoicePageChanged(wxChoicebookEvent& event)
12941307
}
12951308
SelectFaceModel(name);
12961309
UpdatePreview("", *wxWHITE);
1310+
if (_modelPreview) {
1311+
_modelPreview->SetPencilEnabled(CanUsePencil());
1312+
}
12971313
}
12981314

12991315
void ModelFacesPanel::OnNodeRangeGridCellLeftDClick(wxGridEvent& event)
@@ -1897,6 +1913,7 @@ void ModelFacesPanel::OnPreviewMouseLeave(wxMouseEvent& event)
18971913
void ModelFacesPanel::OnPreviewLeftDown(wxMouseEvent& event)
18981914
{
18991915
if (!_isActive) return;
1916+
if (!CanEditPreviewNodes()) return;
19001917
if (_modelPreview && _modelPreview->HitTestPencilIcon(event.GetX(), event.GetY())) {
19011918
_modelPreview->ShowPencilSizeMenu();
19021919
return;
@@ -1916,6 +1933,7 @@ void ModelFacesPanel::OnPreviewLeftDown(wxMouseEvent& event)
19161933
void ModelFacesPanel::OnPreviewLeftDClick(wxMouseEvent& event)
19171934
{
19181935
if (!_isActive) return;
1936+
if (!CanEditPreviewNodes()) return;
19191937
if (!_modelPreview) return;
19201938
glm::vec3 ray_origin;
19211939
glm::vec3 ray_direction;
@@ -2036,6 +2054,22 @@ void ModelFacesPanel::SelectAllInBoundingRect(bool shiftDwn, bool freeform)
20362054
return;
20372055
}
20382056

2057+
if (faceData[name]["Type"] == "SingleNode") {
2058+
int row = SingleNodeGrid->GetGridCursorRow();
2059+
if (row < 0)
2060+
return;
2061+
std::vector<int> nodes = GetDragSelectedNodes(freeform);
2062+
if (nodes.empty())
2063+
return;
2064+
// Single Node rows hold exactly one node -- take the first node the
2065+
// drag/pencil touched rather than building a range out of all of them.
2066+
std::string node = model->GetNodeName(nodes[0] - 1, true);
2067+
SingleNodeGrid->SetCellValue(row, CHANNEL_COL, node);
2068+
SingleNodeGrid->Refresh();
2069+
GetValue(SingleNodeGrid, row, CHANNEL_COL, faceData[name]);
2070+
return;
2071+
}
2072+
20392073
if (faceData[name]["Type"] != "NodeRange") {
20402074
return;
20412075
}

src-ui-wx/model/ModelFacesPanel.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ class ModelFacesPanel : public wxPanel {
178178
void StartOutputToLights();
179179
bool StopOutputToLights();
180180
void SelectFaceModel(const std::string& s);
181+
// Pencil/click-drag node editing on the preview requires a face to be selected --
182+
// NameChoice->GetSelection() is wxNOT_FOUND otherwise, and SelectAllInBoundingRect's
183+
// NameChoice->GetString(GetSelection()) call would hit a wx assertion.
184+
bool CanEditPreviewNodes() const;
185+
// Pencil-specific: Single Node rows hold one node each, so freeform
186+
// painting (which collects a whole swath) doesn't apply -- plain
187+
// click-drag still works there and just takes the first node hit.
188+
bool CanUsePencil() const;
181189
void UpdatePreview(const std::string& channels, wxColor c);
182190
std::vector<int> GetSelectedGridRows(wxGrid* grid, int fallbackRow);
183191
void UpdatePreviewRows(wxGrid* grid, int fallbackRow);

src-ui-wx/model/ModelStatesPanel.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ void ModelStatesPanel::UpdateStateType()
925925
}
926926
SelectStateModel(name);
927927
if (model) ClearNodeColor(model);
928+
ValidateWindow();
928929
}
929930

930931
void ModelStatesPanel::OnNodeRangeGridCellLeftDClick(wxGridEvent& event)
@@ -1226,6 +1227,20 @@ void ModelStatesPanel::ValidateWindow()
12261227
}
12271228
}
12281229
}
1230+
1231+
if (_modelPreview) {
1232+
_modelPreview->SetPencilEnabled(CanUsePencil());
1233+
}
1234+
}
1235+
1236+
bool ModelStatesPanel::CanEditPreviewNodes() const
1237+
{
1238+
return NameChoice->GetSelection() != wxNOT_FOUND;
1239+
}
1240+
1241+
bool ModelStatesPanel::CanUsePencil() const
1242+
{
1243+
return CanEditPreviewNodes() && StateTypeChoice->GetSelection() != SINGLE_NODE_STATE;
12291244
}
12301245

12311246
bool ModelStatesPanel::HasInvalidRows() const
@@ -1700,6 +1715,7 @@ void ModelStatesPanel::OnPreviewMouseLeave(wxMouseEvent& event)
17001715
void ModelStatesPanel::OnPreviewLeftDown(wxMouseEvent& event)
17011716
{
17021717
if (!_isActive) return;
1718+
if (!CanEditPreviewNodes()) return;
17031719
if (_modelPreview && _modelPreview->HitTestPencilIcon(event.GetX(), event.GetY())) {
17041720
_modelPreview->ShowPencilSizeMenu();
17051721
return;
@@ -1721,6 +1737,7 @@ void ModelStatesPanel::OnPreviewLeftDown(wxMouseEvent& event)
17211737
void ModelStatesPanel::OnPreviewLeftDClick(wxMouseEvent& event)
17221738
{
17231739
if (!_isActive) return;
1740+
if (!CanEditPreviewNodes()) return;
17241741
if (!_modelPreview) return;
17251742
glm::vec3 ray_origin;
17261743
glm::vec3 ray_direction;
@@ -1846,6 +1863,22 @@ void ModelStatesPanel::SelectAllInBoundingRect(bool shiftDwn, bool freeform)
18461863
return;
18471864
}
18481865

1866+
if (stateData[name]["Type"] == "SingleNode") {
1867+
int row = SingleNodeGrid->GetGridCursorRow();
1868+
if (row < 0)
1869+
return;
1870+
std::vector<int> nodes = GetDragSelectedNodes(freeform);
1871+
if (nodes.empty())
1872+
return;
1873+
// Single Node rows hold exactly one node -- take the first node the
1874+
// drag/pencil touched rather than building a range out of all of them.
1875+
std::string node = model->GetNodeName(nodes[0] - 1, true);
1876+
SingleNodeGrid->SetCellValue(row, CHANNEL_COL, node);
1877+
SingleNodeGrid->Refresh();
1878+
GetValue(SingleNodeGrid, row, CHANNEL_COL, stateData[name]);
1879+
return;
1880+
}
1881+
18491882
if (stateData[name]["Type"] != "NodeRange") {
18501883
return;
18511884
}

src-ui-wx/model/ModelStatesPanel.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ class ModelStatesPanel : public wxPanel {
172172
void GetValue(wxGrid* grid, const int row, const int col, std::map<std::string, std::string>& info);
173173
void AddLabel(wxString label);
174174
void ValidateWindow();
175+
// Pencil/click-drag node editing on the preview requires a state to be selected --
176+
// NameChoice->GetSelection() is wxNOT_FOUND otherwise, and SelectAllInBoundingRect's
177+
// NameChoice->GetString(GetSelection()) call would hit a wx assertion.
178+
bool CanEditPreviewNodes() const;
179+
// Pencil-specific: Single Node rows hold one node each, so freeform
180+
// painting (which collects a whole swath) doesn't apply -- plain
181+
// click-drag still works there and just takes the first node hit.
182+
bool CanUsePencil() const;
175183
void PopulateStateFilter();
176184
void UpdateStateFilter();
177185
void BuildStateAnimSteps();

src-ui-wx/model/SubModelsPanel.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,15 @@ void SubModelsPanel::ValidateWindow()
19261926
_animPlaying ||
19271927
(ListCtrl_SubModels->GetSelectedItemCount() == 1 && TypeNotebook->GetSelection() == 0)
19281928
);
1929+
1930+
if (_modelPreview) {
1931+
_modelPreview->SetPencilEnabled(CanEditPreviewNodes());
1932+
}
1933+
}
1934+
1935+
bool SubModelsPanel::CanEditPreviewNodes() const
1936+
{
1937+
return ListCtrl_SubModels->GetSelectedItemCount() == 1 && TypeNotebook->GetSelection() == 0;
19291938
}
19301939

19311940
void SubModelsPanel::UnSelectAll()
@@ -2682,6 +2691,7 @@ void SubModelsPanel::OnPreviewMouseLeave(wxMouseEvent& event)
26822691
void SubModelsPanel::OnPreviewLeftDown(wxMouseEvent& event)
26832692
{
26842693
if (!_isActive) return;
2694+
if (!CanEditPreviewNodes()) return;
26852695
if (_modelPreview && _modelPreview->HitTestPencilIcon(event.GetX(), event.GetY())) {
26862696
_modelPreview->ShowPencilSizeMenu();
26872697
return;
@@ -2705,6 +2715,7 @@ void SubModelsPanel::OnPreviewLeftDown(wxMouseEvent& event)
27052715
void SubModelsPanel::OnPreviewLeftDClick(wxMouseEvent& event)
27062716
{
27072717
if (!_isActive) return;
2718+
if (!CanEditPreviewNodes()) return;
27082719
glm::vec3 ray_origin;
27092720
glm::vec3 ray_direction;
27102721
GetMouseLocation(event.GetX(), event.GetY(), ray_origin, ray_direction);

0 commit comments

Comments
 (0)