From 41f3cd5af16dd5a01e55fc30718b947aa3f8d055 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:52:18 +0000 Subject: [PATCH 1/3] Initial plan From 6b502922eba7a4ed2d8657a4814e9b6e58f8aa53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:58:40 +0000 Subject: [PATCH 2/3] Add physics colliders to buildings and roadside props (reapply PR #63) Co-authored-by: adam133 <20442729+adam133@users.noreply.github.com> Agent-Logs-Url: https://github.com/adam133/vectorroad/sessions/842df42c-2f5c-44e3-b977-2d3c18a53034 --- Assets/Scripts/Core/MapSceneBuilder.cs | 44 ++++ .../Tests/PlayMode/CollisionPlayModeTests.cs | 204 ++++++++++++++++++ .../PlayMode/CollisionPlayModeTests.cs.meta | 2 + 3 files changed, 250 insertions(+) create mode 100644 Assets/Tests/PlayMode/CollisionPlayModeTests.cs create mode 100644 Assets/Tests/PlayMode/CollisionPlayModeTests.cs.meta diff --git a/Assets/Scripts/Core/MapSceneBuilder.cs b/Assets/Scripts/Core/MapSceneBuilder.cs index 6be1525..53ce7e5 100644 --- a/Assets/Scripts/Core/MapSceneBuilder.cs +++ b/Assets/Scripts/Core/MapSceneBuilder.cs @@ -298,6 +298,48 @@ private void BuildRoad( var ditchRenderer = ditchGo.AddComponent(); Registry?.ApplyTo(ditchRenderer, result.DitchTextureId); } + + var props = RoadsidePropPlacer.Place(finalSpline, roadType, region: region, wayId: road.WayId); + foreach (PropPlacement prop in props) + SpawnPropCollider(prop, parent.transform); + } + + private static void SpawnPropCollider(PropPlacement prop, Transform parent) + { + var go = new GameObject($"Prop_{prop.Type}"); + go.transform.SetParent(parent, false); + go.transform.position = prop.Position; + go.transform.forward = prop.Forward; + + switch (prop.Type) + { + case PropType.LampPost: + case PropType.SignPost: + { + var col = go.AddComponent(); + col.radius = 0.1f; + col.height = 4f; + col.center = new Vector3(0f, 2f, 0f); + break; + } + + case PropType.Tree: + { + var col = go.AddComponent(); + col.radius = 0.3f; + col.height = 4f; + col.center = new Vector3(0f, 2f, 0f); + break; + } + + case PropType.Fence: + { + var col = go.AddComponent(); + col.size = new Vector3(2f, 1.5f, 0.1f); + col.center = new Vector3(0f, 0.75f, 0f); + break; + } + } } private static Vector3[] ClampRoadSplineToTerrain( @@ -440,12 +482,14 @@ private void BuildBuilding(BuildingFootprint building, RegionType region) wallGo.AddComponent().sharedMesh = result.WallMesh; var wallRenderer = wallGo.AddComponent(); Registry?.ApplyTo(wallRenderer, result.WallTextureId); + wallGo.AddComponent().sharedMesh = result.WallMesh; var roofGo = new GameObject("Roof"); roofGo.transform.SetParent(parent.transform, false); roofGo.AddComponent().sharedMesh = result.RoofMesh; var roofRenderer = roofGo.AddComponent(); Registry?.ApplyTo(roofRenderer, result.RoofTextureId); + roofGo.AddComponent().sharedMesh = result.RoofMesh; } private void BuildWater(WaterBody water, RegionType region) diff --git a/Assets/Tests/PlayMode/CollisionPlayModeTests.cs b/Assets/Tests/PlayMode/CollisionPlayModeTests.cs new file mode 100644 index 0000000..d328789 --- /dev/null +++ b/Assets/Tests/PlayMode/CollisionPlayModeTests.cs @@ -0,0 +1,204 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using VectorRoad.DataInversion; +using VectorRoad.Procedural; + +namespace VectorRoad.Tests.PlayMode +{ + /// + /// Play-mode tests verifying that buildings and roadside props receive the correct + /// physics collider components so the car cannot pass through them. + /// + public class CollisionPlayModeTests + { + // GameObjects created during each test – destroyed in TearDown. + private readonly List _created = new(); + + [UnityTearDown] + public IEnumerator TearDown() + { + foreach (GameObject go in _created) + { + if (go != null) + Object.Destroy(go); + } + _created.Clear(); + yield return null; + } + + // Helper to create and track a temporary GameObject. + private GameObject MakeGO(string name = "TestGO") + { + var go = new GameObject(name); + _created.Add(go); + return go; + } + + // ── Building wall collider ───────────────────────────────────────────── + + [UnityTest] + public IEnumerator BuildingWall_WithMeshCollider_BlocksRigidbody() + { + // Build a minimal square building mesh using BuildingGenerator. + var footprint = new[] + { + new Vector3( 0f, 0f, 0f), + new Vector3(10f, 0f, 0f), + new Vector3(10f, 0f, 10f), + new Vector3( 0f, 0f, 10f), + }; + + BuildingMeshResult result = BuildingGenerator.Extrude(footprint, wayId: 1); + + // Replicate what MapSceneBuilder.BuildBuilding does. + var wallGo = MakeGO("Walls"); + wallGo.AddComponent().sharedMesh = result.WallMesh; + wallGo.AddComponent(); + var col = wallGo.AddComponent(); + col.sharedMesh = result.WallMesh; + + yield return null; + + Assert.That(wallGo.GetComponent(), Is.Not.Null, + "Building wall must have a MeshCollider."); + Assert.That(wallGo.GetComponent().sharedMesh, Is.Not.Null, + "Building wall MeshCollider must reference the wall mesh."); + } + + [UnityTest] + public IEnumerator BuildingRoof_WithMeshCollider_HasCollider() + { + var footprint = new[] + { + new Vector3( 0f, 0f, 0f), + new Vector3(10f, 0f, 0f), + new Vector3(10f, 0f, 10f), + new Vector3( 0f, 0f, 10f), + }; + + BuildingMeshResult result = BuildingGenerator.Extrude(footprint, wayId: 2); + + var roofGo = MakeGO("Roof"); + roofGo.AddComponent().sharedMesh = result.RoofMesh; + roofGo.AddComponent(); + var col = roofGo.AddComponent(); + col.sharedMesh = result.RoofMesh; + + yield return null; + + Assert.That(roofGo.GetComponent(), Is.Not.Null, + "Building roof must have a MeshCollider."); + Assert.That(roofGo.GetComponent().sharedMesh, Is.Not.Null, + "Building roof MeshCollider must reference the roof mesh."); + } + + // ── Prop collider shapes ────────────────────────────────────────────── + + [UnityTest] + public IEnumerator LampPost_Collider_IsCapsuleWithCorrectDimensions() + { + var go = MakeGO("Prop_LampPost"); + var col = go.AddComponent(); + col.radius = 0.1f; + col.height = 4f; + col.center = new Vector3(0f, 2f, 0f); + + yield return null; + + var capsule = go.GetComponent(); + Assert.That(capsule, Is.Not.Null, "LampPost must have a CapsuleCollider."); + Assert.That(capsule.radius, Is.EqualTo(0.1f).Within(1e-5f)); + Assert.That(capsule.height, Is.EqualTo(4f).Within(1e-5f)); + Assert.That(capsule.center.y, Is.EqualTo(2f).Within(1e-5f)); + } + + [UnityTest] + public IEnumerator SignPost_Collider_IsCapsuleWithCorrectDimensions() + { + var go = MakeGO("Prop_SignPost"); + var col = go.AddComponent(); + col.radius = 0.1f; + col.height = 4f; + col.center = new Vector3(0f, 2f, 0f); + + yield return null; + + var capsule = go.GetComponent(); + Assert.That(capsule, Is.Not.Null, "SignPost must have a CapsuleCollider."); + Assert.That(capsule.radius, Is.EqualTo(0.1f).Within(1e-5f)); + Assert.That(capsule.height, Is.EqualTo(4f).Within(1e-5f)); + Assert.That(capsule.center.y, Is.EqualTo(2f).Within(1e-5f)); + } + + [UnityTest] + public IEnumerator Tree_Collider_IsCapsuleWithWiderRadius() + { + var go = MakeGO("Prop_Tree"); + var col = go.AddComponent(); + col.radius = 0.3f; + col.height = 4f; + col.center = new Vector3(0f, 2f, 0f); + + yield return null; + + var capsule = go.GetComponent(); + Assert.That(capsule, Is.Not.Null, "Tree must have a CapsuleCollider."); + Assert.That(capsule.radius, Is.EqualTo(0.3f).Within(1e-5f), + "Tree trunk radius should be wider than a lamp post."); + Assert.That(capsule.radius, Is.GreaterThan(0.1f), + "Tree radius must be larger than a post radius."); + } + + [UnityTest] + public IEnumerator Fence_Collider_IsBoxWithCorrectDimensions() + { + var go = MakeGO("Prop_Fence"); + var col = go.AddComponent(); + col.size = new Vector3(2f, 1.5f, 0.1f); + col.center = new Vector3(0f, 0.75f, 0f); + + yield return null; + + var box = go.GetComponent(); + Assert.That(box, Is.Not.Null, "Fence must have a BoxCollider."); + Assert.That(box.size.x, Is.EqualTo(2f).Within(1e-5f), + "Fence span (X) should be 2 m."); + Assert.That(box.size.y, Is.EqualTo(1.5f).Within(1e-5f), + "Fence height (Y) should be 1.5 m."); + Assert.That(box.center.y, Is.EqualTo(0.75f).Within(1e-5f), + "Fence centre must sit above the ground plane."); + } + + // ── Prop placement positions are off-road ───────────────────────────── + + [UnityTest] + public IEnumerator RoadsidePropPlacer_LampPostsArePlacedBeyondRoadEdge() + { + var spline = new List + { + new(0f, 0f, 0f), + new(0f, 0f, 100f), + }; + + var placements = RoadsidePropPlacer.Place( + spline, RoadType.Residential, RegionType.Temperate, wayId: 42); + + float halfWidth = RoadMeshExtruder.GetWidthForRoadType(RoadType.Residential) * 0.5f; + float minLateral = halfWidth + RoadMeshExtruder.DefaultKerbWidth; + + Assert.That(placements, Is.Not.Empty, "Should have at least one prop placement."); + + foreach (PropPlacement p in placements) + { + float lateralDist = Mathf.Abs(p.Position.x); + Assert.That(lateralDist, Is.GreaterThan(minLateral), + $"Prop at {p.Position} must be outside the road edge ({minLateral} m)."); + } + + yield return null; + } + } +} diff --git a/Assets/Tests/PlayMode/CollisionPlayModeTests.cs.meta b/Assets/Tests/PlayMode/CollisionPlayModeTests.cs.meta new file mode 100644 index 0000000..6a109d5 --- /dev/null +++ b/Assets/Tests/PlayMode/CollisionPlayModeTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4a4cc5dfa99743198d083e322566d02b From 6520276befb87e447aa28512456073e525ab046f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:11:47 +0000 Subject: [PATCH 3/3] Add grey visual meshes to roadside prop GameObjects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each prop type now has a child Mesh GameObject alongside its collider: - LampPost / SignPost: Capsule mesh scaled (0.2, 2, 0.2) → grey material - Tree: Capsule mesh scaled (0.6, 2, 0.6) → olive green material - Fence: Cube mesh scaled (2, 1.5, 0.1) → weathered-wood brown material PlaceholderMaterialFactory gains four new IDs (prop_lamppost, prop_signpost, prop_tree, prop_fence) with category-appropriate colours. SpawnPropCollider is made non-static so it can access Registry. Co-authored-by: adam133 <20442729+adam133@users.noreply.github.com> Agent-Logs-Url: https://github.com/adam133/vectorroad/sessions/43f6e8ed-5435-42e0-aa54-687c15822478 --- Assets/Scripts/Core/MapSceneBuilder.cs | 31 +++++++++++++++- .../Procedural/PlaceholderMaterialFactory.cs | 15 ++++++++ .../PlaceholderMaterialFactoryTests.cs | 35 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/Core/MapSceneBuilder.cs b/Assets/Scripts/Core/MapSceneBuilder.cs index 53ce7e5..891fe66 100644 --- a/Assets/Scripts/Core/MapSceneBuilder.cs +++ b/Assets/Scripts/Core/MapSceneBuilder.cs @@ -304,7 +304,7 @@ private void BuildRoad( SpawnPropCollider(prop, parent.transform); } - private static void SpawnPropCollider(PropPlacement prop, Transform parent) + private void SpawnPropCollider(PropPlacement prop, Transform parent) { var go = new GameObject($"Prop_{prop.Type}"); go.transform.SetParent(parent, false); @@ -320,6 +320,9 @@ private static void SpawnPropCollider(PropPlacement prop, Transform parent) col.radius = 0.1f; col.height = 4f; col.center = new Vector3(0f, 2f, 0f); + + string textureId = prop.Type == PropType.LampPost ? "prop_lamppost" : "prop_signpost"; + AddCapsuleVisual(go, scale: new Vector3(0.2f, 2f, 0.2f), centerY: 2f, textureId: textureId); break; } @@ -329,6 +332,8 @@ private static void SpawnPropCollider(PropPlacement prop, Transform parent) col.radius = 0.3f; col.height = 4f; col.center = new Vector3(0f, 2f, 0f); + + AddCapsuleVisual(go, scale: new Vector3(0.6f, 2f, 0.6f), centerY: 2f, textureId: "prop_tree"); break; } @@ -337,11 +342,35 @@ private static void SpawnPropCollider(PropPlacement prop, Transform parent) var col = go.AddComponent(); col.size = new Vector3(2f, 1.5f, 0.1f); col.center = new Vector3(0f, 0.75f, 0f); + + AddBoxVisual(go, scale: new Vector3(2f, 1.5f, 0.1f), centerY: 0.75f, textureId: "prop_fence"); break; } } } + private void AddCapsuleVisual(GameObject parent, Vector3 scale, float centerY, string textureId) + { + var visual = new GameObject("Mesh"); + visual.transform.SetParent(parent.transform, false); + visual.transform.localPosition = new Vector3(0f, centerY, 0f); + visual.transform.localScale = scale; + visual.AddComponent().sharedMesh = Resources.GetBuiltinResource("Capsule.fbx"); + var mr = visual.AddComponent(); + Registry?.ApplyTo(mr, textureId); + } + + private void AddBoxVisual(GameObject parent, Vector3 scale, float centerY, string textureId) + { + var visual = new GameObject("Mesh"); + visual.transform.SetParent(parent.transform, false); + visual.transform.localPosition = new Vector3(0f, centerY, 0f); + visual.transform.localScale = scale; + visual.AddComponent().sharedMesh = Resources.GetBuiltinResource("Cube.fbx"); + var mr = visual.AddComponent(); + Registry?.ApplyTo(mr, textureId); + } + private static Vector3[] ClampRoadSplineToTerrain( System.Collections.Generic.IList spline, TerrainMeshResult terrainMesh, diff --git a/Assets/Scripts/Procedural/PlaceholderMaterialFactory.cs b/Assets/Scripts/Procedural/PlaceholderMaterialFactory.cs index 20a2853..54fd4cb 100644 --- a/Assets/Scripts/Procedural/PlaceholderMaterialFactory.cs +++ b/Assets/Scripts/Procedural/PlaceholderMaterialFactory.cs @@ -70,6 +70,12 @@ internal static class PlaceholderMaterialFactory // Lane markings "lane_marking_oneway", "lane_marking_twoway", + + // Roadside props + "prop_lamppost", + "prop_signpost", + "prop_tree", + "prop_fence", }; // ── Public API ───────────────────────────────────────────────────────── @@ -157,6 +163,15 @@ private static Color GetPlaceholderColor(string id) if (id.StartsWith("lane_marking")) return Color.white; + if (id == "prop_lamppost" || id == "prop_signpost") + return new Color(0.60f, 0.60f, 0.60f); // mid grey metal post + + if (id == "prop_tree") + return new Color(0.30f, 0.50f, 0.20f); // muted olive green + + if (id == "prop_fence") + return new Color(0.65f, 0.55f, 0.45f); // weathered wood + return new Color(0.50f, 0.50f, 0.50f); // neutral fallback } } diff --git a/Tests/VectorRoad.Tests/PlaceholderMaterialFactoryTests.cs b/Tests/VectorRoad.Tests/PlaceholderMaterialFactoryTests.cs index e5aa8de..4ef4ce5 100644 --- a/Tests/VectorRoad.Tests/PlaceholderMaterialFactoryTests.cs +++ b/Tests/VectorRoad.Tests/PlaceholderMaterialFactoryTests.cs @@ -43,6 +43,10 @@ public void Create_SetsNameToTextureId() [TestCase("water_tropical")] [TestCase("lane_marking_oneway")] [TestCase("lane_marking_twoway")] + [TestCase("prop_lamppost")] + [TestCase("prop_signpost")] + [TestCase("prop_tree")] + [TestCase("prop_fence")] public void Create_AllKnownIds_ReturnMaterialWithDistinctColor(string textureId) { // Magenta (r=1, g=0, b=1) is Unity's "missing material" colour. @@ -108,6 +112,36 @@ public void Create_UnknownId_ReturnsNeutralGreyMaterial() Assert.That(mat.color.b, Is.EqualTo(0.5f).Within(0.001f)); } + [Test] + public void Create_PropPostIds_HaveMidGreyColor() + { + foreach (var id in new[] { "prop_lamppost", "prop_signpost" }) + { + var mat = PlaceholderMaterialFactory.Create(id); + Assert.That(mat.color.r, Is.EqualTo(0.60f).Within(0.001f), $"{id}: red channel"); + Assert.That(mat.color.g, Is.EqualTo(0.60f).Within(0.001f), $"{id}: green channel"); + Assert.That(mat.color.b, Is.EqualTo(0.60f).Within(0.001f), $"{id}: blue channel"); + } + } + + [Test] + public void Create_PropTree_HasGreenDominance() + { + var mat = PlaceholderMaterialFactory.Create("prop_tree"); + Assert.That(mat.color.g, Is.GreaterThan(mat.color.r), "prop_tree: green > red"); + Assert.That(mat.color.g, Is.GreaterThan(mat.color.b), "prop_tree: green > blue"); + } + + [Test] + public void Create_PropFence_HasWarmBrownTone() + { + var mat = PlaceholderMaterialFactory.Create("prop_fence"); + // Weathered wood: red > blue, both > 0.4 + Assert.That(mat.color.r, Is.GreaterThan(mat.color.b), "prop_fence: red > blue"); + Assert.That(mat.color.r, Is.GreaterThan(0.4f), "prop_fence: visible red component"); + Assert.That(mat.color.b, Is.GreaterThan(0.4f), "prop_fence: visible blue component"); + } + // ── FillMissing ─────────────────────────────────────────────────────── [Test] @@ -132,6 +166,7 @@ public void FillMissing_PopulatesAllKnownTextureIds() "terrain_grass", "water", "water_arctic", "water_tropical", "lane_marking_oneway", "lane_marking_twoway", + "prop_lamppost", "prop_signpost", "prop_tree", "prop_fence", }) { Assert.That(registry.GetMaterial(id), Is.Not.Null,