Skip to content

Commit b8cf0df

Browse files
author
Shane Wall
committed
Add baked auto-connectors and navmesh compatibility coverage
Introduce bake-time auto-connectors generated from paint-layer height boundaries, including dock settings, persisted level state, and a new HFAutoConnector generator integrated into HFBakeSystem post-processing. Scope connector generation out of selection-only bakes, preserve navmesh baking in that path, and harden navmesh parsed-geometry configuration behind a version-safe helper that supports both geometry_parsed_geometry_type and parsed_geometry_type. Expand automated coverage with dedicated auto-connector tests plus bake-system integration tests for connector generation, collision creation, navmesh settings propagation, parsed-geometry configuration, and helper branch behavior. Update docs and project changelog/roadmap entries to describe the feature and validation.
1 parent 10cd2d7 commit b8cf0df

17 files changed

Lines changed: 1245 additions & 17 deletions

‎CHANGELOG.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ The format is based on Keep a Changelog, and this project follows semantic versi
55

66
## [Unreleased]
77
### Added
8+
- **Better Terrain Integration — Auto Connectors** (Apr 2026): Auto-generate ramps or stairs between
9+
height levels during bake. `HFAutoConnector` class (`paint/hf_auto_connector.gd`) scans all paint
10+
layer pairs, detects cross-layer height boundaries (adjacent cells where one layer's filled cell
11+
neighbours another layer's filled cell at a different height, threshold ≥0.1 world units), groups
12+
contiguous boundary edges by direction, and generates connector meshes via the existing
13+
`HFConnectorTool`. Three modes: **Ramp** (smooth slope), **Stairs** (stepped with configurable step
14+
height), and **Auto** (picks stairs when height diff ≥ threshold, ramp otherwise). Connector width
15+
configurable in cells. Deduplication uses canonical 6-part key (both layer indices + both cell coords)
16+
so corner and T-junction edges are never dropped. Integrated into `HFBakeSystem.postprocess_bake()` —
17+
connectors generate before navmesh bake so `PARSED_GEOMETRY_STATIC_COLLIDERS` mode picks up connector
18+
collision shapes. Selection bakes (`bake_selected`) skip auto-connectors to avoid pulling in
19+
unrelated geometry. 4 new export properties on LevelRoot: `bake_auto_connectors`, `bake_connector_mode`,
20+
`bake_connector_stair_height`, `bake_connector_width`. Dock UI: "Auto Connectors" checkbox, Mode
21+
dropdown (Ramp/Stairs/Auto), Step Height and Width spinboxes in Manage tab Bake section. Full state
22+
persistence in `.hflevel` via `hf_state_system.gd` and dock settings export/import.
23+
27 tests in `test_auto_connector.gd` + 13 integration tests in `test_bake_system.gd`.
24+
Total: **1270 tests across 72 files**.
25+
### Fixed
26+
- **NavigationMesh parsed_geometry_type property name** (Apr 2026): `bake_navmesh()` unconditionally
27+
assigned `nav_mesh.parsed_geometry_type`, which was renamed to `geometry_parsed_geometry_type` in
28+
Godot 4.6. Every navmesh bake logged `Invalid assignment of property or key 'parsed_geometry_type'`
29+
and silently failed to set collider-only parse mode. Extracted to version-safe
30+
`_set_parsed_geometry_type(target, value)` static helper that probes both property names via `in`.
31+
4 unit tests exercise both branches (new-name, legacy-name via mock, both-names priority, neither-name
32+
fallback).
833
- **Merge Tool** (Apr 2026): Combine 2+ selected brushes into a single CUSTOM brush before baking.
934
`HFBrushSystem.merge_brushes_by_ids()` collects all faces from source brushes and transforms
1035
their `local_verts` and normals through the full `Transform3D` pipeline (source local → world →

‎DEVELOPMENT.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ addons/hammerforge/
9191
hf_entity_system.gd Entity definitions, placement, Entity I/O connections
9292
hf_brush_system.gd Brush CRUD, cuts, materials, picking, hollow, clip, merge, tie/untie
9393
hf_drag_system.gd Drag lifecycle, preview, axis locking
94-
hf_bake_system.gd Bake orchestration (single/chunked/selected/dirty), preview modes (Full/Wireframe/Proxy), time estimate
94+
hf_bake_system.gd Bake orchestration (single/chunked/selected/dirty), preview modes (Full/Wireframe/Proxy), time estimate, auto-connectors
9595
hf_paint_system.gd Floor + surface paint, layer CRUD
9696
hf_state_system.gd State capture/restore, settings, transactions
9797
hf_file_system.gd .hflevel/.map/.glTF I/O, threaded writes, autosave failure reporting
@@ -119,7 +119,8 @@ addons/hammerforge/
119119
hf_reconciler.gd Stable-ID reconciliation (floors, walls, heightmap floors)
120120
hf_generated_model.gd Data model (FloorRect, WallSeg, HeightmapFloor)
121121
hf_stroke.gd Stroke types (brush/erase/rect/line/bucket/blend/sculpt_raise/lower/smooth/flatten)
122-
hf_connector_tool.gd Ramp/stair mesh generation between layers
122+
hf_connector_tool.gd Ramp/stair mesh generation between layers (manual connector defs)
123+
hf_auto_connector.gd Auto-detect height boundaries + generate connectors during bake
123124
hf_foliage_populator.gd MultiMeshInstance3D procedural scatter (height/slope filtering)
124125
hf_brush_to_heightmap.gd Convert selected brushes to heightmap paint layer (rasterize top faces)
125126
hf_scatter_brush.gd Interactive scatter/foliage brush (circle/spline, density preview, commit)

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<img src="https://img.shields.io/badge/Godot-4.6%2B-478cbf?logo=godot-engine&logoColor=white" alt="Godot 4.6+">
1010
<img src="https://img.shields.io/badge/License-MIT-green" alt="MIT License">
1111
<img src="https://img.shields.io/badge/Status-Early%20Alpha-red" alt="Early Alpha">
12-
<img src="https://img.shields.io/badge/Tests-1226%20passing-brightgreen" alt="1226 tests passing">
12+
<img src="https://img.shields.io/badge/Tests-1270%20passing-brightgreen" alt="1270 tests passing">
1313
<img src="https://img.shields.io/badge/GDScript-25k%2B%20lines-blueviolet" alt="25k+ lines">
1414
</p>
1515

@@ -40,7 +40,7 @@ HammerForge is a single `addons/` folder. No external tools, no custom builds, n
4040

4141
| | |
4242
|---|---|
43-
| **21 subsystems** + coordinator architecture | **1226 unit + integration tests** with CI on every push |
43+
| **21 subsystems** + coordinator architecture | **1270 unit + integration tests** with CI on every push |
4444
| **15 brush shapes** (box through dodecahedron) | **150 built-in prototype textures** for instant greyboxing |
4545
| **Quake `.map`** + **glTF `.glb`** export | **.hflevel** native format with threaded I/O |
4646
| **Customizable keymaps** (JSON) | **Plugin API** for custom tools |
@@ -120,7 +120,7 @@ Grid-based paint layers with chunked storage for large worlds:
120120
- **Displacement surfaces:** Source-style subdivided face grids (power 2-4) with Raise/Lower/Smooth/Noise/Alpha paint modes, sew adjacent displacements, elevation scale
121121
- **Convert Selection to Heightmap:** select brushes → rasterize top faces → create sculptable terrain layer
122122
- **Material blending:** four-slot shader with per-cell blend weights painted directly on the grid
123-
- **Auto-connectors:** ramp and stair mesh generation between layers at different heights
123+
- **Auto-connectors:** ramp and stair mesh generation between layers at different heights, auto-generated during bake with mode selection (Ramp/Stairs/Auto), configurable step height and width
124124
- **Foliage & Scatter brush:** circle/spline shapes, density preview via MultiMesh (Dots/Wireframe/Full), slope/height filtering, align-to-normal, commit as permanent MultiMeshInstance3D
125125
- **Region streaming:** sparse chunk loading for open worlds
126126

‎ROADMAP.md‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Roadmap
22

3-
Last updated: April 6, 2026
3+
Last updated: April 8, 2026
44

55
This roadmap is a directional plan. Items may change based on user feedback.
66
Priorities are informed by a Hammer Editor gap analysis — see GAP_ANALYSIS.md for details.
@@ -233,11 +233,21 @@ Priorities are informed by a Hammer Editor gap analysis — see GAP_ANALYSIS.md
233233
- **Plugin displacement paint input**: raycast plane intersection constrained by convex polygon bounds check. Paint gated behind paint mode enabled + Displacement section expanded.
234234
- 55 new tests across 2 files (test_displacement 40, test_bevel 15). Total: **1172 tests across 69 files**.
235235

236-
## Next (Wave 2c remaining)
237-
- Material atlasing for large scenes.
236+
## Done (Material Atlasing — Draw-Call Reduction)
237+
- **Material atlas** (`HFMaterialAtlas`): shelf bin-packing of albedo textures into single atlas (up to 4096x4096). 2px gutter padding with edge-pixel extension. Half-texel UV inset. Per-face tiling detection splits hardware-repeat faces into separate surfaces. Baker `bake_from_faces()` integration with `remap_uv()`. `bake_use_atlas` LevelRoot property + dock checkbox + state persistence.
238+
- 26 new tests. Total: **1203 tests across 70 files**.
239+
240+
## Done (Merge Tool — Brush Combination)
241+
- **Merge brushes** (`HFBrushSystem.merge_brushes_by_ids()`): combine 2+ selected brushes into one CUSTOM brush. Full Transform3D pipeline (local→world→merged local), per-brush material_override as per-face material_idx. Ctrl+Shift+M keybinding, context toolbar Mrg button, command palette entry.
242+
- 23 new tests. Total: **1226 tests across 71 files**.
243+
244+
## Done (Better Terrain Integration — Auto-Connectors)
245+
- **Auto-connector system** (`hf_auto_connector.gd`): auto-detect cross-layer height boundaries during bake. 4-directional neighbor scan, 6-part canonical dedupe key (handles corners/T-junctions), flood-fill grouping, ramp/stairs/auto mode selection. Connectors include CollisionShape3D for navmesh parsing. Selection-only bakes skip connectors.
246+
- **Bake pipeline integration**: `postprocess_bake()` with `selection_only` flag. `_append_auto_connectors()` creates meshes + collision shapes. Version-safe `_set_parsed_geometry_type()` static helper for Godot 4.6 navmesh property rename.
247+
- **Dock UI**: Auto Connectors checkbox, Mode dropdown (Ramp/Stairs/Auto), Step H and Width spinboxes in Manage tab Bake section.
248+
- 44 new tests (27 auto_connector + 17 bake_system integration). Total: **1270 tests across 73 files**.
238249

239250
## Future (Wave 3 -- Polish)
240-
- Merge tool (combine two adjacent brushes into one convex brush).
241251
- Multiple simultaneous cordons.
242252
- Multi-tool presets for common workflows.
243253
- Additional bake pipelines (merge strategies, export helpers).

‎addons/hammerforge/dock.gd‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ var bake_chunk_size_spin: SpinBox = null
163163
var bake_visible_only_check: CheckBox = null
164164
var bake_use_multimesh_check: CheckBox = null
165165
var bake_use_atlas_check: CheckBox = null
166+
var bake_auto_connectors_check: CheckBox = null
167+
var bake_connector_mode_opt: OptionButton = null
168+
var bake_connector_stair_height_spin: SpinBox = null
169+
var bake_connector_width_spin: SpinBox = null
166170
# -- Quick Play mode controls --
167171
var quick_play_camera_btn: Button = null
168172
var quick_play_area_btn: Button = null
@@ -521,6 +525,7 @@ func _connect_setting_signals() -> void:
521525
[bake_visible_only_check, "bake_visible_only"],
522526
[bake_use_multimesh_check, "bake_use_multimesh"],
523527
[bake_use_atlas_check, "bake_use_atlas"],
528+
[bake_auto_connectors_check, "bake_auto_connectors"],
524529
[commit_freeze, "commit_freeze"],
525530
[autosave_enabled, "hflevel_autosave_enabled"],
526531
[show_grid, "grid_visible"],
@@ -539,6 +544,7 @@ func _connect_setting_signals() -> void:
539544
[bake_navmesh_cell_height, "bake_navmesh_cell_height"],
540545
[bake_navmesh_agent_height, "bake_navmesh_agent_height"],
541546
[bake_navmesh_agent_radius, "bake_navmesh_agent_radius"],
547+
[bake_connector_stair_height_spin, "bake_connector_stair_height"],
542548
]
543549
for binding in float_bindings:
544550
var ctrl: SpinBox = binding[0] as SpinBox
@@ -549,12 +555,20 @@ func _connect_setting_signals() -> void:
549555
var int_bindings: Array = [
550556
[autosave_minutes, "hflevel_autosave_minutes"],
551557
[autosave_keep, "hflevel_autosave_keep"],
558+
[bake_connector_width_spin, "bake_connector_width"],
552559
]
553560
for binding in int_bindings:
554561
var ctrl: SpinBox = binding[0] as SpinBox
555562
var prop: String = binding[1]
556563
if ctrl:
557564
ctrl.value_changed.connect(_on_setting_int_changed.bind(prop))
565+
# OptionButton → int property
566+
if bake_connector_mode_opt:
567+
bake_connector_mode_opt.item_selected.connect(
568+
func(idx: int) -> void:
569+
if level_root and _root_has_property("bake_connector_mode"):
570+
level_root.set("bake_connector_mode", idx)
571+
)
558572
# Debug checkbox (special — also sets local debug_enabled bool)
559573
if debug_logs:
560574
debug_logs.toggled.connect(_on_debug_toggled)
@@ -573,6 +587,7 @@ func _apply_ui_state_to_root() -> void:
573587
[bake_visible_only_check, "bake_visible_only"],
574588
[bake_use_multimesh_check, "bake_use_multimesh"],
575589
[bake_use_atlas_check, "bake_use_atlas"],
590+
[bake_auto_connectors_check, "bake_auto_connectors"],
576591
[commit_freeze, "commit_freeze"],
577592
[autosave_enabled, "hflevel_autosave_enabled"],
578593
[show_grid, "grid_visible"],
@@ -590,6 +605,7 @@ func _apply_ui_state_to_root() -> void:
590605
[bake_navmesh_cell_height, "bake_navmesh_cell_height"],
591606
[bake_navmesh_agent_height, "bake_navmesh_agent_height"],
592607
[bake_navmesh_agent_radius, "bake_navmesh_agent_radius"],
608+
[bake_connector_stair_height_spin, "bake_connector_stair_height"],
593609
]
594610
for pair in float_pairs:
595611
var ctrl: SpinBox = pair[0] as SpinBox
@@ -599,12 +615,15 @@ func _apply_ui_state_to_root() -> void:
599615
var int_pairs: Array = [
600616
[autosave_minutes, "hflevel_autosave_minutes"],
601617
[autosave_keep, "hflevel_autosave_keep"],
618+
[bake_connector_width_spin, "bake_connector_width"],
602619
]
603620
for pair in int_pairs:
604621
var ctrl: SpinBox = pair[0] as SpinBox
605622
var prop: String = pair[1]
606623
if ctrl and _root_has_property(prop):
607624
level_root.set(prop, int(ctrl.value))
625+
if bake_connector_mode_opt and _root_has_property("bake_connector_mode"):
626+
level_root.set("bake_connector_mode", bake_connector_mode_opt.get_selected_id())
608627
if _root_has_property("debug_logging"):
609628
level_root.set("debug_logging", debug_enabled)
610629

@@ -997,6 +1016,13 @@ func _apply_all_tooltips() -> void:
9971016
_set_tooltip(bake_navmesh_cell_height, "Navigation mesh cell height (Y)")
9981017
_set_tooltip(bake_navmesh_agent_height, "Navigation agent height")
9991018
_set_tooltip(bake_navmesh_agent_radius, "Navigation agent radius")
1019+
_set_tooltip(
1020+
bake_auto_connectors_check,
1021+
(
1022+
"Auto-generate ramps or stairs between height levels during bake"
1023+
+ "\nRequires at least 2 paint layers at different heights"
1024+
)
1025+
)
10001026
# FloorPaint tab
10011027
_set_tooltip(
10021028
paint_tool_select,
@@ -3555,6 +3581,14 @@ func _sync_grid_settings_from_root() -> void:
35553581
bake_use_multimesh_check.button_pressed = bool(connected_root.get("bake_use_multimesh"))
35563582
if bake_use_atlas_check and _root_has_property("bake_use_atlas"):
35573583
bake_use_atlas_check.button_pressed = bool(connected_root.get("bake_use_atlas"))
3584+
if bake_auto_connectors_check and _root_has_property("bake_auto_connectors"):
3585+
bake_auto_connectors_check.button_pressed = bool(connected_root.get("bake_auto_connectors"))
3586+
if bake_connector_mode_opt and _root_has_property("bake_connector_mode"):
3587+
bake_connector_mode_opt.select(int(connected_root.get("bake_connector_mode")))
3588+
if bake_connector_stair_height_spin and _root_has_property("bake_connector_stair_height"):
3589+
bake_connector_stair_height_spin.value = float(connected_root.get("bake_connector_stair_height"))
3590+
if bake_connector_width_spin and _root_has_property("bake_connector_width"):
3591+
bake_connector_width_spin.value = int(connected_root.get("bake_connector_width"))
35583592
if bake_chunk_size_spin and _root_has_property("bake_chunk_size"):
35593593
bake_chunk_size_spin.value = float(connected_root.get("bake_chunk_size"))
35603594
if bake_navmesh and _root_has_property("bake_navmesh"):
@@ -5772,6 +5806,14 @@ func _collect_editor_settings() -> Dictionary:
57725806
bake_settings["use_multimesh"] = bake_use_multimesh_check.button_pressed
57735807
if bake_use_atlas_check:
57745808
bake_settings["use_atlas"] = bake_use_atlas_check.button_pressed
5809+
if bake_auto_connectors_check:
5810+
bake_settings["auto_connectors"] = bake_auto_connectors_check.button_pressed
5811+
if bake_connector_mode_opt:
5812+
bake_settings["connector_mode"] = bake_connector_mode_opt.get_selected_id()
5813+
if bake_connector_stair_height_spin:
5814+
bake_settings["connector_stair_height"] = float(bake_connector_stair_height_spin.value)
5815+
if bake_connector_width_spin:
5816+
bake_settings["connector_width"] = int(bake_connector_width_spin.value)
57755817
return {
57765818
"version": 1,
57775819
"saved_at": Time.get_datetime_string_from_system(),
@@ -5854,6 +5896,14 @@ func _apply_editor_settings(data: Dictionary) -> void:
58545896
bake_use_multimesh_check.button_pressed = bool(bake.get("use_multimesh", false))
58555897
if bake_use_atlas_check and bake.has("use_atlas"):
58565898
bake_use_atlas_check.button_pressed = bool(bake.get("use_atlas", false))
5899+
if bake_auto_connectors_check and bake.has("auto_connectors"):
5900+
bake_auto_connectors_check.button_pressed = bool(bake.get("auto_connectors", false))
5901+
if bake_connector_mode_opt and bake.has("connector_mode"):
5902+
bake_connector_mode_opt.select(int(bake.get("connector_mode", 0)))
5903+
if bake_connector_stair_height_spin and bake.has("connector_stair_height"):
5904+
bake_connector_stair_height_spin.value = float(bake.get("connector_stair_height", 0.25))
5905+
if bake_connector_width_spin and bake.has("connector_width"):
5906+
bake_connector_width_spin.value = int(bake.get("connector_width", 2))
58575907
_sync_bake_option_visibility()
58585908

58595909

‎addons/hammerforge/level_root.gd‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const HFGeometrySynth = preload("paint/hf_geometry_synth.gd")
2121
const HFGeneratedReconciler = preload("paint/hf_reconciler.gd")
2222
const HFStroke = preload("paint/hf_stroke.gd")
2323
const HFHeightmapSynth = preload("paint/hf_heightmap_synth.gd")
24+
const HFAutoConnectorType = preload("paint/hf_auto_connector.gd")
2425
const HFHeightmapIO = preload("paint/hf_heightmap_io.gd")
2526
const HFExtrudeToolType = preload("hf_extrude_tool.gd")
2627
const HFInputStateType = preload("input_state.gd")
@@ -98,6 +99,10 @@ var _grid_snap: float = 16.0
9899
@export var bake_visible_only: bool = false
99100
@export var bake_use_multimesh: bool = false
100101
@export var bake_use_atlas: bool = false
102+
@export var bake_auto_connectors: bool = false
103+
@export var bake_connector_mode: int = 0 # HFAutoConnector.ConnectorMode (RAMP=0, STAIRS=1, AUTO=2)
104+
@export var bake_connector_stair_height: float = 0.25
105+
@export var bake_connector_width: int = 2
101106
@export var bake_use_thread_pool: bool = true
102107
var _hflevel_autosave_enabled: bool = true
103108
@export var hflevel_autosave_enabled: bool = true:

0 commit comments

Comments
 (0)