feat(bf-18yn): seed realistic node geometry in spaxel-sim default placement
- Modify AddNode handler to assign spread-out positions when nodes are created without explicit positions (at origin), preventing co-location that causes Fresnel excess path |P-T|+|P-R|-|T-R| to collapse toward 0 - Use DefaultNodePositions infrastructure from simulator package to distribute nodes across the room in a grid pattern - Preserve explicitly-set positions, only spread default-origin nodes - Add tests verifying spread behavior and explicit position preservation This fixes the core symptom in bf-4q5w where co-located nodes prevented blob formation in the fusion accumulation grid. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4fe25fcfaf
commit
669d3793aa
2 changed files with 149 additions and 0 deletions
|
|
@ -209,6 +209,19 @@ func (h *SimulatorHandler) AddNode(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
h.mu.Lock()
|
||||
// If node has no explicit position (co-located at origin), assign a spread
|
||||
// position so nodes don't collapse to the same point, which would cause
|
||||
// Fresnel excess path |P-T|+|P-R|-|T-R| to approach 0 and prevent blob
|
||||
// formation (bf-18yn, bf-4q5w). Use the existing spread-position
|
||||
// infrastructure that mirrors cmd/sim's generateNodePositions.
|
||||
if node.Position.X == 0 && node.Position.Y == 0 && node.Position.Z == 0 {
|
||||
// Get current node count including the one we're about to add
|
||||
currentNodes := h.nodes.All()
|
||||
positions := simulator.DefaultNodePositions(h.space, len(currentNodes)+1)
|
||||
if len(positions) > 0 {
|
||||
node.Position = positions[len(currentNodes)]
|
||||
}
|
||||
}
|
||||
h.nodes.Add(&node)
|
||||
h.mu.Unlock()
|
||||
|
||||
|
|
|
|||
136
mothership/internal/api/simulator_test.go
Normal file
136
mothership/internal/api/simulator_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// Package api tests the simulator REST API
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/spaxel/mothership/internal/simulator"
|
||||
)
|
||||
|
||||
// TestAddNode_SpreadsDefaultOrigin tests that nodes created without explicit
|
||||
// positions (at origin {0,0,0}) are assigned spread-out positions instead of
|
||||
// remaining co-located, which would collapse Fresnel excess path length toward
|
||||
// 0 and prevent blob formation (bf-18yn, bf-4q5w).
|
||||
func TestAddNode_SpreadsDefaultOrigin(t *testing.T) {
|
||||
handler := NewSimulatorHandler()
|
||||
space := simulator.DefaultSpace()
|
||||
handler.mu.Lock()
|
||||
handler.space = space
|
||||
handler.mu.Unlock()
|
||||
|
||||
// Create multiple nodes without positions (all at origin)
|
||||
var createdNodes []simulator.Node
|
||||
for i := 0; i < 4; i++ {
|
||||
nodeJSON := map[string]interface{}{
|
||||
"id": string(rune('a' + i)),
|
||||
"name": "Node Name",
|
||||
}
|
||||
body, _ := json.Marshal(nodeJSON)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/simulator/nodes", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AddNode(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var node simulator.Node
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &node); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
createdNodes = append(createdNodes, node)
|
||||
}
|
||||
|
||||
// Verify all nodes have distinct positions (not co-located at origin)
|
||||
positions := make(map[[3]float64]int)
|
||||
for _, node := range createdNodes {
|
||||
pos := [3]float64{node.Position.X, node.Position.Y, node.Position.Z}
|
||||
|
||||
// Check not at origin
|
||||
if node.Position.X == 0 && node.Position.Y == 0 && node.Position.Z == 0 {
|
||||
t.Errorf("node %s still at origin (0,0,0)", node.ID)
|
||||
}
|
||||
|
||||
// Check distinct
|
||||
positions[pos]++
|
||||
if positions[pos] > 1 {
|
||||
t.Errorf("duplicate position %v for node %s", pos, node.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify positions span the room (not all at same point)
|
||||
_, _, _, _, _, _ = space.Bounds()
|
||||
|
||||
var minXSeen, maxXSeen, minYSeen, maxYSeen float64
|
||||
for i, node := range createdNodes {
|
||||
if i == 0 {
|
||||
minXSeen, maxXSeen = node.Position.X, node.Position.X
|
||||
minYSeen, maxYSeen = node.Position.Y, node.Position.Y
|
||||
}
|
||||
if node.Position.X < minXSeen {
|
||||
minXSeen = node.Position.X
|
||||
}
|
||||
if node.Position.X > maxXSeen {
|
||||
maxXSeen = node.Position.X
|
||||
}
|
||||
if node.Position.Y < minYSeen {
|
||||
minYSeen = node.Position.Y
|
||||
}
|
||||
if node.Position.Y > maxYSeen {
|
||||
maxYSeen = node.Position.Y
|
||||
}
|
||||
}
|
||||
|
||||
// For 4 nodes, we should see some spread across the room
|
||||
// (not strictly required for correctness, but verifies the fix works)
|
||||
if minXSeen == maxXSeen && minYSeen == maxYSeen {
|
||||
t.Errorf("nodes don't span room: all at (%.2f, %.2f)", minXSeen, minYSeen)
|
||||
}
|
||||
|
||||
t.Logf("Successfully spread %d nodes across room: X=[%.2f,%.2f] Y=[%.2f,%.2f]",
|
||||
len(createdNodes), minXSeen, maxXSeen, minYSeen, maxYSeen)
|
||||
}
|
||||
|
||||
// TestAddNode_KeepsExplicitPosition tests that nodes with explicit positions
|
||||
// are not modified.
|
||||
func TestAddNode_KeepsExplicitPosition(t *testing.T) {
|
||||
handler := NewSimulatorHandler()
|
||||
|
||||
nodeJSON := map[string]interface{}{
|
||||
"id": "test-node",
|
||||
"position": map[string]interface{}{
|
||||
"x": 1.5,
|
||||
"y": 2.3,
|
||||
"z": 1.0,
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(nodeJSON)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/simulator/nodes", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AddNode(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var node simulator.Node
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &node); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
// Verify explicit position was preserved
|
||||
if node.Position.X != 1.5 || node.Position.Y != 2.3 || node.Position.Z != 1.0 {
|
||||
t.Errorf("explicit position not preserved: got (%.2f, %.2f, %.2f)",
|
||||
node.Position.X, node.Position.Y, node.Position.Z)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue