refactor(M03): extract M2 placement grouper

This commit is contained in:
2026-07-17 00:05:49 +04:00
parent 9c9020e60f
commit 9df32d38ef
12 changed files with 633 additions and 59 deletions
+296
View File
@@ -0,0 +1,296 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for ADT M2 grouping.
const GROUPER_SCRIPT := preload("res://src/render/m2/m2_placement_grouper.gd")
const TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd"
)
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_empty_and_invalid_inputs(failures)
_verify_historical_path_normalization(failures)
_verify_group_order(failures)
_verify_tile_local_defaults(failures)
_verify_regular_transform(failures)
_verify_waterfall_transform(failures)
_verify_scale_clamp(failures)
_verify_fresh_output(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_PLACEMENT_GROUPER: %s" % failure)
quit(1)
return
print(
"M2_PLACEMENT_GROUPER PASS cases=9 iterations=100 placements=256 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_empty_and_invalid_inputs(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
_expect_equal(
grouper.group_placements(Vector3.ZERO, PackedStringArray(), []).size(),
0,
"empty",
failures
)
var placements: Array = [
"invalid",
{"name_id": -1},
{"name_id": 2},
{"name_id": 0},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray([""]),
placements
)
_expect_equal(groups.size(), 0, "invalid placements", failures)
func _verify_historical_path_normalization(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray([
"World\\Generic\\Tree.mdx",
"World/Generic/Rock.mdl",
"World/Generic/Upper.MDX",
])
var placements: Array = [{"name_id": 0}, {"name_id": 1}, {"name_id": 2}]
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_true(groups.has("World/Generic/Tree.m2"), "backslash and mdx normalization", failures)
_expect_true(groups.has("World/Generic/Rock.m2"), "mdl normalization", failures)
_expect_true(groups.has("World/Generic/Upper.MDX"), "uppercase suffix preservation", failures)
func _verify_group_order(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var placements: Array = [
{"name_id": 0, "pos": Vector3(3.0, 0.0, 0.0)},
{"name_id": 1, "pos": Vector3(8.0, 0.0, 0.0)},
{"name_id": 0, "pos": Vector3(5.0, 0.0, 0.0)},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2", "Rock.m2"]),
placements
)
_expect_equal(groups.size(), 2, "group count", failures)
var tree_transforms: Array = groups.get("Tree.m2", [])
_expect_equal(tree_transforms.size(), 2, "tree transform count", failures)
if tree_transforms.size() == 2:
_expect_vector_near(tree_transforms[0].origin, Vector3(3.0, 0.0, 0.0), "first tree", failures)
_expect_vector_near(tree_transforms[1].origin, Vector3(5.0, 0.0, 0.0), "second tree", failures)
func _verify_tile_local_defaults(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3(10.0, 20.0, 30.0),
PackedStringArray(["Tree.m2"]),
[{"name_id": 0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_vector_near(transform.origin, Vector3(-10.0, -20.0, -30.0), "default local position", failures)
_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis and scale", failures)
func _verify_regular_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var rotation := Vector3(0.2, -0.4, 0.6)
var groups: Dictionary = grouper.group_placements(
Vector3(1.0, 2.0, 3.0),
PackedStringArray(["Tree.m2"]),
[{
"name_id": 0,
"pos": Vector3(5.0, 8.0, 13.0),
"rot": rotation,
"scale": 2.0,
}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(transform.basis, Basis.from_euler(rotation).scaled(Vector3.ONE * 2.0), "regular basis", failures)
_expect_vector_near(transform.origin, Vector3(4.0, 6.0, 10.0), "regular origin", failures)
func _verify_waterfall_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var resolver := TRANSFORM_RESOLVER_SCRIPT.new()
var path := "World/X/ElwynnTallWaterfall01.m2"
var rotation := Vector3(0.05, -0.2, 0.15)
var scale_value := 1.5
var position := Vector3(20.0, 30.0, 40.0)
var tile_origin := Vector3(2.0, 3.0, 4.0)
var groups: Dictionary = grouper.group_placements(
tile_origin,
PackedStringArray([path]),
[{
"name_id": 0,
"pos": position,
"rot": rotation,
"scale": scale_value,
}]
)
var transform: Transform3D = (groups[path] as Array)[0]
var expected_basis: Basis = resolver.resolve_basis(rotation, path).scaled(
Vector3.ONE * scale_value
)
var expected_origin: Vector3 = (
position
- tile_origin
+ resolver.resolve_origin_offset(rotation, path, scale_value)
)
_expect_basis_near(transform.basis, expected_basis, "waterfall basis", failures)
_expect_vector_near(transform.origin, expected_origin, "waterfall origin", failures)
func _verify_scale_clamp(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2"]),
[{"name_id": 0, "scale": -5.0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(
transform.basis,
Basis.IDENTITY.scaled(Vector3.ONE * 0.0001),
"negative scale clamp",
failures
)
func _verify_fresh_output(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2"])
var placements: Array = [{"name_id": 0}]
var first: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
(first["Tree.m2"] as Array).clear()
var second: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_equal((second["Tree.m2"] as Array).size(), 1, "fresh output", failures)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
var grouping_task_source := _source_between(
loader_source,
"func _group_tile_m2_task(",
"func _drain_m2_group_results()",
failures
)
_expect_equal(
loader_source.count("_m2_placement_grouper.group_placements("),
1,
"one loader grouping adapter",
failures
)
_expect_true(
not grouping_task_source.contains("for placement_variant in m2_placements"),
"loader omits grouping loop",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouper basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouper offset adapter",
failures
)
for forbidden_dependency in ["WorkerThreadPool", "Mutex", "RenderingServer", "ResourceLoader"]:
_expect_true(
not grouper_source.contains(forbidden_dependency),
"grouper omits %s" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2", "Rock.mdx"])
var placements: Array = []
for placement_index in range(256):
placements.append({
"name_id": placement_index % 2,
"pos": Vector3(float(placement_index), 0.0, float(-placement_index)),
"rot": Vector3(0.1, 0.2, 0.3),
"scale": 1.0,
})
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
if groups.size() != 2:
failures.append("timing iteration %d produced wrong group count" % iteration)
break
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 x 256 grouping bounded", failures)
return elapsed_milliseconds
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
func _expect_basis_near(
actual_basis: Basis,
expected_basis: Basis,
label: String,
failures: Array[String]
) -> void:
if not actual_basis.is_equal_approx(expected_basis):
failures.append("%s basis mismatch" % label)
func _expect_vector_near(
actual_value: Vector3,
expected_value: Vector3,
label: String,
failures: Array[String]
) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_equal(
actual_value: int,
expected_value: int,
label: String,
failures: Array[String]
) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://bbx57qy0lp11w
@@ -4,6 +4,7 @@ extends SceneTree
const RESOLVER_SCRIPT := preload("res://src/render/m2/m2_placement_transform_resolver.gd")
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const CLIFFROCK_WORLD_YAW_OFFSET := PI
@@ -130,6 +131,7 @@ func _verify_path_normalization_and_scale_clamp(failures: Array[String]) -> void
func _verify_loader_boundary(failures: Array[String]) -> void:
var source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
_expect_true(
source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"),
"loader composes resolver",
@@ -137,14 +139,26 @@ func _verify_loader_boundary(failures: Array[String]) -> void:
)
_expect_equal(
source.count("_m2_placement_transform_resolver.resolve_basis("),
3,
"three basis adapters",
2,
"two direct loader basis adapters",
failures
)
_expect_equal(
source.count("_m2_placement_transform_resolver.resolve_origin_offset("),
3,
"three offset adapters",
2,
"two direct loader offset adapters",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouped basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouped offset adapter",
failures
)
_expect_true(