Skip to content

Commit af2ea11

Browse files
author
Shane Wall
committed
Add snap modes, operation results, validation gates, and coverage/docs updates
This commit expands HammerForge's editor UX and operation safety in several related areas, while also updating the surrounding docs and test coverage to match the new behavior. Editor and UX changes: - add a centralized HFSnapSystem with grid, vertex, and center snap modes - wire snap_system into LevelRoot and expose snap mode controls in the dock - improve drag HUD messaging by surfacing live W x H x D dimensions during base and height placement - add HFOpResult as a lightweight operation result type with message, optional fix hint, and user_text() formatting for user-facing warnings Operation safety and cleanup: - return HFOpResult from brush delete, hollow, and clip operations instead of silently no-oping on failure - add can_hollow_brush() and can_clip_brush() pre-validation helpers so editor handlers can reject invalid operations before creating undo entries - update plugin and dock handlers to gate hollow/clip undo commits on validator success and surface formatted user-facing error text - skip the active preview brush in snap candidate collection to avoid self-snapping during drag placement - clean up brush cross-references on deletion, including group membership, visgroup metadata, and dangling entity I/O connections Entity and runtime support: - add cleanup_dangling_connections() support to the entity system and expose delegates on LevelRoot - preserve the newer snap and operation result behavior through the LevelRoot API surface - fix plugin-side parsing/lint issues by restoring the HFInputState preload and simplifying get_drag_dimensions() control flow Tests and documentation: - add coverage for drag dimension formatting, snap system behavior, operation result helpers, validation gates, and reference cleanup - document validator/user_text() test scope honestly instead of overstating handler integration coverage - update README, changelog, roadmap, contribution/development docs, install/upgrade notes, MVP guide, and user guide for the new editor behavior and workflow expectations Validation performed locally: - gdformat --check addons/hammerforge/ - gdlint addons/hammerforge/ - Godot 4.6.1 headless import/parse pass - targeted GUT runs for test_snap_system and test_op_result during review/fixup
1 parent 99d8642 commit af2ea11

27 files changed

Lines changed: 1391 additions & 49 deletions

CHANGELOG.md

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

66
## [Unreleased]
77
### Added
8+
- **FreeCAD-Inspired Improvements (Mar 2026):**
9+
- **Operation result reporting** (`hf_op_result.gd`): `HFOpResult` lightweight result class returned
10+
by `hollow_brush_by_id()`, `clip_brush_by_id()`, and `delete_brush_by_id()`. Carries `ok`, `message`,
11+
and `fix_hint` fields. Failed operations now surface actionable toast notifications (e.g. "Wall
12+
thickness 6 is too large for brush (smallest dim 10) — Use a thickness less than 5") instead of
13+
silently returning. `_op_fail()` helper emits `user_message` signal at WARNING level automatically.
14+
- **Geometry-aware snap system** (`hf_snap_system.gd`): centralized `HFSnapSystem` with three snap
15+
modes — **Grid** (existing behavior), **Vertex** (8 box corners of all brushes), and **Center**
16+
(brush centers). Closest geometry candidate within threshold beats grid snap. `_snap_point()` in
17+
`level_root.gd` now delegates to the snap system. Dock shows G/V/C toggle buttons below the grid
18+
snap row. Replaces the previous grid-only snapping.
19+
- **Live dimensions during drag**: `input_state.gd` gains `get_drag_dimensions()` and
20+
`format_dimensions()`. The mode indicator banner now shows real-time brush dimensions during
21+
DRAG_BASE and DRAG_HEIGHT gestures (e.g. "Step 1/2: Draw base — 64 x 32 x 48",
22+
"Step 2/2: Set height — 64 x 96 x 48").
23+
- **Reference cleanup on deletion**: `delete_brush()` now calls `_cleanup_brush_references()` which
24+
strips group membership (auto-cleans empty groups), clears visgroup meta, and warns via toast when
25+
entity I/O connections targeting the deleted node are removed. New
26+
`cleanup_dangling_connections(deleted_name)` on `HFEntitySystem` removes all I/O connections
27+
targeting a deleted node and returns the removal count. Exposed on LevelRoot as a delegate.
28+
- **GUT tests** for new systems: `test_op_result.gd` (15), `test_snap_system.gd` (12),
29+
`test_drag_dimensions.gd` (8), `test_reference_cleanup.gd` (9) = 44 new tests.
30+
Total: 413 tests across 27 files.
831
- **UX Intuitiveness Overhaul (Mar 2026):**
932
- **Mode indicator banner**: colored banner between toolbar and tabs shows current tool, gesture
1033
stage ("Step 1/2: Draw base"), and numeric input. Color-coded per tool: Draw (blue), Select

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Thanks for helping improve HammerForge.
2727
- **Brush mutations** should call `root.tag_brush_dirty(id)` (guarded with `has_method`) so the reconciler can skip unchanged geometry.
2828
- **Multi-brush operations** should wrap in `begin_signal_batch()` / `end_signal_batch()` (or use transactions, which batch automatically) to prevent UI thrash.
2929
- **User preferences** (application-scoped) go in `HFUserPrefs`. **Level settings** go on LevelRoot.
30+
- **Operations that can fail** (hollow, clip, delete) should return `HFOpResult`. Use `_op_fail(msg, hint)` in brush_system to emit `user_message` and return a fail result in one call. Include an actionable `fix_hint` string so users know how to resolve the issue.
31+
- **Snapping** goes through `HFSnapSystem` (on `level_root.snap_system`). New snap modes should be added as bitmask flags in `SnapMode` enum and collected in `_collect_candidates()`.
32+
- **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.
3033
- Avoid adding new dependencies unless necessary.
3134

3235
## Running Checks Locally

DEVELOPMENT.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Development Guide
22

3-
Last updated: March 24, 2026
3+
Last updated: March 26, 2026
44

55
This document covers local setup, codebase structure, and how to test features.
66

@@ -36,6 +36,8 @@ addons/hammerforge/
3636
hf_tool_registry.gd Plugin API: tool registration, dispatch, external tool loader
3737
hf_keymap.gd Customizable keyboard shortcuts (JSON load/save, action matching)
3838
hf_user_prefs.gd Cross-session user preferences (user://hammerforge_prefs.json)
39+
hf_snap_system.gd Centralized snap (Grid/Vertex/Center modes, threshold-based candidates)
40+
hf_op_result.gd Lightweight operation result (ok, message, fix_hint)
3941
surface_paint.gd Per-face surface paint tool
4042
uv_editor.gd UV editing dock
4143
highlight.gdshader Selection highlight shader (wireframe, unshaded, alpha)
@@ -117,13 +119,17 @@ addons/hammerforge/
117119
- **Declarative tool settings.** External tools expose `get_settings_schema()` → Array of `{name, type, label, default, min, max, options}`. Dock auto-generates controls via `rebuild_tool_settings()`. Use `get_setting(key)` / `set_setting(key, val)` for storage.
118120
- **Tag-based invalidation.** Call `root.tag_brush_dirty(id)` when a brush is modified; `root.tag_full_reconcile()` for structural changes (hollow, clip). Guard with `root.has_method("tag_brush_dirty")` for test shim compatibility.
119121
- **Signal batching.** Wrap multi-brush operations in `root.begin_signal_batch()` / `root.end_signal_batch()`. Transactions do this automatically. On rollback, call `root.discard_signal_batch()` to drop queued signals without emission.
122+
- **Operation results.** Methods that can fail (hollow, clip, delete) return `HFOpResult` with `ok`, `message`, and `fix_hint`. Use `_op_fail(msg, hint)` in brush_system to both emit `user_message` and return a fail result. Callers can check `result.ok` programmatically, but failures also auto-toast via the `user_message` signal.
123+
- **Geometry-aware snapping.** `_snap_point()` delegates to `HFSnapSystem`. Three modes (Grid=1, Vertex=2, Center=4) as a bitmask. Vertex mode collects 8 box corners from all brushes; Center mode collects brush centers. Closest candidate within `snap_threshold` beats grid snap. Pass `exclude_ids` to skip the brush being dragged.
124+
- **Reference cleanup.** `delete_brush()` calls `_cleanup_brush_references()` which strips group_id meta (+ cleans empty groups via `visgroup_system._cleanup_empty_group()`), clears visgroup membership, and calls `entity_system.cleanup_dangling_connections()` to remove I/O connections targeting the deleted node. Always fires before the node is removed from the tree.
125+
- **Live dimensions.** `input_state.get_drag_dimensions()` returns `Vector3(W, H, D)` during DRAG_BASE/DRAG_HEIGHT; `Vector3.ZERO` otherwise. `format_dimensions()` renders as `"64 x 32 x 48"` (whole numbers omit decimals). The mode indicator banner appends dimensions to the stage hint during drag gestures.
120126

121127
### CI
122128

123129
The project has a GitHub Actions workflow (`.github/workflows/ci.yml`) that runs on push and PR to `main`:
124130
- `gdformat --check` -- verifies formatting
125131
- `gdlint` -- checks lint rules (configured in `.gdlintrc`)
126-
- **GUT unit tests** -- 371 tests across 23 test files (runs Godot headless)
132+
- **GUT unit tests** -- 413 tests across 27 test files (runs Godot headless)
127133

128134
Run locally before pushing:
129135
```
@@ -161,6 +167,10 @@ Tests live in `tests/` and use the [GUT](https://github.com/bitwes/Gut) framewor
161167
| `test_user_prefs.gd` | 9 | Default values, get/set prefs, section collapsed state, recent files (add/dedup/max 10), JSON roundtrip |
162168
| `test_dirty_tags.gd` | 11 | Brush dirty tags (add/dedup), paint chunk tags, full reconcile flag, consume-clears, signal batch queue/flush/discard/nesting |
163169
| `test_prototype_textures.gd` | 27 | Catalog constants, path generation, texture existence, material persistence (resource_path), batch loading into MaterialManager |
170+
| `test_op_result.gd` | 15 | HFOpResult constructors, hollow/clip/delete return values, fail emits user_message, fix_hint population |
171+
| `test_snap_system.gd` | 12 | Grid/Vertex/Center snap modes, threshold, exclude list, priority (closer geometry beats grid), empty scene fallback |
172+
| `test_drag_dimensions.gd` | 8 | get_drag_dimensions() in all modes, format_dimensions() whole/fractional/zero |
173+
| `test_reference_cleanup.gd` | 9 | Delete cleans group/visgroup membership, entity I/O cleanup_dangling_connections, preserves unrelated, no-crash on clean node |
164174

165175
Run all tests:
166176
```
@@ -236,6 +246,28 @@ Selection Tools (Brush tab — visible when brushes are selected)
236246
- Select a different entity -- confirm the I/O list updates to show that entity's connections.
237247
- Save .hflevel with entity I/O connections, reload, and confirm connections persist.
238248

249+
Snap Modes
250+
- In Brush tab, click V (Vertex) toggle next to Grid Snap presets.
251+
- Place a brush, then start drawing another near a corner of the first brush -- confirm it snaps to the exact corner.
252+
- Click C (Center) toggle. Draw a brush near the center of an existing brush -- confirm it snaps to the center.
253+
- Disable G (Grid) and both V and C -- confirm brush placement is unsnapped.
254+
- Re-enable G -- confirm grid snapping resumes.
255+
256+
Live Dimensions
257+
- Start drawing a brush and observe the mode indicator banner showing "Step 1/2: Draw base — W x H x D" with live updating dimensions.
258+
- Click to advance to height stage and observe "Step 2/2: Set height — W x H x D" with height updating as you move the mouse.
259+
- Type "64" and press Enter -- confirm the dimension display reflects the typed value.
260+
261+
Operation Feedback
262+
- Select a very small brush (e.g. 4x4x4) and press Ctrl+H with wall thickness 4 -- confirm a toast appears with "Wall thickness too large" and a fix hint.
263+
- Select a brush and press Ctrl+H with a valid thickness -- confirm success (no error toast, 6 walls created).
264+
- Press Shift+X on a brush -- confirm success toast or appropriate error if split position is invalid.
265+
266+
Reference Cleanup
267+
- Place a brush, add it to a group (Ctrl+G), then delete it. Confirm the group is automatically cleaned up.
268+
- Place a brush, add it to a visgroup in the Manage tab, then delete the brush. Confirm the visgroup no longer lists the deleted brush.
269+
- Create two entities with an I/O connection between them. Delete the target entity. Confirm a toast reports the removed connection count.
270+
239271
Brush workflow
240272
- Draw an Add brush and confirm resize handles work.
241273
- Draw a Subtract brush and apply cuts.

HammerForge_SPEC.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# HammerForge Spec
22

3-
Last updated: March 23, 2026
3+
Last updated: March 26, 2026
44

55
This document describes HammerForge's architecture and data flow.
66

@@ -33,6 +33,9 @@ All signals are defined on `LevelRoot`. Subsystems emit them via `root.<signal>.
3333
| `state_saved()` | `.hflevel` save completed |
3434
| `state_loaded()` | `.hflevel` load completed |
3535
| `autosave_failed(error_message)` | Threaded autosave write failed |
36+
| `user_message(text, level)` | Subsystem-to-dock notification routing (0=INFO, 1=WARNING, 2=ERROR) |
37+
| `material_list_changed()` | Material palette updated (add/remove) |
38+
| `face_selection_changed()` | Face selection changed (snapshot comparison) |
3639

3740
### Core Scripts
3841

@@ -56,14 +59,16 @@ All signals are defined on `LevelRoot`. Subsystems emit them via `root.<signal>.
5659
| `uv_editor.gd` + `uv_editor.tscn` | UV editing dock control |
5760
| `hf_keymap.gd` | Customizable keyboard shortcuts (JSON load/save, action → binding mapping) |
5861
| `hf_user_prefs.gd` | Cross-session user preferences (`user://hammerforge_prefs.json`) |
62+
| `hf_snap_system.gd` | Centralized snap system (Grid/Vertex/Center modes, threshold-based candidate selection) |
63+
| `hf_op_result.gd` | Lightweight operation result (`ok`, `message`, `fix_hint`) returned by brush operations |
5964

6065
### Subsystems (`addons/hammerforge/systems/`)
6166

6267
| Subsystem | class_name | Responsibility |
6368
|-----------|------------|----------------|
6469
| `hf_grid_system.gd` | `HFGridSystem` | Editor grid setup, visibility, transform, axis-plane intersection |
65-
| `hf_entity_system.gd` | `HFEntitySystem` | Entity definitions, placement, capture/restore, Entity I/O connections |
66-
| `hf_brush_system.gd` | `HFBrushSystem` | Brush CRUD, picking, pending/committed cuts, materials, face selection, hollow, clip, tie/untie. O(1) brush ID cache and material instance cache |
70+
| `hf_entity_system.gd` | `HFEntitySystem` | Entity definitions, placement, capture/restore, Entity I/O connections, dangling connection cleanup |
71+
| `hf_brush_system.gd` | `HFBrushSystem` | Brush CRUD, picking, pending/committed cuts, materials, face selection, hollow, clip, tie/untie. O(1) brush ID cache. Returns `HFOpResult` on failable ops. Auto-cleans references on deletion |
6772
| `hf_drag_system.gd` | `HFDragSystem` | Drag lifecycle, preview management, axis locking, height computation. Owns `HFInputState` |
6873
| `hf_bake_system.gd` | `HFBakeSystem` | Bake orchestration (single/chunked), CSG assembly, navmesh, collision |
6974
| `hf_paint_system.gd` | `HFPaintSystem` | Floor paint input, surface paint, paint layer CRUD, face selection |

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ HammerForge brings classic brush workflows (Hammer / TrenchBroom style) into God
3737
- **Add / Subtract operations** with pending cut staging
3838
- **Extrude Up / Down** -- click a face and drag to extend brushes vertically
3939
- **Shape palette** -- box, cylinder, sphere, cone, wedge, pyramid, prisms, ellipsoid, capsule, torus, and platonic solids
40-
- **Grid snapping** with quick presets and axis locks
40+
- **Geometry-aware snapping** -- Grid, Vertex (brush corners), and Center snap modes with G/V/C toggles
41+
- **Live dimensions** -- real-time W x H x D display during drag and height adjustment
4142
- **Resize gizmo** with full undo/redo support
4243

4344
### Face Materials + UVs
@@ -69,8 +70,8 @@ HammerForge brings classic brush workflows (Hammer / TrenchBroom style) into God
6970
- **Foliage scatter:** height/slope-filtered MultiMeshInstance3D placement
7071

7172
### Structural Tools (Hammer-Inspired)
72-
- **Hollow** (Ctrl+H) -- convert solid brushes to hollow rooms with configurable wall thickness
73-
- **Clip** (Shift+X) -- split brushes along an axis-aligned plane into two pieces
73+
- **Hollow** (Ctrl+H) -- convert solid brushes to hollow rooms with configurable wall thickness (actionable error toast on invalid thickness)
74+
- **Clip** (Shift+X) -- split brushes along an axis-aligned plane into two pieces (actionable error toast on out-of-bounds split)
7475
- **Brush Entity Classes** -- Tie/Untie brushes as func_detail, func_wall, trigger volumes (color-coded viewport overlays)
7576
- **Entity I/O** -- Source-style input/output connections (output → target.input with parameter, delay, fire-once)
7677
- **Move to Floor/Ceiling** (Ctrl+Shift+F/C) -- snap brushes to nearest surface
@@ -80,6 +81,7 @@ HammerForge brings classic brush workflows (Hammer / TrenchBroom style) into God
8081
### Organization + Workflow
8182
- **Visgroups** -- named visibility groups (e.g. "walls", "detail", "lighting") with per-group show/hide toggle
8283
- **Brush/Entity Grouping** -- persistent groups that select/move together (Ctrl+G / Ctrl+U)
84+
- **Reference cleanup on deletion** -- deleting brushes auto-cleans group/visgroup membership and warns about dangling entity I/O connections
8385
- **Texture Lock** -- UV alignment preserved automatically when moving or resizing brushes
8486
- **Cordon (Partial Bake)** -- restrict bake to an AABB region with yellow wireframe visualization
8587
- **Sticky LevelRoot** -- selecting other scene nodes no longer breaks viewport input
@@ -128,6 +130,8 @@ HammerForge brings classic brush workflows (Hammer / TrenchBroom style) into God
128130

129131
### Modular Architecture
130132
- `LevelRoot` is a thin coordinator delegating to **10 subsystem classes** (grid, entity, brush, drag, bake, paint, state, file, validation, visgroup)
133+
- **Operation result reporting** -- `HFOpResult` return values with actionable fix hints on brush operations (hollow, clip, delete)
134+
- **Centralized snap system** -- `HFSnapSystem` with Grid/Vertex/Center modes, threshold-based candidate selection
131135
- **Central signal registry** -- 14 signals on LevelRoot for event-driven UI updates
132136
- **Batched signal emission** -- multi-brush operations coalesce signals to prevent UI thrash
133137
- **Tag-based invalidation** -- dirty tags on brushes/paint/chunks for selective reconciliation
@@ -144,7 +148,7 @@ HammerForge brings classic brush workflows (Hammer / TrenchBroom style) into God
144148
- Explicit **input state machine** for drag/paint operations
145149
- Type-safe inter-module calls (no duck-typing)
146150
- Threaded .hflevel I/O with error handling
147-
- **CI**: automated `gdformat` + `gdlint` checks and **GUT unit tests** (371 tests) on push/PR
151+
- **CI**: automated `gdformat` + `gdlint` checks and **GUT unit tests** (413 tests) on push/PR
148152

149153
## Installation
150154

@@ -214,5 +218,5 @@ Start-Process -FilePath "C:\Godot\Godot_v4.6-stable_win64.exe" `
214218

215219
<p align="center">
216220
<strong>MIT License</strong><br>
217-
<sub>Last updated: March 24, 2026</sub>
221+
<sub>Last updated: March 26, 2026</sub>
218222
</p>

ROADMAP.md

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

3-
Last updated: March 24, 2026
3+
Last updated: March 26, 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.
@@ -92,6 +92,13 @@ Priorities are informed by a Hammer Editor gap analysis — see GAP_ANALYSIS.md
9292
- HTML preview page (`docs/prototype_textures_preview.html`) for browsing all textures.
9393
- GUT tests (27 cases) and dedicated documentation.
9494

95+
## Done (FreeCAD-Inspired Improvements)
96+
- Operation result reporting: `HFOpResult` return values with actionable fix hints on hollow/clip/delete. Failures auto-toast via `user_message`.
97+
- Geometry-aware snap system: `HFSnapSystem` with Grid/Vertex/Center modes. Closest geometry candidate within threshold beats grid snap. G/V/C toggle buttons in dock.
98+
- Live dimensions during drag: mode indicator banner shows real-time W x H x D during DRAG_BASE and DRAG_HEIGHT.
99+
- Reference cleanup on deletion: deleting brushes auto-strips group/visgroup membership and cleans dangling entity I/O connections with toast notification.
100+
- 44 new tests (op_result, snap_system, drag_dimensions, reference_cleanup). Total: 413 tests across 27 files.
101+
95102
## Next (Wave 2b remaining + Wave 2c)
96103
- Vertex editing (move individual brush vertices).
97104
- Entity connection visualization (colored lines between connected entities in viewport).
@@ -103,14 +110,16 @@ Priorities are informed by a Hammer Editor gap analysis — see GAP_ANALYSIS.md
103110
- Path tool (click-to-place path_corner/path_track chains for NPC routes, cameras).
104111
- Displacement sewing (stitch adjacent heightmap edges to share vertices).
105112
- Material atlasing for large scenes.
106-
- Duplicator / instanced geometry (source brush group + transform rules → N synchronized copies — inspired by QuArK's duplicator system).
113+
- Measurement tools (ruler, dimension display on selected brushes, distance between brushes).
107114

108115
## Future (Wave 3 -- Polish)
109116
- Polygon tool (draw arbitrary convex shapes by clicking vertices, extrude to brush).
110117
- Merge tool (combine two adjacent brushes into one convex brush).
111118
- Multiple simultaneous cordons.
112119
- Multi-tool presets for common workflows.
113120
- Additional bake pipelines (merge strategies, export helpers).
121+
- Snap-to-edge and snap-to-perpendicular modes for the snap system.
122+
- Preference packs (e.g. "Speedrunner", "Precision") for one-click workflow presets.
114123
- Formalized plugin API (`HFEditorPlugin` base class for custom tool scripts with menu/toolbar hooks).
115124
- Per-project entity definition files (game pak separation — different entity sets per project).
116125
- Bezier patch editing (control-point-grid surfaces as first-class brush type).

0 commit comments

Comments
 (0)