1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
#pragma once
#ifdef BUILD_UI
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui.h"
#include <algorithm>
#include <cctype>
#include <map>
#include <string>
#include "ui/BaseBackend.h"
// Small shared building blocks for factory preview UIs (BaseFactoryUI::DrawUI).
namespace UI {
// Fixed body height for an asset card. The asset list precomputes each row's
// height (see MainView's manual clipper), so a card must never grow with its
// content — overflow scrolls inside this body instead.
constexpr float kAssetBodyHeight = 132.0f;
inline float AssetCardHeight() {
const ImGuiStyle& style = ImGui::GetStyle();
return ImGui::GetTextLineHeightWithSpacing() + style.ItemSpacing.y * 3 + kAssetBodyHeight;
}
// Asset name on the left, resource type on the right, then a divider.
inline void AssetHeader(const std::string& name, const std::string& type) {
ImGui::TextUnformatted(name.c_str());
const ImVec2 typeSize = ImGui::CalcTextSize(type.c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x - typeSize.x);
ImGui::TextDisabled("%s", type.c_str());
ImGui::Separator();
}
// Scrolling, fixed-height body region. Pair with EndAssetBody. The caller must
// have a unique ID on the ID stack (MainView pushes the asset index).
inline void BeginAssetBody() {
ImGui::BeginChild("##assetBody", ImVec2(0, kAssetBodyHeight), false);
}
inline void EndAssetBody() {
ImGui::EndChild();
}
// Aligned key/value line.
inline void KV(const char* label, const std::string& value) {
ImGui::TextDisabled("%-16s", label);
ImGui::SameLine();
ImGui::TextUnformatted(value.c_str());
}
// Shading setup for game display lists that impose their own render state.
// Maps a dropdown index to the ModelPart shade fields. "auto" imposes nothing
// (the DL renders as authored); the others force a combine + lighting.
struct ShadeSetup {
uint8_t gameShade; // 0 shade, 1 texture-modulate, 2 flat prim, 3 auto
bool unlit;
bool fullAmbient;
};
inline int ShadeSetupCount() {
return 6;
}
inline const char* ShadeSetupName(int idx) {
static const char* kNames[] = { "auto", "textured", "textured + lit", "vertex color", "vertex color + lit",
"flat" };
return kNames[idx >= 0 && idx < ShadeSetupCount() ? idx : 0];
}
// Maps a config name to a shade-setup index, or -1 if unrecognized. Accepts
// the display names case-insensitively and with '_' standing in for ' '
// (e.g. "textured + lit", "textured_lit", "TEXTURED + LIT").
inline int ShadeSetupIndexByName(const std::string& raw) {
if (raw.empty()) {
return -1;
}
std::string s;
for (char c : raw) {
if (c == '_') {
c = ' ';
}
s.push_back((char)std::tolower((unsigned char)c));
}
for (int i = 0; i < ShadeSetupCount(); ++i) {
if (s == ShadeSetupName(i)) {
return i;
}
}
return -1;
}
inline ShadeSetup ShadeSetupFor(int idx) {
switch (idx) {
case 1: return { 1, true, false }; // textured
case 2: return { 1, false, true }; // textured + lit (white ambient)
case 3: return { 0, true, false }; // vertex color
case 4: return { 0, false, true }; // vertex color + lit
case 5: return { 2, true, false }; // flat primitive
default: return { 3, false, false }; // auto — no imposed state
}
}
// Dropdown editing a shade-setup index; returns true when changed.
inline bool ShadeSetupCombo(const char* id, int& idx) {
idx = std::clamp(idx, 0, ShadeSetupCount() - 1);
bool changed = false;
ImGui::SetNextItemWidth(150.0f);
if (ImGui::BeginCombo(id, ShadeSetupName(idx))) {
for (int i = 0; i < ShadeSetupCount(); ++i) {
if (ImGui::Selectable(ShadeSetupName(i), i == idx)) {
idx = i;
changed = true;
}
}
ImGui::EndCombo();
}
return changed;
}
// Button + popup editing the shared preview light.
inline void LightingControls() {
if (ImGui::SmallButton("light")) {
ImGui::OpenPopup("##previewlight");
}
if (ImGui::BeginPopup("##previewlight")) {
PreviewLighting& light = GetPreviewLighting();
ImGui::Checkbox("enabled", &light.enabled);
ImGui::ColorEdit3("ambient", light.ambient, ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit3("color", light.color, ImGuiColorEditFlags_NoInputs);
ImGui::SetNextItemWidth(200.0f);
ImGui::SliderFloat3("position", light.position, -4.0f, 4.0f, "%.2f");
ImGui::SetNextItemWidth(200.0f);
ImGui::SliderFloat("intensity", &light.intensity, 0.0f, 3.0f, "%.2f");
ImGui::SetNextItemWidth(200.0f);
ImGui::SliderFloat("falloff", &light.falloff, 0.0f, 2.0f, "%.2f");
ImGui::Separator();
PreviewAtmosphere& atmo = GetPreviewAtmosphere();
ImGui::Checkbox("fog", &atmo.fogEnabled);
ImGui::ColorEdit3("fog color", atmo.fogColor, ImGuiColorEditFlags_NoInputs);
ImGui::SetNextItemWidth(200.0f);
ImGui::DragIntRange2("fog range", &atmo.fogStart, &atmo.fogEnd, 2, 0, 1000);
if (atmo.fogEnd <= atmo.fogStart) {
atmo.fogEnd = atmo.fogStart + 1;
}
if (ImGui::SmallButton("reset##light")) {
light = PreviewLighting{};
atmo = PreviewAtmosphere{};
}
ImGui::EndPopup();
}
}
// Camera controls for the item submitted just before this call: drag orbits,
// shift+drag or middle-drag pans, cmd/ctrl+scroll zooms, double-click resets.
inline void OrbitControls(OrbitView& view) {
ImGuiIO& io = ImGui::GetIO();
const bool hovered = ImGui::IsItemHovered();
const bool panning = (ImGui::IsItemActive() && io.KeyShift) ||
(hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle));
if (panning) {
view.panX += io.MouseDelta.x;
view.panY += io.MouseDelta.y;
} else if (ImGui::IsItemActive()) {
view.yaw -= io.MouseDelta.x * 0.01f;
view.pitch = std::clamp(view.pitch + io.MouseDelta.y * 0.01f, -1.55f, 1.55f);
}
// Zoom needs a modifier so plain scroll keeps scrolling the asset list.
if (hovered && io.MouseWheel != 0.0f && (io.KeyCtrl || io.KeySuper)) {
view.zoom = std::clamp(view.zoom * (1.0f + io.MouseWheel * 0.1f), 0.02f, 50.0f);
io.MouseWheel = 0.0f;
}
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
view = OrbitView{};
}
}
// Centered preview box (aspect capped at 3:1) with orbit controls and a dark
// backdrop. `visible` is an on-screen test against the scroll viewport.
struct PreviewCanvas {
ImVec2 origin;
ImVec2 size;
bool visible;
};
inline PreviewCanvas BeginPreviewCanvas(const char* strId, float height, OrbitView& view) {
const float availW = ImGui::GetContentRegionAvail().x;
const float vw = std::min(availW, height * 3.0f);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availW - vw) * 0.5f);
const ImVec2 origin = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(strId, ImVec2(vw, height));
const float winTop = ImGui::GetWindowPos().y;
const float winBot = winTop + ImGui::GetWindowHeight();
const bool visible = ImGui::GetItemRectMax().y > winTop && ImGui::GetItemRectMin().y < winBot;
OrbitControls(view);
if (visible) {
ImGui::GetWindowDrawList()->AddRectFilled(origin, ImVec2(origin.x + vw, origin.y + height),
IM_COL32(18, 18, 22, 255));
}
return { origin, ImVec2(vw, height), visible };
}
// Preview canvases can be resized vertically by dragging their bottom edge.
// The width keeps the default cap (it does not grow with the height). Heights
// are persisted per asset and read back by GetItemHeight so the virtualized
// asset list allocates the right row height.
constexpr float kPreviewDefaultHeight = 320.0f;
constexpr float kPreviewMinHeight = 140.0f;
constexpr float kPreviewMaxHeight = 1400.0f;
constexpr float kPreviewGripHeight = 11.0f;
inline std::map<std::string, float>& PreviewHeights() {
static std::map<std::string, float> heights;
return heights;
}
inline float PreviewHeight(const std::string& key) {
const auto it = PreviewHeights().find(key);
if (it != PreviewHeights().end()) {
return it->second;
}
if (const char* forced = std::getenv("TORCH_UI_CANVASH")) {
const float h = (float)atof(forced);
if (h >= kPreviewMinHeight && h <= kPreviewMaxHeight) {
return h;
}
}
return kPreviewDefaultHeight;
}
// Canvas plus grip, for GetItemHeight sizing.
inline float PreviewBlockHeight(const std::string& key) {
return PreviewHeight(key) + kPreviewGripHeight;
}
// Preview canvas with a drag-to-resize bottom edge (double-click resets).
inline PreviewCanvas BeginResizableCanvas(const char* strId, const std::string& key, OrbitView& view) {
const float height = PreviewHeight(key);
const float availW = ImGui::GetContentRegionAvail().x;
// Width limit is independent of the dragged height.
const float vw = std::min(availW, kPreviewDefaultHeight * 3.0f);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availW - vw) * 0.5f);
const ImVec2 origin = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(strId, ImVec2(vw, height));
const float winTop = ImGui::GetWindowPos().y;
const float winBot = winTop + ImGui::GetWindowHeight();
const bool visible = ImGui::GetItemRectMax().y > winTop && ImGui::GetItemRectMin().y < winBot;
OrbitControls(view);
if (visible) {
ImGui::GetWindowDrawList()->AddRectFilled(origin, ImVec2(origin.x + vw, origin.y + height),
IM_COL32(18, 18, 22, 255));
}
ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + height));
ImGui::PushID(strId);
ImGui::InvisibleButton("##canvasgrip", ImVec2(vw, kPreviewGripHeight));
ImGui::PopID();
const bool hovered = ImGui::IsItemHovered();
const bool active = ImGui::IsItemActive();
if (hovered || active) {
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
}
if (active) {
const float dy = ImGui::GetIO().MouseDelta.y;
if (dy != 0.0f) {
PreviewHeights()[key] = std::clamp(height + dy, kPreviewMinHeight, kPreviewMaxHeight);
}
}
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
PreviewHeights().erase(key);
}
if (visible) {
// Grip bar, brighter while interacting.
const ImVec2 mid(origin.x + vw * 0.5f, origin.y + height + kPreviewGripHeight * 0.5f);
const ImU32 col = active ? IM_COL32(160, 160, 170, 255)
: hovered ? IM_COL32(120, 120, 130, 255)
: IM_COL32(70, 70, 78, 255);
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(mid.x - 24.0f, mid.y - 1.5f),
ImVec2(mid.x + 24.0f, mid.y + 1.5f), col, 1.5f);
}
return { origin, ImVec2(vw, height), visible };
}
} // namespace UI
#endif // BUILD_UI
|