Editor: Added ability to hide/unhide utility brushes and target lines.

Tweaked path tracing so lighting is more realistic.
This commit is contained in:
Justin Marshall
2026-05-11 09:35:14 -07:00
parent e7ff9997ce
commit 4da1776627
2 changed files with 273 additions and 43 deletions
+169 -1
View File
@@ -1012,12 +1012,16 @@ static const char* CAMWND_PROP_FILTER_MASK = "QER_CamWnd_FilterMask";
#define CAMWND_MENU_CMD_SHOW_ENTITIES 62104
#define CAMWND_MENU_CMD_SHOW_LIGHTS 62105
#define CAMWND_MENU_CMD_RESET_FILTERS 62106
#define CAMWND_MENU_CMD_SHOW_TARGET_LINES 62107
#define CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES 62108
#define CAMWND_FILTER_HIDE_WORLD 0x00000001
#define CAMWND_FILTER_HIDE_PATCHES 0x00000002
#define CAMWND_FILTER_HIDE_MODELS 0x00000004
#define CAMWND_FILTER_HIDE_ENTITIES 0x00000008
#define CAMWND_FILTER_HIDE_LIGHTS 0x00000010
#define CAMWND_FILTER_HIDE_TARGET_LINES 0x00000020
#define CAMWND_FILTER_HIDE_TRIGGER_BRUSHES 0x00000040
static LRESULT CALLBACK CamWnd_MenuBarProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
@@ -1186,6 +1190,12 @@ static void CamWnd_HandleMenuCommand(CCamWnd* cam, int command) {
case CAMWND_MENU_CMD_SHOW_LIGHTS:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_LIGHTS);
break;
case CAMWND_MENU_CMD_SHOW_TARGET_LINES:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_TARGET_LINES);
break;
case CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_TRIGGER_BRUSHES);
break;
case CAMWND_MENU_CMD_RESET_FILTERS:
CamWnd_ResetFilters(cam);
break;
@@ -1241,6 +1251,9 @@ static void CamWnd_ShowFiltersMenu(HWND hWnd, CCamWnd* cam) {
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_ENTITIES, CAMWND_MENU_CMD_SHOW_ENTITIES, "Show entities");
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_LIGHTS, CAMWND_MENU_CMD_SHOW_LIGHTS, "Show lights");
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_TARGET_LINES, CAMWND_MENU_CMD_SHOW_TARGET_LINES, "Show actor/link lines");
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_TRIGGER_BRUSHES, CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES, "Show trigger/utility brushes");
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
AppendMenu(menu, MF_STRING, CAMWND_MENU_CMD_RESET_FILTERS, "Reset filters");
command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, pt.x, pt.y, 0, hWnd, NULL);
@@ -1370,6 +1383,154 @@ static void CamWnd_UpdateRuntimeState(CCamWnd* cam, bool renderMode, bool rebuil
}
}
static bool CamWnd_StringStartsWithNoCase(const char* text, const char* prefix) {
if (text == NULL || prefix == NULL || prefix[0] == '\0') {
return false;
}
return idStr::Icmpn(text, prefix, (int)strlen(prefix)) == 0;
}
static bool CamWnd_StringContainsNoCase(const char* text, const char* needle) {
if (text == NULL || needle == NULL || needle[0] == '\0') {
return false;
}
const int needleLen = (int)strlen(needle);
for (const char* scan = text; *scan; scan++) {
if (idStr::Icmpn(scan, needle, needleLen) == 0) {
return true;
}
}
return false;
}
static bool CamWnd_MaterialPathHasSegment(const char* materialName, const char* segment) {
if (materialName == NULL || segment == NULL || segment[0] == '\0') {
return false;
}
const int segmentLen = (int)strlen(segment);
for (const char* scan = materialName; *scan; scan++) {
const bool segmentStart = (scan == materialName || scan[-1] == '/' || scan[-1] == '\\');
if (!segmentStart) {
continue;
}
if (idStr::Icmpn(scan, segment, segmentLen) != 0) {
continue;
}
const char end = scan[segmentLen];
if (end == '\0' || end == '/' || end == '\\') {
return true;
}
}
return false;
}
static bool CamWnd_IsUtilityMaterial(const idMaterial* material) {
if (material == NULL || material->GetName() == NULL || material->GetName()[0] == '\0') {
return false;
}
const char* materialName = material->GetName();
// Nodraw is the important case here and may appear outside textures/common
// in some projects, so match it anywhere in the material name.
if (CamWnd_StringContainsNoCase(materialName, "nodraw")) {
return true;
}
// The broader names below are only treated as utility when they live in a
// common/editor tool-material folder. This avoids hiding ordinary art
// materials that happen to contain words like "clip" or "portal".
if (!CamWnd_MaterialPathHasSegment(materialName, "common") &&
!CamWnd_MaterialPathHasSegment(materialName, "editor")) {
return false;
}
static const char* const utilityTokens[] = {
"caulk",
"trigger",
"clip",
"playerclip",
"monsterclip",
"moveclip",
"missileclip",
"aas",
"ladder",
"origin",
"portal",
"visportal",
"hint",
"skip",
"collision"
};
for (int i = 0; i < (int)(sizeof(utilityTokens) / sizeof(utilityTokens[0])); i++) {
if (CamWnd_StringContainsNoCase(materialName, utilityTokens[i])) {
return true;
}
}
return false;
}
static bool CamWnd_BrushUsesOnlyUtilityMaterials(brush_t* brush) {
if (brush == NULL) {
return false;
}
if (brush->pPatch) {
return CamWnd_IsUtilityMaterial(brush->pPatch->d_texture);
}
bool foundMaterial = false;
for (face_t* face = brush->brush_faces; face; face = face->next) {
if (face->face_winding == NULL) {
continue;
}
if (face->d_texture == NULL) {
return false;
}
foundMaterial = true;
if (!CamWnd_IsUtilityMaterial(face->d_texture)) {
return false;
}
}
return foundMaterial;
}
static bool CamWnd_IsTriggerLikeBrushEntity(brush_t* brush) {
if (brush == NULL || brush->owner == NULL) {
return false;
}
const char* className = ValueForKey(brush->owner, "classname");
if (className == NULL || className[0] == '\0') {
return false;
}
// These brush entities are editor/gameplay volumes rather than visible map
// geometry, so let the camera filter hide them independently from normal
// func_* brush models such as doors, movers, and func_static.
return CamWnd_StringStartsWithNoCase(className, "trigger") ||
CamWnd_StringStartsWithNoCase(className, "func_trigger") ||
CamWnd_StringStartsWithNoCase(className, "func_clip") ||
CamWnd_StringStartsWithNoCase(className, "func_playerclip") ||
CamWnd_StringStartsWithNoCase(className, "func_monsterclip") ||
CamWnd_StringStartsWithNoCase(className, "clip") ||
CamWnd_StringStartsWithNoCase(className, "playerclip") ||
CamWnd_StringStartsWithNoCase(className, "monsterclip");
}
static bool CamWnd_TargetLinesVisible(CCamWnd* cam) {
return (CamWnd_GetFilterMask(cam) & CAMWND_FILTER_HIDE_TARGET_LINES) == 0;
}
static bool CamWnd_MenuFilterBrush(CCamWnd* cam, brush_t* brush) {
if (!brush) {
return false;
@@ -1394,8 +1555,13 @@ static bool CamWnd_MenuFilterBrush(CCamWnd* cam, brush_t* brush) {
const bool isModel = (brush->modelHandle > 0) || (hasOwner && brush->owner->eclass->entityModel) || brush->entityModel;
const bool isBrushModel = hasOwner && IsBModel(brush);
const bool isFixedEntity = hasOwner && brush->owner->eclass->fixedsize;
const bool isTriggerLikeBrushEntity = CamWnd_IsTriggerLikeBrushEntity(brush);
const bool isUtilityMaterialBrush = CamWnd_BrushUsesOnlyUtilityMaterials(brush);
const bool isEntity = isLight || isModel || isBrushModel || isFixedEntity;
if ((mask & CAMWND_FILTER_HIDE_TRIGGER_BRUSHES) && (isTriggerLikeBrushEntity || isUtilityMaterialBrush)) {
return true;
}
if ((mask & CAMWND_FILTER_HIDE_LIGHTS) && isLight) {
return true;
}
@@ -2641,7 +2807,9 @@ void CCamWnd::Cam_Draw() {
// draw pointfile
glEnable(GL_DEPTH_TEST);
CamWnd_DrawVisiblePathLines();
if (CamWnd_TargetLinesVisible(this)) {
CamWnd_DrawVisiblePathLines();
}
if (g_qeglobals.d_pointfile_display_list) {
Pointfile_Draw();