Skip to content

Commit ab68ee3

Browse files
author
Shane Wall
committed
Improve texture workflow feedback and tooltip guidance
Expand texture and material tooltips so the dock exposes the actual workflow: prototype loading, face selection prerequisites, brush-vs-face material behavior, bake material usage, terrain slot blending, UV reset guidance, and texture lock scope. Teach material thumbnail tooltips about hover preview, double-click apply, and right-click actions to make the browser more discoverable. Harden material assignment feedback by validating face selections against real brushes and face bounds, preventing misleading counts, and resolving whole-brush assignments through _find_brush_by_key so unsaved brushes with instance-id fallback are handled correctly. Clean up noisy GUT output by normalizing JSON number assertions in HFLevelIO tests and deleting test brushes immediately in reference cleanup tests to avoid transient orphans during test execution. Validation run: - gdformat --check addons/hammerforge/ - gdlint addons/hammerforge/ - Godot 4.6.1 headless import - GUT headless suite: 694/694 passing
1 parent 0b90e2b commit ab68ee3

7 files changed

Lines changed: 161 additions & 30 deletions

File tree

addons/hammerforge/dock.gd

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -868,12 +868,24 @@ func _apply_all_tooltips() -> void:
868868
_set_tooltip(sides_spin, "Side count for polygon shapes (Pyramid, Prism)")
869869
_set_tooltip(commit_freeze, "Keep committed cuts frozen (restorable)\ninstead of deleting them")
870870
_set_tooltip(collision_layer_opt, "Physics collision layer for baked geometry")
871-
_set_tooltip(active_material_button, "Active material for Select+Paint brush painting")
871+
_set_tooltip(
872+
active_material_button,
873+
(
874+
"Brush-level material override for whole brushes"
875+
+ "\nThis is separate from per-face materials in the Materials tab"
876+
)
877+
)
872878
# Build tab - Bake options
873879
_set_tooltip(bake_merge_meshes, "Merge meshes during bake for better performance")
874880
_set_tooltip(bake_generate_lods, "Generate LOD meshes during bake")
875881
_set_tooltip(bake_lightmap_uv2, "Generate UV2 for lightmap baking")
876-
_set_tooltip(bake_use_face_materials, "Apply per-face materials from the Materials tab")
882+
_set_tooltip(
883+
bake_use_face_materials,
884+
(
885+
"Bake per-face materials into the final mesh"
886+
+ "\nTurn off to ignore face assignments and use brush-level materials instead"
887+
)
888+
)
877889
_set_tooltip(bake_navmesh, "Generate navigation mesh during bake")
878890
_set_tooltip(bake_lightmap_texel, "Lightmap texel density (smaller = higher quality)")
879891
_set_tooltip(bake_navmesh_cell_size, "Navigation mesh cell size (XZ)")
@@ -902,13 +914,25 @@ func _apply_all_tooltips() -> void:
902914
_set_tooltip(layer_y_spin, "Vertical Y offset for the active paint layer")
903915
_set_tooltip(blend_strength_spin, "Blend strength when using the Blend paint tool")
904916
_set_tooltip(blend_slot_select, "Blend target slot (B, C, or D)")
905-
_set_tooltip(terrain_slot_a_button, "Texture for Slot A (base)")
917+
_set_tooltip(
918+
terrain_slot_a_button,
919+
"Choose terrain texture for Slot A (base layer)\nBlend between slots with the Blend paint tool"
920+
)
906921
_set_tooltip(terrain_slot_a_scale, "UV scale for Slot A texture")
907-
_set_tooltip(terrain_slot_b_button, "Texture for Slot B")
922+
_set_tooltip(
923+
terrain_slot_b_button,
924+
"Choose terrain texture for Slot B\nBlend between slots with the Blend paint tool"
925+
)
908926
_set_tooltip(terrain_slot_b_scale, "UV scale for Slot B texture")
909-
_set_tooltip(terrain_slot_c_button, "Texture for Slot C")
927+
_set_tooltip(
928+
terrain_slot_c_button,
929+
"Choose terrain texture for Slot C\nBlend between slots with the Blend paint tool"
930+
)
910931
_set_tooltip(terrain_slot_c_scale, "UV scale for Slot C texture")
911-
_set_tooltip(terrain_slot_d_button, "Texture for Slot D")
932+
_set_tooltip(
933+
terrain_slot_d_button,
934+
"Choose terrain texture for Slot D\nBlend between slots with the Blend paint tool"
935+
)
912936
_set_tooltip(terrain_slot_d_scale, "UV scale for Slot D texture")
913937
# SurfacePaint tab
914938
_set_tooltip(paint_target_select, "Paint target: Floor (grid) or Surface (UV)")
@@ -917,21 +941,48 @@ func _apply_all_tooltips() -> void:
917941
_set_tooltip(surface_paint_layer_select, "Active surface paint layer")
918942
_set_tooltip(surface_paint_layer_add, "Add a new surface paint layer")
919943
_set_tooltip(surface_paint_layer_remove, "Remove the selected surface paint layer")
920-
_set_tooltip(surface_paint_texture, "Texture for the selected surface paint layer")
944+
_set_tooltip(
945+
surface_paint_texture,
946+
(
947+
"Choose the texture for the selected surface-paint layer"
948+
+ "\nRequires a selected face and an existing paint layer"
949+
)
950+
)
921951
# Materials tab
922952
_set_tooltip(
923953
face_select_mode,
924-
"Enable per-face selection for material assignment\nClick faces in viewport to select them"
954+
(
955+
"Enable per-face texturing"
956+
+ "\nClick faces in the viewport to select them"
957+
+ "\nShift+Click adds more faces"
958+
+ "\nRequired for per-face assign, UV edit, and surface paint"
959+
)
925960
)
926961
_set_tooltip(material_add, "Add a material to the palette")
927962
_set_tooltip(material_remove, "Remove selected material from palette")
928963
_set_tooltip(
929-
material_load_prototypes, "Refresh all built-in prototype textures into the palette"
964+
material_load_prototypes,
965+
(
966+
"Load built-in prototype textures into the palette"
967+
+ "\nUse this first if the browser looks empty"
968+
)
969+
)
970+
_set_tooltip(
971+
material_assign,
972+
(
973+
"Apply the selected material to all selected faces"
974+
+ "\nTip: choose a texture in the browser, then click faces in Face Select Mode"
975+
)
930976
)
931-
_set_tooltip(material_assign, "Assign selected material to selected faces")
932977
_set_tooltip(face_clear, "Clear face selection")
933978
# UV tab
934-
_set_tooltip(uv_reset, "Reset UV coordinates to defaults for selected face")
979+
_set_tooltip(
980+
uv_reset,
981+
(
982+
"Reset this face to default projected UVs"
983+
+ "\nUse after stretching or before Fit/Center/Left/Right justify"
984+
)
985+
)
935986
# Manage tab
936987
_set_tooltip(floor_btn, "Create a default floor brush")
937988
_set_tooltip(apply_cuts_btn, "Move pending cuts into the draft brush tree")
@@ -3663,9 +3714,44 @@ func _on_material_assign() -> void:
36633714
return
36643715
if not level_root:
36653716
return
3717+
var face_count := _count_selected_faces()
3718+
if face_count == 0:
3719+
show_toast("No faces selected — select faces first", 1)
3720+
return
36663721
_commit_state_action(
36673722
"Assign Face Material", "assign_material_to_selected_faces", [_selected_material_index]
36683723
)
3724+
var mat_name := _material_display_name(_selected_material_index)
3725+
show_toast(
3726+
"Applied %s to %d face%s" % [mat_name, face_count, "" if face_count == 1 else "s"], 0
3727+
)
3728+
3729+
3730+
func _count_selected_faces() -> int:
3731+
if not level_root:
3732+
return 0
3733+
var total := 0
3734+
for key in level_root.face_selection.keys():
3735+
var brush = level_root._find_brush_by_key(str(key))
3736+
if not brush:
3737+
continue
3738+
var indices: Array = level_root.face_selection.get(key, [])
3739+
for idx in indices:
3740+
if int(idx) >= 0 and int(idx) < brush.faces.size():
3741+
total += 1
3742+
return total
3743+
3744+
3745+
func _material_display_name(index: int) -> String:
3746+
if not level_root or not level_root.material_manager:
3747+
return "material"
3748+
var mat = level_root.material_manager.get_material(index)
3749+
if mat == null:
3750+
return "material"
3751+
var name: String = (
3752+
mat.resource_name if mat.resource_name != "" else mat.resource_path.get_file()
3753+
)
3754+
return name if name != "" else "material"
36693755

36703756

36713757
func _on_face_clear() -> void:
@@ -3690,7 +3776,15 @@ func _on_browser_material_double_clicked(index: int) -> void:
36903776
material_browser.set_selected_index(index)
36913777
# Double-click triggers immediate face assignment.
36923778
if level_root and index >= 0:
3779+
var face_count := _count_selected_faces()
3780+
if face_count == 0:
3781+
show_toast("No faces selected — select faces first", 1)
3782+
return
36933783
_commit_state_action("Assign Face Material", "assign_material_to_selected_faces", [index])
3784+
var mat_name := _material_display_name(index)
3785+
show_toast(
3786+
"Applied %s to %d face%s" % [mat_name, face_count, "" if face_count == 1 else "s"], 0
3787+
)
36943788

36953789

36963790
func _on_browser_context_menu(index: int, global_pos: Vector2) -> void:
@@ -3711,19 +3805,34 @@ func _on_material_context_action(id: int) -> void:
37113805
var idx = _material_context_index
37123806
if idx < 0:
37133807
return
3808+
var mat_name := _material_display_name(idx)
37143809
match id:
37153810
0: # Apply to Selected Faces
37163811
_selected_material_index = idx
3812+
var face_count := _count_selected_faces()
3813+
if face_count == 0:
3814+
show_toast("No faces selected — select faces first", 1)
3815+
return
37173816
_commit_state_action("Assign Face Material", "assign_material_to_selected_faces", [idx])
3817+
show_toast(
3818+
"Applied %s to %d face%s" % [mat_name, face_count, "" if face_count == 1 else "s"],
3819+
0
3820+
)
37183821
1: # Apply to Whole Brush — assign material to ALL faces on selected brushes
37193822
_selected_material_index = idx
3720-
if _selection_nodes.is_empty():
3823+
var brush_ids := _get_selected_brush_ids()
3824+
if brush_ids.is_empty():
37213825
show_toast("No brushes selected", 1)
37223826
return
37233827
_commit_state_action(
3724-
"Assign Brush Material",
3725-
"assign_material_to_whole_brushes",
3726-
[idx, _get_selected_brush_ids()]
3828+
"Assign Brush Material", "assign_material_to_whole_brushes", [idx, brush_ids]
3829+
)
3830+
show_toast(
3831+
(
3832+
"Applied %s to %d brush%s"
3833+
% [mat_name, brush_ids.size(), "" if brush_ids.size() == 1 else "es"]
3834+
),
3835+
0
37273836
)
37283837
2: # Toggle Favorite
37293838
if material_browser:
@@ -4832,7 +4941,7 @@ func _setup_texture_lock_ui() -> void:
48324941
texture_lock_check = CheckBox.new()
48334942
texture_lock_check.text = "Texture Lock"
48344943
texture_lock_check.button_pressed = true
4835-
texture_lock_check.tooltip_text = "Preserve UV alignment when moving or resizing brushes"
4944+
texture_lock_check.tooltip_text = "Keep texture alignment while moving, resizing, hollowing, or clipping brushes"
48364945
texture_lock_check.toggled.connect(_on_texture_lock_toggled)
48374946
brush_vbox.add_child(texture_lock_check)
48384947

addons/hammerforge/level_root.gd

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,8 +1108,8 @@ func get_primary_selected_face() -> Dictionary:
11081108
return brush_system.get_primary_selected_face()
11091109

11101110

1111-
func assign_material_to_selected_faces(material_index: int) -> void:
1112-
brush_system.assign_material_to_selected_faces(material_index)
1111+
func assign_material_to_selected_faces(material_index: int) -> int:
1112+
return brush_system.assign_material_to_selected_faces(material_index)
11131113

11141114

11151115
func assign_material_to_faces_by_id(
@@ -1124,15 +1124,18 @@ func assign_material_to_faces_by_id(
11241124
brush.assign_material_to_faces(material_index, typed_indices)
11251125

11261126

1127-
func assign_material_to_whole_brushes(material_index: int, brush_ids: Array) -> void:
1127+
func assign_material_to_whole_brushes(material_index: int, brush_ids: Array) -> int:
1128+
var count := 0
11281129
for bid in brush_ids:
1129-
var brush: DraftBrush = brush_system.find_brush_by_id(str(bid))
1130+
var brush: DraftBrush = brush_system._find_brush_by_key(str(bid))
11301131
if not brush or not is_instance_valid(brush):
11311132
continue
11321133
var all_indices: Array[int] = []
11331134
for i in range(brush.faces.size()):
11341135
all_indices.append(i)
11351136
brush.assign_material_to_faces(material_index, all_indices)
1137+
count += all_indices.size()
1138+
return count
11361139

11371140

11381141
func _apply_face_selection() -> void:

addons/hammerforge/systems/hf_brush_system.gd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,8 @@ func get_primary_selected_face() -> Dictionary:
764764
return {}
765765

766766

767-
func assign_material_to_selected_faces(material_index: int) -> void:
767+
func assign_material_to_selected_faces(material_index: int) -> int:
768+
var count := 0
768769
for key in root.face_selection.keys():
769770
var brush = _find_brush_by_key(str(key))
770771
if not brush:
@@ -774,6 +775,8 @@ func assign_material_to_selected_faces(material_index: int) -> void:
774775
for idx in indices:
775776
typed.append(int(idx))
776777
brush.assign_material_to_faces(material_index, typed)
778+
count += typed.size()
779+
return count
777780

778781

779782
func _apply_face_selection() -> void:

addons/hammerforge/systems/hf_paint_system.gd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ func get_primary_selected_face() -> Dictionary:
300300
return {}
301301

302302

303-
func assign_material_to_selected_faces(material_index: int) -> void:
303+
func assign_material_to_selected_faces(material_index: int) -> int:
304+
var count := 0
304305
for key in root.face_selection.keys():
305306
var brush = root._find_brush_by_key(str(key))
306307
if not brush:
@@ -310,6 +311,8 @@ func assign_material_to_selected_faces(material_index: int) -> void:
310311
for idx in indices:
311312
typed.append(int(idx))
312313
brush.assign_material_to_faces(material_index, typed)
314+
count += typed.size()
315+
return count
313316

314317

315318
func apply_face_selection() -> void:

addons/hammerforge/ui/hf_material_browser.gd

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,16 @@ func _create_thumb_button(palette_index: int, mat: Material, mat_path: String) -
263263
container.add_child(label)
264264

265265
# Tooltip
266-
container.tooltip_text = _get_material_label(mat)
266+
var tip := _get_material_label(mat)
267267
if _favorites.has(mat_path):
268-
container.tooltip_text += " [Favorite]"
268+
tip += " [Favorite]"
269+
tip += (
270+
"\n\nLeft-click: Select material"
271+
+ "\nHover: Preview on selected faces"
272+
+ "\nDouble-click: Apply to selected faces"
273+
+ "\nRight-click: Apply to whole brush, favorite, copy name"
274+
)
275+
container.tooltip_text = tip
269276

270277
# Wrap in a button-like panel for click/hover
271278
var btn = Button.new()

tests/test_hflevel_io.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func test_build_parse_complex_payload():
289289
}
290290
var payload = HFLevelIO.build_payload(data)
291291
var parsed = HFLevelIO.parse_payload(payload)
292-
assert_eq(parsed.get("version"), 2)
292+
assert_eq(int(parsed.get("version", 0)), 2)
293293
var brushes = parsed.get("brushes", [])
294294
assert_eq(brushes.size(), 2, "Brushes array preserved")
295295
assert_eq(brushes[0].get("id"), "brush_1")
@@ -318,4 +318,4 @@ func test_full_pipeline_round_trip():
318318
assert_almost_eq(decoded["scale"].y, 3.0, 0.001, "Full pipeline: scale.y")
319319
assert_true(decoded["color"] is Color, "Full pipeline: Color survives")
320320
assert_eq(decoded["name"], "level_1", "Full pipeline: string survives")
321-
assert_eq(decoded["count"], 42, "Full pipeline: int survives")
321+
assert_eq(int(decoded["count"]), 42, "Full pipeline: integer value survives")

tests/test_reference_cleanup.gd

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ func _make_entity(entity_name: String) -> DraftEntity:
116116
return e
117117

118118

119+
func _delete_brush_now(brush: DraftBrush) -> void:
120+
brush_sys.delete_brush(brush, false)
121+
if is_instance_valid(brush):
122+
brush.free()
123+
124+
119125
# ===========================================================================
120126
# Group cleanup
121127
# ===========================================================================
@@ -125,15 +131,15 @@ func test_delete_brush_cleans_group_membership():
125131
var b = _make_brush(Vector3.ZERO, Vector3(32, 32, 32), "g1")
126132
visgroup_sys.group_selection("mygroup", [b])
127133
assert_eq(visgroup_sys.get_group_members("mygroup").size(), 1)
128-
brush_sys.delete_brush(b)
134+
_delete_brush_now(b)
129135
assert_eq(visgroup_sys.get_group_members("mygroup").size(), 0)
130136

131137

132138
func test_delete_brush_cleans_empty_group():
133139
var b = _make_brush(Vector3.ZERO, Vector3(32, 32, 32), "g2")
134140
visgroup_sys.group_selection("tempgroup", [b])
135141
assert_true(visgroup_sys.groups.has("tempgroup"))
136-
brush_sys.delete_brush(b)
142+
_delete_brush_now(b)
137143
assert_false(visgroup_sys.groups.has("tempgroup"), "Empty group should be removed")
138144

139145

@@ -147,7 +153,7 @@ func test_delete_brush_clears_visgroup_meta():
147153
visgroup_sys.create_visgroup("lights")
148154
visgroup_sys.add_to_visgroup(b, "lights")
149155
assert_eq(visgroup_sys.get_members_of("lights").size(), 1)
150-
brush_sys.delete_brush(b)
156+
_delete_brush_now(b)
151157
# After deletion, the visgroup should have no members
152158
assert_eq(visgroup_sys.get_members_of("lights").size(), 0)
153159

@@ -197,6 +203,6 @@ func test_cleanup_no_connections_returns_zero():
197203

198204
func test_delete_with_no_references_no_crash():
199205
var b = _make_brush(Vector3.ZERO, Vector3(32, 32, 32), "plain")
200-
brush_sys.delete_brush(b)
206+
_delete_brush_now(b)
201207
# No crash, no error — just works
202208
assert_true(true)

0 commit comments

Comments
 (0)