Last updated: April 3, 2026
This document describes HammerForge's architecture and data flow.
- Fast in-editor greyboxing with brush workflows.
- Editor responsiveness by avoiding live CSG.
- Reliable bake pipeline and clean data model.
- Modular codebase with clear separation of concerns.
HammerForge uses a coordinator + subsystems pattern. LevelRoot is a thin coordinator that owns all container nodes, exported properties, and signals, and delegates work to 16 RefCounted subsystem classes. Each subsystem receives a reference to LevelRoot in its constructor.
All signals are defined on LevelRoot. Subsystems emit them via root.<signal>.emit(...). UI and other consumers subscribe instead of polling.
| Signal | Description |
|---|---|
bake_started |
Emitted when a bake begins |
bake_progress(value, label) |
Progress 0..1 with short label during bake |
bake_finished(success) |
Emitted when a bake completes |
grid_snap_changed(value) |
Grid snap updated |
brush_added(brush_id) |
A brush was created |
brush_removed(brush_id) |
A brush was deleted |
brush_changed(brush_id) |
A brush was modified (transform, material, etc.) |
entity_added(node) |
An entity was added |
entity_removed(node) |
An entity was removed |
selection_changed(brush_ids) |
Brush selection changed |
paint_layer_changed(layer_index) |
A paint layer was modified |
state_saved() |
.hflevel save completed |
state_loaded() |
.hflevel load completed |
autosave_failed(error_message) |
Threaded autosave write failed |
user_message(text, level) |
Subsystem-to-dock notification routing (0=INFO, 1=WARNING, 2=ERROR) |
material_list_changed() |
Material palette updated (add/remove) |
face_selection_changed() |
Face selection changed (snapshot comparison) |
selection_clear_requested() |
Dock requests plugin clear hf_selection before editor_selection.clear() (reimport guard) |
| Script | Role |
|---|---|
plugin.gd |
EditorPlugin entry point, input routing, undo/redo, sticky LevelRoot discovery |
level_root.gd |
Thin coordinator: containers, exports, signals, delegates to subsystems |
dock.gd + dock.tscn |
UI dock (4 tabs: Brush, Paint, Entities, Manage), collapsible sections, tool state |
ui/collapsible_section.gd |
Reusable HFCollapsibleSection toggle-header VBoxContainer |
input_state.gd |
Drag/paint/extrude state machine (Mode enum: IDLE, DRAG_BASE, DRAG_HEIGHT, SURFACE_PAINT, EXTRUDE) |
hf_extrude_tool.gd |
Extrude Up/Down tool (face pick + drag to extend brushes) |
brush_instance.gd |
DraftBrush node (authored geometry) |
baker.gd |
CSG -> mesh bake pipeline |
face_data.gd |
Per-face materials, UVs, and paint layers |
material_manager.gd |
Shared materials palette (+ library persistence, usage tracking) |
hf_entity_def.gd |
Data-driven entity definition system (JSON + built-in defaults) |
hf_gesture.gd |
Gesture tracker base class (update/commit/cancel pattern) |
undo_helper.gd |
Undo/redo helper with command collation support |
face_selector.gd |
Raycast face selection helper |
surface_paint.gd |
Per-face surface paint tool |
uv_editor.gd + uv_editor.tscn |
UV editing dock control |
hf_keymap.gd |
Customizable keyboard shortcuts (JSON load/save, action → binding mapping) |
hf_user_prefs.gd |
Cross-session user preferences (user://hammerforge_prefs.json) |
hf_snap_system.gd |
Centralized snap system (Grid/Vertex/Center modes, threshold-based candidate selection) |
hf_op_result.gd |
Lightweight operation result (ok, message, fix_hint) returned by brush operations |
hf_prefab.gd |
Reusable brush+entity group with variants, tags, live-linking (save/load .hfprefab, I/O remap) |
hf_polygon_tool.gd |
Polygon tool: click convex verts → extrude to brush (tool_id=102, KEY_P) |
hf_path_tool.gd |
Path tool: click waypoints → corridor brushes with miter joints (tool_id=103, KEY_SEMICOLON) |
| Script | Role |
|---|---|
hf_tutorial_wizard.gd |
Interactive 5-step tutorial with signal-driven auto-advance and persistent progress |
hf_shortcut_dialog.gd |
Searchable shortcut reference dialog (filterable Tree with categories) |
hf_prefab_library.gd |
Prefab library dock section (search, tags, variants, drag-drop, context menu) |
hf_prefab_overlay.gd |
Prefab ghost overlay (wireframe bounding box + override markers on hover) |
hf_welcome_panel.gd |
Legacy welcome panel (replaced by tutorial wizard) |
hf_toast.gd |
Toast notification system (auto-fading stacked messages) |
hf_material_browser.gd |
Visual material browser (thumbnail grid, search, filters, favorites, drag-drop) |
paint_tab_builder.gd |
Builds Paint tab sections + signal connections |
entity_tab_builder.gd |
Builds Entity Properties + Entity I/O + I/O Wiring sections (all context-hidden until entity selected) |
manage_tab_builder.gd |
Builds Manage tab sections (Bake, File, Settings, Prefabs, etc.) |
selection_tools_builder.gd |
Builds Selection Tools section (hollow, clip, merge, move, tie, duplicator) |
| Subsystem | class_name | Responsibility |
|---|---|---|
hf_grid_system.gd |
HFGridSystem |
Editor grid setup, visibility, transform, axis-plane intersection |
hf_entity_system.gd |
HFEntitySystem |
Entity definitions, placement, capture/restore, Entity I/O connections, dangling connection cleanup, fire_output() |
hf_brush_system.gd |
HFBrushSystem |
Brush CRUD, picking, pending/committed cuts, materials, face selection, hollow, clip, merge, tie/untie. O(1) brush ID cache. Returns HFOpResult on failable ops. Auto-cleans references on deletion |
hf_drag_system.gd |
HFDragSystem |
Drag lifecycle, preview management, axis locking, height computation. Owns HFInputState |
hf_bake_system.gd |
HFBakeSystem |
Bake orchestration (single/chunked/selected/dirty), CSG assembly, navmesh, collision, preview modes (Full/Wireframe/Proxy), time estimate |
hf_paint_system.gd |
HFPaintSystem |
Floor paint input, surface paint, paint layer CRUD, face selection |
hf_state_system.gd |
HFStateSystem |
State capture/restore (brushes, entities, floor, sun, paint), settings, transactions (begin/commit/rollback) |
hf_file_system.gd |
HFFileSystem |
.hflevel save/load, .map import/export, glTF export, threaded I/O, autosave failure reporting |
hf_validation_system.gd |
HFValidationSystem |
Validation, dependency checks, auto-fix helpers (vertex weld, planarity fix), bake issue detection (degenerate/floating/overlapping/non-planar/micro-gap). Configurable weld_tolerance and planarity_tolerance. Edge-key topology hashing intentionally decoupled from weld knob |
hf_visgroup_system.gd |
HFVisgroupSystem |
Visgroups (visibility groups), brush/entity grouping |
hf_carve_system.gd |
HFCarveSystem |
Boolean-subtract carve (progressive-remainder box slicing) |
hf_io_visualizer.gd |
HFIOVisualizer |
Entity I/O connection lines in viewport (ImmediateMesh) |
hf_subtract_preview.gd |
HFSubtractPreview |
Wireframe AABB intersection overlay between subtract and additive brushes (debounced, pooled) |
hf_vertex_system.gd |
HFVertexSystem |
Vertex/edge selection, move, split, merge with convexity validation. Edge sub-mode with wireframe overlay |
hf_spawn_system.gd |
HFSpawnSystem |
Player spawn lookup, validation (floor/collision/headroom), auto-fix, debug visualisation |
hf_prefab_system.gd |
HFPrefabSystem |
Prefab instance registry (stable entity UIDs), variant cycling, live-linked propagation, override tracking, push-to-source |
addons/hammerforge/ui/collapsible_section.gd:HFCollapsibleSection-- reusable collapsible section with toggle-header buttonaddons/hammerforge/highlight.gdshader: selection highlight shader (wireframe, unshaded, alpha)addons/hammerforge/hf_prototype_textures.gd:HFPrototypeTextures-- 150 built-in SVG textures (15 patterns x 10 colors) with static catalog APIaddons/hammerforge/textures/prototypes/: embedded SVG texture library for greyboxingaddons/hammerforge/hf_measure_tool.gd:HFMeasureTool-- multi-ruler measurement tool (tool_id=100, persistent rulers, angle display, snap reference)addons/hammerforge/hf_decal_tool.gd:HFDecalTool-- decal placement tool (tool_id=101, raycast + surface-normal Decal nodes)addons/hammerforge/hf_polygon_tool.gd:HFPolygonTool-- convex polygon → extruded brush (tool_id=102, KEY_P)addons/hammerforge/hf_path_tool.gd:HFPathTool-- waypoint path → corridor brushes with miter joints (tool_id=103, KEY_SEMICOLON)addons/hammerforge/ui/hf_tutorial_wizard.gd: interactive 5-step tutorial wizard (Draw → Subtract → Paint → Entity → Bake)addons/hammerforge/ui/hf_shortcut_dialog.gd: searchable shortcut reference dialog with category groupingaddons/hammerforge/ui/hf_prefab_library.gd: prefab library dock section with drag-and-dropaddons/hammerforge/paint/*: floor paint grid, layers, tools, inference, geometry synthesis, reconciliation, heightmap integrationaddons/hammerforge/paint/hf_region_manager.gd: region streaming helpers (region bounds, radius, index)addons/hammerforge/hflevel_io.gd: variant encoding/decoding for .hflevel formataddons/hammerforge/map_io.gd: .map file import/exportaddons/hammerforge/prefab_factory.gd: advanced shape generation (wedges, prisms, platonic solids, etc.)
| Script | class_name | Responsibility |
|---|---|---|
hf_paint_grid.gd |
HFPaintGrid |
Grid storage, coordinate conversion |
hf_paint_layer.gd |
HFPaintLayer |
Layer data: bitset + material_ids + blend_weights (+ _2/_3) + heightmap |
hf_paint_layer_manager.gd |
HFPaintLayerManager |
Multi-layer management, active layer |
hf_paint_tool.gd |
HFPaintTool |
Paint input, stroke handling, routes to appropriate synth. Shared build_heightmap_model() for heightmap reconciliation |
hf_stroke.gd |
HFStroke |
Stroke data (cells, timing, tool type, brush shape) |
hf_geometry_synth.gd |
HFGeometrySynth |
Greedy meshing for flat floors/walls |
hf_heightmap_synth.gd |
HFHeightmapSynth |
Heightmap-displaced mesh generation (SurfaceTool) |
hf_heightmap_io.gd |
HFHeightmapIO |
Load/generate/serialize heightmaps (base64 PNG) |
hf_generated_model.gd |
HFGeneratedModel |
Data model: FloorRect, WallSeg, HeightmapFloor |
hf_reconciler.gd |
HFGeneratedReconciler |
Stable-ID node reconciliation (floors, walls, heightmap floors) |
hf_connector_tool.gd |
HFConnectorTool |
Ramp/stair mesh generation between layers |
hf_foliage_populator.gd |
HFFoliagePopulator |
MultiMeshInstance3D procedural scatter |
hf_blend.gdshader |
-- | Four-slot blend shader (UV2 blend map, RGB weights) |
hf_inference_engine.gd |
HFInferenceEngine |
Inference for paint operations |
LevelRoot (Node3D)
- DraftBrushes
- PendingCuts
- CommittedCuts
- MaterialManager
- PaintLayers
- SurfacePaint
- Generated
- Floors
- Walls
- HeightmapFloors
- Entities
- BakedGeometry
- TempFloor (CSGBox3D, optional — created by Create Floor / New Level)
- DefaultSun (DirectionalLight3D, optional — created by New Level, state-tracked)
- Named groups (e.g. "walls", "detail") with per-group show/hide.
- Membership stored on nodes via
node.set_meta("visgroups", PackedStringArray). A node can belong to multiple visgroups. - A node in ANY hidden visgroup becomes hidden (Hammer semantics). Nodes not in any visgroup are always visible.
HFVisgroupSystemmanages CRUD, membership, visibility refresh, and serialization.- Visgroups persist in
.hflevelviacapture_visgroups()/restore_visgroups().
- Persistent groups that select and move together.
- Single group per node via
node.set_meta("group_id", group_name). - Clicking a grouped node expands selection to all group members.
- Ctrl+G groups selection, Ctrl+U ungroups.
- Groups persist in
.hflevelviacapture_groups()/restore_groups().
- When
texture_lockis enabled (default), moving or resizing a brush automatically compensates face UV offset and scale. - Per-projection-axis math in
face_data.gd:adjust_uvs_for_transform():- PLANAR_X: projects (z, y), PLANAR_Y: projects (x, z), PLANAR_Z: projects (x, y).
- BOX_UV resolves to the planar axis matching the face normal.
- CYLINDRICAL is skipped (complex, future enhancement).
- Position compensation:
uv_offset -= projected_delta * uv_scale. - Size compensation:
uv_scale *= inverse_size_ratioper projection axis. - Hook in
hf_brush_system.gd:set_brush_transform_by_id()captures old transform, applies new, then adjusts UVs.
- Restricts bake to an AABB region. Brushes outside the cordon are skipped.
- Properties on LevelRoot:
cordon_enabled: bool,cordon_aabb: AABB. - Filter applied in
hf_bake_system.gd:collect_chunk_brushes(),append_brush_list_to_csg(),_append_face_bake_container(). - Helper
_brush_in_cordon()computes brush world AABB and tests intersection withcordon_aabb. - "Set from Selection" computes merged AABB of selected brushes + 1.0 margin.
- Yellow wireframe visualization via ImmediateMesh (12 AABB edge lines, unshaded, no depth test).
- Cordon settings persist in
.hflevel.
- Draw creates DraftBrush nodes in DraftBrushes.
- Subtract brushes are staged in PendingCuts until Apply Cuts.
- Extrude Up/Down picks a face via
FaceSelector, creates a preview brush along the face normal, and commits a new DraftBrush on release. UsesHFExtrudeTool(RefCounted). - Hollow (Ctrl+H): converts a solid brush into 6 wall brushes with configurable thickness.
- Clip (Shift+X): splits a brush along an axis-aligned plane into two new brushes. Preserves material, brush entity class, visgroups, and group ID.
- Move to Floor/Ceiling (Ctrl+Shift+F/C): raycasts against other brush AABBs to snap selection vertically.
- Carve (Ctrl+Shift+R): boolean-subtract one brush from all intersecting brushes.
HFCarveSystemuses progressive-remainder algorithm to produce up to 6 box slices per target. Preserves material, operation, visgroups, group_id, brush_entity_class. - Merge (Ctrl+Shift+M): combines 2+ selected brushes into a single CUSTOM brush. Transforms all face
local_vertsand normals through the fullTransform3Dpipeline (source local → world → merged local viaaffine_inverse()), so rotated/scaled brushes merge correctly. Per-brushmaterial_overrideis registered into MaterialManager and stamped as per-facematerial_idx. Validates same operation type. Inherits metadata from first brush. - Numeric input: type exact dimensions during drag or extrude (Enter applies, Backspace edits).
- UV Justify: fit/center/left/right/top/bottom alignment modes for selected faces.
- Bake builds a temporary CSG tree from DraftBrushes + CommittedCuts and outputs BakedGeometry. If cordon is enabled, only brushes intersecting the cordon AABB are included. Brush entity classes
func_detailandtrigger_*are excluded from structural bake. - Undo/redo actions prefer brush IDs and state snapshots over long-lived Node references.
- Command collation:
HFUndoHelpersupports acollation_tagparameter. Consecutive actions with the same tag and samefull_statescope within 1 second merge into one undo entry viaMERGE_ENDS(nudge, resize, paint). Mismatchedfull_statebreaks the collation window. - Transactions:
HFStateSystemprovidesbegin_transaction()/commit_transaction()/rollback_transaction()for atomic multi-step operations. The transaction captures a state snapshot on begin and restores it on rollback.
Entity types and brush entity classes are data-driven via HFEntityDef (hf_entity_def.gd):
- Loaded from
entities.jsonatentity_defs_path(default:res://addons/hammerforge/entities.json). - Falls back to built-in defaults (func_detail, func_wall, trigger_once, trigger_multiple).
- Each definition has:
classname,description,color,is_brush_entity,properties,scene_path. HFEntityDef.load_definitions(path)returnsArray[HFEntityDef].filter_brush_entities()/filter_point_entities()for filtering by type.- Dock brush entity class dropdown is populated from definitions, not hardcoded.
- Declarative property forms: the
propertiesarray on each definition supports typed entries ({name, type, default, label}) that auto-generate dock controls (LineEdit, SpinBox, CheckBox, OptionButton, ColorPickerButton, Vector3 spinboxes) when an entity is selected. Changes write toentity.entity_dataand sync the Inspector. Inspired by QuArK's:formsystem.
HFGesture (hf_gesture.gd) is a base class for encapsulated input gestures:
- Holds
root,camera,start_position,current_position,numeric_buffer. - Subclasses override
update(event),commit(),cancel(). handle_numeric_key(keycode)routes digit/period/backspace/enter to the numeric buffer.- New tools should subclass
HFGestureto be self-contained (own state, no global mode enum needed).
MaterialManager (material_manager.gd) manages the shared material palette:
- Library persistence:
save_library(path)/load_library(path)serialize material resource paths to JSON. - Usage tracking:
record_usage()/release_usage()/rebuild_usage()track which materials are used by brushes. - Cleanup:
find_unused_materials()returns palette materials not used by any brush. - Prototype textures:
HFPrototypeTextures.load_all_into(manager)batch-loads 150 built-in SVG textures asStandardMaterial3Dresources. The dock exposes this via the "Refresh Prototypes" button in the Paint tab → Materials section. - Visual browser:
HFMaterialBrowser(ui/hf_material_browser.gd) provides a thumbnail grid with search, pattern/color filters, favorites, hover preview, context menu, and drag-and-drop. Replaces the text-onlyItemList. - Texture Picker: T key activates an eyedropper that raycasts to a face, reads
FaceData.material_idx, and sets it as the browser's current selection.
Data
- Grid -> Layer -> Chunked storage (bitset + material_ids + blend_weights + blend_weights_2 + blend_weights_3).
- Each layer optionally has a
heightmap: Imageandheight_scale: float. - Paint layers are stored under PaintLayers.
Tools
- Brush, Erase, Rect, Line, Bucket, Blend (enum
HFStroke.Tool, values 0-5). - Sculpt Raise, Sculpt Lower, Sculpt Smooth, Sculpt Flatten (values 6-9) — operate directly on heightmap Image pixels with configurable strength, radius, and falloff.
- Blend tool writes per-cell blend weights to slots B/C/D on already-filled cells.
Brush Shape
- Square: fills every cell in the radius range (full box).
- Circle: clips corners using Euclidean distance check.
Generation (flat layers -- no heightmap)
- Floors: greedy rectangle merge -> DraftBrush boxes.
- Walls: boundary edges + merged segments -> DraftBrush boxes.
Generation (heightmap layers)
- Floors: per-cell displaced quads via
HFHeightmapSynth-> ArrayMesh -> MeshInstance3D. - Per-chunk blend image (Image FORMAT_RGBA8) built from cell blend weights (RGB = slots B/C/D).
- Blend shader (
hf_blend.gdshader) mixes four slots via UV2-sampled blend map (slot A implicit). - Walls: still use flat
HFGeometrySynth(no heightmap displacement on walls).
Reconciliation
- Stable IDs for generated geometry (floors, walls, heightmap floors).
- Dirty chunk scoping to avoid unnecessary churn.
Generated/HeightmapFloorscontainer for MeshInstance3D nodes.
Auto-Connectors
HFConnectorToolgenerates ramp or stair ArrayMesh between two cells on different layers.- Ramp: sloped quad strip. Stairs: horizontal treads + vertical risers.
Foliage Populator
HFFoliagePopulatorscatters instances via MultiMeshInstance3D.- Filters by height range, slope threshold; configurable density, scale, rotation, seed.
- Floor paint chunks are grouped into regions (default 512x512 cells).
- Streaming loads regions within a radius of the cursor and unloads distant regions.
- Region files (
.hfr) store per-region chunk data to keep.hflevelsmall. - Region index is stored in the
.hflevelstate underterrain_regions.
- Entities live under LevelRoot/Entities or are tagged
is_entity. - Entities are selectable but excluded from bake.
- Definitions come from
addons/hammerforge/entities.json.
- Source-style trigger/target system modeled after Hammer/Source entity I/O.
- Connections stored as
entity_io_outputsmeta (Array of Dictionaries) on entity nodes. - Each connection:
{output_name, target_name, input_name, parameter, delay, fire_once}. HFEntitySystemprovides:add_entity_output(),remove_entity_output(),get_entity_outputs(),find_entities_by_name(),get_all_connections(),fire_output().find_entities_by_name()searches bothentities_nodeanddraft_brushes_nodefor target resolution.fire_output(entity, output_name, parameter)delegates toHFIORuntimedispatcher if present in the scene, falls back to direct multi-target resolution otherwise.- I/O connections are serialized with entity info in
.hflevelsaves and undo/redo state viacapture_entity_info()/restore_entity_from_info(). - Dock UI: collapsible "Entity I/O" section in Entities tab with fields for Output, Target, Input, Parameter, Delay, Fire Once. Add/Remove buttons and connection ItemList. Context-hidden (only visible when an entity is selected); auto-refreshes on selection change.
- Viewport visualization:
HFIOVisualizerdraws ImmediateMesh lines between connected entities. Color-coded: green=standard, orange=fire_once, yellow=selected entity connections. Throttled refresh (10 frames). "Show I/O Lines" checkbox in Entities tab. - Runtime signal translation:
HFIORuntime(hf_io_runtime.gd) auto-wiresentity_io_outputsmetadata into Godot signals on bake/export. Auto-injected byexport_playtest_scene()and optionally bypostprocess_bake()(bake_wire_ioexport). Connections keyed by node instance ID; targets resolved to all matching nodes. Delivery cascade: direct method → snake_case →_on_io_input()→ user signal. Source entities receiveio_<OutputName>user signals. Delay viaSceneTreeTimer, fire-once tracked per-connection.extra_scan_root_paths: Array[NodePath](@export) persists scan roots across scene save/reload._prune_overlapping_roots()deduplicates by instance ID and ancestor/descendant relationship.
- Brushes can be tagged with a
brush_entity_classmeta:func_detail,func_wall,trigger_once,trigger_multiple. - Tie/Untie via
HFBrushSystem.tie_brushes_to_entity()/untie_brushes_from_entity(). func_detailandtrigger_*brushes are excluded from structural bake via_is_structural_brush()inHFBakeSystem.- Visual indicators:
func_detail= cyan tint,trigger_*= orange tint (semi-transparent overlay inbrush_instance.gd).
- Stores brushes, entities, level settings, materials palette, and paint layers.
- Brush records include face data (materials, UVs, paint layers), visgroup membership, group_id, and
brush_entity_class. - Entity records include visgroup membership, group_id, and
io_outputs(Entity I/O connections). - Paint layers include grid settings, chunk size, bitset data,
material_ids,blend_weights(+ _2/_3), and terrain slot settings. - Optional per-layer:
heightmap_b64(base64 PNG),height_scale. Missing keys = no heightmap (backward-compatible). - Level settings include
texture_lock,cordon_enabled,cordon_aabb_pos,cordon_aabb_size. - Visgroup definitions and group registry stored in state via
capture_visgroups()/capture_groups().
- Temporary CSG tree for DraftBrushes (including generated flat floors/walls).
- Heightmap floor meshes are duplicated directly into baked output (bypass CSG) with trimesh collision shapes.
- Chunked baking (default
bake_chunk_size = 32): groups brushes by grid coordinate, bakes each chunk independently. - Owner assignment uses
_assign_owner_recursive()after the baked container is added to the tree (avoids premature owner errors during chunked bake). - Optional mesh merging, LODs, lightmap UV2, navmesh.
- Optional face-material bake (per-face materials, no CSG).
- Automated occluder generation (
bake_generate_occluders):postprocess_bake()scans all bakedMeshInstance3Dnodes (recursing intoBakedChunk_*intermediary nodes), groups coplanar triangles by normal (5° threshold) and plane distance (0.1 unit threshold), and createsOccluderInstance3DwithArrayOccluder3Dper group exceedingbake_occluder_min_area(default 4.0 world units²). Occluders are parented under anOccludersNode3D child of the baked container. Idempotent — re-bake replaces previous occluders. - Collision uses Add brushes only.
- Navmesh defaults:
cell_height = 0.25(matches Godot NavigationServer3D map default). - Bake Selected (
bake_selected()): bakes only selected brushes and merges output into the existingbaked_container(does not replace it). - Bake Changed (
bake_dirty()): bakes only brushes with dirty tags (_dirty_brush_ids). Tags are cleared only when_last_bake_successis true; failed bakes retain all dirty tags. - Preview modes (
PreviewModeenum: FULL, WIREFRAME, PROXY):_apply_preview_visuals()overrides material on baked meshes. Wireframe uses inlineShaderMaterialwithrender_mode wireframe. Proxy uses unshaded semi-transparentStandardMaterial3D. - Bake time estimate (
estimate_bake_time()): ratio-based extrapolation from_last_bake_duration_msand brush count. - Bake issue detection (
HFValidationSystem.check_bake_issues()): returns Array of{type, severity, message, node}dicts. Checks: degenerate brush (sev=2), oversized (sev=1), floating subtract (sev=1), overlapping subtracts (sev=1), non-manifold edges (sev=2), open edges (sev=1), non-planar faces (sev=1), micro-gaps between brushes (sev=1), occlusion coverage (sev=0 info or sev=1 warning). Non-planar detection usesplanarity_tolerance(default 0.01). Micro-gap detection usesweld_tolerance(default 0.001). Both use 27-cell spatial hash neighbor lookup for boundary-safe distance checks._edge_key()for topology (non-manifold/open-edge) uses fixed 0.001 precision, intentionally decoupled fromweld_tolerance. - Auto-fix helpers:
weld_brush_vertices(brush)snaps near-coincident vertices withinweld_tolerancevia BFS grouping +ensure_geometry()refresh.fix_non_planar_faces(brush)projects drifting vertices onto the best-fit plane from each face's first 3 vertices.
Face data is stored per DraftBrush face with material assignment, UV projection, and optional paint layers. Surface paint is a per-face splat system. It updates preview materials and can be baked using the face-material bake option.
plugin.gd (input)
-> level_root.gd (coordinator)
-> hf_drag_system.gd (draw tool: drag lifecycle + preview)
-> hf_extrude_tool.gd (extrude up/down: face pick + drag + commit)
-> hf_brush_system.gd (brush CRUD, pending cuts, materials)
-> hf_paint_system.gd (floor + surface paint)
-> hf_bake_system.gd (CSG assembly + mesh output)
-> hf_state_system.gd (undo/redo state capture)
-> hf_file_system.gd (persistence, threaded I/O)
-> hf_grid_system.gd (editor grid)
-> hf_entity_system.gd (entity placement)
-> hf_visgroup_system.gd (visgroups + grouping)
All public methods on LevelRoot are thin one-line delegates to the appropriate subsystem. This preserves the existing API so plugin.gd and dock.gd call root.bake(), root.begin_drag(), etc. without change.
plugin.gd's _forward_3d_gui_input() is a ~50-line dispatcher that routes to focused handlers: _handle_paint_input(), _handle_keyboard_input(), _handle_rmb_cancel(), _handle_select_mouse(), _handle_extrude_mouse(), _handle_draw_mouse(), _handle_mouse_motion(). A shared _get_nudge_direction() helper is used by both _forward_3d_gui_input() and _shortcut_input().
input_state.gd (HFInputState) replaces 18+ loose state variables with an explicit Mode enum:
| Mode | Description |
|---|---|
IDLE |
No active operation |
DRAG_BASE |
Drawing the base rectangle of a new brush |
DRAG_HEIGHT |
Setting the height of a new brush |
SURFACE_PAINT |
Actively painting on a brush face |
EXTRUDE |
Actively extruding a face up or down |
Transitions: begin_drag() -> advance_to_height() -> end_drag() / cancel(). Extrude: begin_extrude() -> end_extrude() / cancel().
The dock uses 4 tabs with collapsible sections for visual hierarchy:
| Tab | Contents |
|---|---|
| Brush | Shape, size, grid snap, quick snap presets, material picker, operation mode (Add/Sub), texture lock |
| Paint | 7 collapsible sections: Floor Paint, Heightmap, Blend & Terrain, Regions, Materials (with Refresh Prototypes), UV Editor, Surface Paint |
| Entities | Entity palette with drag-and-drop, Create DraftEntity, Entity Properties + Entity I/O + I/O Wiring (context-hidden collapsible sections, visible only when entity selected) |
| Manage | Bake, Actions (floor/cuts/clear), File, Presets, History, Settings, Performance, plus Visgroups & Cordon (inserted programmatically) |
- Brush tab includes contextual Selection Tools section (hollow, clip, merge, move, tie, duplicator) visible when brushes are selected.
- Tab contents built by dedicated builder classes:
PaintTabBuilder,EntityTabBuilder,ManageTabBuilder,SelectionToolsBuilder(inui/). Each is RefCounted withbuild()andconnect_signals()methods. Dock delegates to builders, reducingdock.gdby ~35%. - Collapsible sections have HSeparator, 4px indented content, and persisted collapsed state. All 18 sections tracked in
_all_sectionsdict. - "No LevelRoot" banner and autosave warning defined in dock.tscn.
- Compact toolbar: single-char labels (D, S, +, -, P, ▲, ▼) with descriptive tooltips. VSeparator before extrude buttons.
- Signal-driven sync: Setting controls push values via
toggled/value_changedsignals. Paint layers, materials, surface paint, and face selection sync instantly viapaint_layer_changed,material_list_changed,face_selection_changed,selection_changedsignals. Initial sync on root connect populates materials and surface paint. Perf panel throttled to every 30 frames; disabled hints are flag-driven. Form label widths standardized to 70px.
plugin.gduses stickyactive_root: selecting non-LevelRoot nodes does not null the reference._handles()returns true for any node when a LevelRoot exists in the scene (deep recursive search)._edit()only nullsactive_rootwhen the root node is removed from the tree.dock.gdmirrors the sticky pattern and uses_find_level_root_in()for deep tree search.
plugin.gdsuppresses spurious emptyselection_changedsignals viashould_suppress_empty_selection()(static method). Texture reimport can trigger Godot's EditorSelection to emit empty selections; the guard ignores these whenhf_selectionis non-empty.- Intentional deselect protocol: clear
hf_selectionbefore callingeditor_selection.clear(). Plugin deselect paths (Escape, delete, duplicate) do this directly. Dock deselect paths (_on_clear_selection_pressed,_on_commit_cuts) emitselection_clear_requestedsignal; plugin's_on_dock_selection_clearhandler clearshf_selectionin response.
dock.resolve_material_assign_action(mat_index)returns{action, method, args, toast}. Used by_on_material_assign()and_on_browser_material_double_clicked().- Priority: face selection (via
_count_selected_faces()) > whole-brush (via_get_selected_brush_ids()) > error toast. - Context menu options "Apply to Selected Faces" and "Apply to Whole Brush" remain explicit (no fallback).
All keyboard shortcuts are data-driven via HFKeymap (hf_keymap.gd). Plugin loads bindings from user://hammerforge_keymap.json (or built-in defaults). Each binding maps an action name (e.g. "hollow") to {keycode, ctrl, shift, alt}. Plugin uses _keymap.matches(action, event) instead of hardcoded KEY_* checks. Toolbar labels and tooltips pull display strings from the keymap.
HFUserPrefs (hf_user_prefs.gd) stores cross-session application-scoped preferences in user://hammerforge_prefs.json. Separate from per-level settings on LevelRoot. Includes: default grid snap, autosave interval, recent files (max 10, MRU), collapsed section states, last tool ID, HUD visibility.
LevelRoot maintains dirty tags for selective reconciliation:
tag_brush_dirty(brush_id)— marks a specific brush as needing rebuild.tag_paint_dirty(chunk_coord)— marks a paint chunk as dirty.tag_full_reconcile()— marks entire scene for full rebuild (structural changes like hollow/clip).consume_dirty_tags()— returns and clears all tags (called by reconciler).
Brush system calls these on create/delete/transform/hollow/clip. Tags are guarded with has_method() for test shim compatibility.
LevelRoot supports batched signal emission for multi-brush operations:
begin_signal_batch()/end_signal_batch()with depth-counted nesting.- During batch, signals are queued. On flush, brush add/remove/change signals coalesce into a single
selection_changedemission. - Transactions auto-batch:
begin_transaction()callsbegin_signal_batch();commit_transaction()callsend_signal_batch(). discard_signal_batch()drops queued signals on rollback.
HFEditorTool exposes can_activate(root) and get_poll_fail_reason(root). HFGesture exposes can_start(root). Dock uses these to gray out buttons and set tooltips. Plugin guards keyboard shortcuts with early-exit when poll fails (e.g. Hollow requires selection).
External tools expose get_settings_schema() → Array of {name, type, label, default, min, max, options}. Supported types: bool, int, float, string, enum, color. Dock rebuild_tool_settings() auto-generates controls from the schema. get_setting(key) / set_setting(key, val) for storage.
- Theme-aware dock styling with comprehensive tooltips on all controls.
- Context-sensitive shortcut HUD overlay (8 views: draw idle, dragging base, adjusting height, select, extrude idle, extruding active, floor paint, surface paint). Displays current axis lock. Updated via
plugin.gd->shortcut_hud.gd:update_context(). - Customizable keyboard shortcuts -- all bindings data-driven via
HFKeymapJSON. - Status bar mode indicator -- shows active mode (Draw/Select/Extrude/Paint) with live state updates.
- Tool poll -- buttons gray out with tooltip when action can't run (e.g. Hollow with no selection).
- Paint tool keyboard shortcuts (B/E/R/L/K) active when Paint Mode is enabled.
- Selection count in status bar, updated on every selection change.
- Color-coded status bar: errors in red (auto-clear 5s), warnings in yellow, success messages auto-clear after 3s.
- Bake progress bar with chunk status updates.
- Pending subtract brushes rendered in orange-red with high emission (
_make_pending_cut_material()), visually distinct from applied cuts (standard red via_make_brush_material()). - Shader-based editor grid with follow mode.
- Grouping shortcuts: Ctrl+G (group selection), Ctrl+U (ungroup).
- Brush operations: Ctrl+H (hollow), Shift+X (clip), Ctrl+Shift+F (floor), Ctrl+Shift+C (ceiling).
- Direct typed calls between plugin/dock/LevelRoot (no duck-typing).
- O(1) brush ID lookup and brush count via
_brush_cache/_brush_countinHFBrushSystem. - Material instance caching in
HFBrushSystem(composite key: operation/solid/unshaded). - Persistent cordon
ImmediateMeshreused viaclear_surfaces()(no per-call allocation). - Selection highlight uses external
highlight.gdshader(no inline GLSL strings).
- Validate Level scans for missing materials, zero-size brushes, invalid face indices, and paint layers without grids.
- Auto-fix clears invalid face selections, resets invalid face material indices, and rebuilds missing layer grids.
- Bake Dry Run reports counts and chunking without generating geometry.
- Performance panel shows active brush count (with ProgressBar), entity count, vertex estimate, paint memory, bake chunk count, last bake time, recommended chunk size, and health summary (green/yellow/red).
Unit tests use the GUT framework and run headless via CI.
| Test File | Tests | Coverage |
|---|---|---|
test_visgroup_system.gd |
18 | Visgroup CRUD, visibility toggle, membership, round-trip serialization |
test_grouping.gd |
9 | Group creation, meta storage, ungroup, regroup, serialization |
test_texture_lock.gd |
10 | UV offset/scale compensation for PLANAR_X/Y/Z, BOX_UV, CYLINDRICAL |
test_cordon_filter.gd |
10 | AABB intersection, cordon-filtered collection, chunk_coord utility |
test_keymap.gd |
16 | Default bindings, key matching (simple/ctrl/shift/ctrl+shift), modifier rejection, display strings, rebinding, JSON roundtrip |
test_user_prefs.gd |
12 | Default values, get/set prefs, section collapse state, recent files (add/dedup/max 10), JSON roundtrip, hint dismissed/dismiss/roundtrip |
test_dirty_tags.gd |
11 | Brush dirty tags, paint chunk tags, full reconcile flag, consume-clears, signal batch queue/flush/discard/nesting |
test_prototype_textures.gd |
27 | Catalog constants, path generation, texture existence, material persistence (resource_path), batch loading into MaterialManager |
test_op_result.gd |
15 | HFOpResult constructors, hollow/clip/delete return values, fail emits user_message, fix_hint population |
test_snap_system.gd |
12 | Grid/Vertex/Center snap modes, threshold, exclude list, priority, empty scene fallback |
test_drag_dimensions.gd |
8 | get_drag_dimensions() in all modes, format_dimensions() whole/fractional/zero |
test_reference_cleanup.gd |
9 | Delete cleans group/visgroup membership, entity I/O cleanup_dangling_connections |
test_bake_system.gd |
38 | build_bake_options, structural/trigger filtering, chunk_coord, bake_dry_run, warn_bake_failure, estimate_bake_time, preview modes, _last_bake_success, dirty tag retention, wireframe ShaderMaterial |
test_bake_issues.gd |
10 | check_bake_issues: degenerate, oversized, floating subtract, overlapping subtracts, clean level, entity skip |
test_weld_and_planarity.gd |
21 | Non-planar detection, vertex welding + ensure_geometry refresh, planarity auto-fix, micro-gap detection, edge-key independence, boundary-straddling coverage, MapIO integration + unit |
test_quick_play_modes.gd |
12 | Severity blocking (0/1/2), cordon save/restore, dirty tag retention, camera yaw via entity_data, spawn restore |
test_integration.gd |
22 | End-to-end: brush lifecycle, paint + heightmap, entity workflow, visgroup cross-system, snap, bake, I/O cleanup, info round-trip |
test_shortcut_dialog.gd |
8 | Category assignment (tools, paint, axis lock, editing), action labels, get_all_bindings copy safety |
test_tutorial_wizard.gd |
7 | Step advancement, persistence, skip/dismiss, validate_subtract, no-root safety |
test_subtract_preview.gd |
8 | AABB intersection math (overlapping, no-overlap, contained, partial axis), enable/disable, debounce |
test_prefab.gd |
11 | Empty prefab, to_dict/from_dict roundtrip, transform preservation, file save/load, invalid data, entity I/O |
Total: 807 tests across 47 files.
Tests use root shim scripts (dynamically created GDScript) to provide the LevelRoot interface without circular preload dependencies. Configuration in .gutconfig.json.