Thanks for helping improve HammerForge.
- Small, focused changes are preferred.
- Large changes should start with an issue or discussion before a PR.
- Keep changes aligned with the current MVP and architecture.
- Open an issue describing the problem and proposed fix.
- Keep PRs small and limited to one topic.
- Update docs and tests when behavior changes.
- Run formatting, lint, and test checks before submitting (see below).
- Follow the subsystem architecture (LevelRoot is the public API).
- Prefer undo actions that use stable IDs and state snapshots. Use
collation_tagfor rapid operations. - Use transactions (
begin_transaction/commit_transaction) for multi-step brush operations. - New entity types go in
entities.json, not hardcoded in GDScript. - New input tools should subclass
HFGesturefor self-contained state management. - Subscribe to LevelRoot signals instead of polling in
_process(). - Keyboard shortcuts go through
_keymap.matches("action_name", event), never hardcodedKEY_*checks. Add new default bindings inHFKeymap._default_bindings(). Toolbar uses single-char labels with tooltips. - External tools should implement
can_activate()for tool availability andget_settings_schema()for auto-generated dock UI. Seehf_editor_tool.gdfor the full API. - New dock sections should use
HFCollapsibleSection.create()and register with_register_section()for persisted collapse state. Use 70px label widths for form rows. - User-facing messages should use
dock.show_toast(msg, level)orroot.user_message.emit(msg, level)instead of (or in addition to)push_error/push_warning. Level: 0=INFO, 1=WARNING, 2=ERROR. - Brush mutations should call
root.tag_brush_dirty(id)(guarded withhas_method) so the reconciler can skip unchanged geometry. - Multi-brush operations should wrap in
begin_signal_batch()/end_signal_batch()(or use transactions, which batch automatically) to prevent UI thrash. - User preferences (application-scoped) go in
HFUserPrefs. Level settings go on LevelRoot. - Operations that can fail (hollow, clip, delete) should return
HFOpResult. Use_op_fail(msg, hint)in brush_system to emituser_messageand return a fail result in one call. Include an actionablefix_hintstring so users know how to resolve the issue. - Destructive operations should preview before commit: operations that permanently modify or delete geometry (carve, clip, hollow) must show a wireframe overlay preview and a
ConfirmationDialogbefore executing. Pattern: (1) validate withcan_*(), (2) callpreview.show_preview(...), (3) createConfirmationDialog, (4) on confirmed: clear preview + commit viaHFUndoHelper; on canceled: clear preview +queue_free()dialog. All lambdas must guard withis_instance_valid(self)andis_instance_valid(root). Use_add_confirmable_dialog(dlg)in plugin.gd to track dialogs for teardown cleanup. - Bulk delete confirmation: deleting 3+ brushes should show a confirmation dialog. Single/dual deletes remain instant to avoid friction. The dialog should remind users of Ctrl+Z availability.
- Snapping goes through
HFSnapSystem(onlevel_root.snap_system). New snap modes should be added as bitmask flags inSnapModeenum and collected in_collect_candidates(). - Deletion cleanup is handled automatically by
_cleanup_brush_references()in brush_system. If you add new cross-reference types (beyond groups, visgroups, entity I/O), add cleanup logic there. - Viewport hints should be added to
MODE_HINTSinshortcut_hud.gd. Each mode key maps to an instructional string. Hints auto-dismiss and persist viahf_user_prefs.gd. - Prefabs use
HFPrefab(hf_prefab.gd) for capture/instantiate. Add new prefab-related UI toui/hf_prefab_library.gd. Prefab instantiation should always usebegin_signal_batch()/end_signal_batch(). - Dock tab builders: New UI sections should be added to the appropriate builder file (
ui/paint_tab_builder.gd,ui/entity_tab_builder.gd,ui/manage_tab_builder.gd,ui/selection_tools_builder.gd) rather than directly indock.gd. Each builder hasbuild()(creates controls) andconnect_signals()(wires them up). - Registered tools (HFEditorTool subclasses) get automatic dock settings UI via
get_settings_schema(), keyboard dispatch viahandle_keyboard(), and poll-based button state viacan_activate(). Register in plugin.gd via_tool_registry.register_tool(). Tools that create brushes should useself.undo_redo(set by the registry on activation). - Vertex system operations (
split_edge,merge_vertices) should useget_pre_op_snapshots()for face snapshot undo. Edge splitting skips convexity validation (mathematically safe on convex hulls). Vertex merging validates convexity and reverts on failure. - Wireframe overlay refresh: After replacing
mesh_instance.mesh(e.g. inrebuild_preview()), always call_apply_brush_entity_overlay(),_apply_subtract_wireframe_overlay(), and_apply_additive_wireframe_overlay(). Failing to refresh overlays causes wireframe drift. Color convention: green=additive, red=subtractive, blue=entity. - Grid snap changes must emit
grid_snap_applied: If adding a new code path that changesgrid_snap, ensure it flows throughdock._apply_grid_snap()or that the root'sgrid_snap_changedsignal fires (which dock relays viagrid_snap_applied). This keeps the viewport HUD indicator in sync. - Face winding convention: All faces must use CW vertex winding as seen from outside the brush (Godot 4's front-face convention).
_compute_normal()produces outward normals for CW faces automatically. Never negate normals manually afterensure_geometry(). When adding new face generators, verify normals point outward from the brush centroid. - Polygon/path tools create brushes via
root.brush_system.create_brush_from_info()with afaceskey containing serialized face data. UseFaceData.from_dict()/to_dict()for serialization. Face dicts includewinding_version: 1; omitting this key triggers load-time migration. - Spawn system (
root.spawn_system): useget_active_spawn()for primary-flag-aware spawn lookup,validate_spawn()for physics-based validation,auto_fix_spawn()to apply suggested fixes,create_default_spawn()for fallback creation. Quick Play flow calls these automatically. Debug visualisation viashow_validation_debug()/cleanup_debug(). Spawn properties (primary,angle,height_offset) are defined inentities.jsonand auto-generated in the Entities dock. - Validation tolerances:
HFValidationSystemhas two configurable tolerances —weld_tolerance(default 0.001) for vertex coincidence in welding/micro-gap detection, andplanarity_tolerance(default 0.01) for face-plane deviation. The_edge_key()function used by non-manifold/open-edge topology checks uses a fixed 0.001 precision and must NOT be coupled toweld_tolerance— changing_edge_keyprecision would mask real topology issues when users raise the weld knob. Keep new spatial-hash lookups distance-based with 27-cell neighbor search (see_cell_keys()) rather than single-bucket — bucket boundaries silently miss valid pairs. Always callface.ensure_geometry()after mutatinglocal_vertsso normals and bounds stay in sync. - Incremental bake:
bake_selected()merges into the existingbaked_container— never replace the container wholesale.bake_dirty()uses_last_bake_successto decide whether to clear dirty tags; failed bakes must retain all tags so they can be retried. - Bake preview modes: use the
PreviewModeenum (FULL, WIREFRAME, PROXY). Wireframe must useShaderMaterialwithrender_mode wireframe—StandardMaterial3Dhas nowireframeproperty in Godot 4.6. - Quick Play variants:
_on_quick_play_from_camera()and_on_quick_play_selected_area()must follow the same severity ≥ 2 blocking, auto-create, and fix-dialog patterns as_on_quick_play(). Both must restore temporary state (spawn position/angle, cordon) on both success and error paths. Use_restore_spawn()helper and explicit type annotations (e.g.var old_pos: Vector3 =) to avoid GDScript:=inference failures with untyped spawn references. - Camera yaw propagation: write yaw to
entity_data["angle"](notset_meta). The playtest runtime readsdeg_to_rad(entity_data.get("angle", 0.0))atlevel_root.gdline ~1979. - Avoid adding new dependencies unless necessary.
gdformat --check addons/hammerforge/
gdlint addons/hammerforge/
Tests live in tests/ and use the GUT framework (installed in addons/gut/).
Run all tests headless:
godot --headless -s res://addons/gut/gut_cmdln.gd --path .
If you get "class_names not imported", run godot --headless --import --path . first.
- Test files go in
tests/with thetest_prefix (e.g.test_my_feature.gd). - Extend
GutTestand useassert_eq,assert_true,assert_almost_eq, etc. - Use root shim scripts (dynamically created GDScript) to avoid circular dependency with LevelRoot. See existing tests for the pattern.
- Keep tests focused: one behavior per test function.
- For negative-path tests that trigger runtime warnings, use
HFLog.warn()in production code andHFLog.begin_test_capture()/end_test_capture()in tests. This prevents expected warnings from polluting the test output. Seetest_bevel.gdfor the pattern.
All checks (format, lint, unit tests) run automatically on push/PR to main via GitHub Actions.
- Be clear about tradeoffs and known limitations.
- Include before/after behavior notes in PR descriptions.