VibeGeometry/tools/verify_fieldvalue_translations.py
2026-05-04 16:00:47 +01:00

404 lines
15 KiB
Python

"""Compare Shriinivas fieldvalue scalar helpers against Geometry Script translations."""
from __future__ import annotations
import importlib.util
import json
import math
import sys
from pathlib import Path
import bpy
ROOT = Path(__file__).resolve().parents[1]
TRANSLATION = ROOT / "examples" / "geometry_script" / "shriinivas_fieldvalue.py"
CASES = [
{"Number": 1234.0, "Position": 0},
{"Number": 1234.0, "Position": 1},
{"Number": 98765.0, "Position": 2},
{"Number": 120305.0, "Position": 3},
]
NEXT_DIGIT_CASES = [
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 0},
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 1},
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 2},
{"Whole Part": 98765.0, "Fraction Part": 4321.0, "Max Precision": 3, "Position": 4},
]
DECIMAL_CASE = {"Vertical Segment Size": 2.4, "radius": 0.18}
SEGMENT_CASES = [
{
"X Major": True,
"Segment Length Along X": 2.0,
"Segment Length Along Y": 0.45,
"Secondary Axis Segment Thickness": 0.45,
"Sharpness": 0.75,
"offset": (0.2, -0.1, 0.0),
},
{
"X Major": False,
"Segment Length Along X": 0.4,
"Segment Length Along Y": 1.8,
"Secondary Axis Segment Thickness": 0.4,
"Sharpness": 1.0,
"offset": (-0.3, 0.25, 0.0),
},
]
SEVEN_SEGMENTS_CASE = {
"Horizontal Segment Size": 2.2,
"Horizontal Segment Thickness": 0.42,
"Vertical Segment Size": 1.7,
"Vertical Segment Thickness": 0.36,
"X Separation": 0.13,
"Y Seperation": 0.17,
"Tip Sharpness": 0.8,
}
FIELD_VALUE_CASE = {
"Float Value": 12.34,
"Digits Before Decimal": 3,
"Precision": 2,
"Scale": 0.05,
"Digit Separation": 0.2,
"Element Separation": 1.0,
"X Offset": 0.0,
"Y Offset": 0.0,
"Z Offset": 0.001,
"Rotation": 0,
"Segment Width Factor": 0.5,
"Segment Separation Factor": 0.1,
"Tip Sharpness": 1.0,
"Leading Zeros": False,
}
DELETE_DIGITS = range(0, 13)
DELETE_SEGMENT_POSITIONS = range(0, 15)
def _load_translation() -> None:
spec = importlib.util.spec_from_file_location("vg_fieldvalue_translation", TRANSLATION)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
def _socket_by_name(sockets, name: str):
for socket in sockets:
if socket.name == name:
return socket
raise KeyError(name)
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
if name in bpy.data.node_groups:
bpy.data.node_groups.remove(bpy.data.node_groups[name])
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
group.is_modifier = True
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
return group
def _build_count_wrapper(name: str, digit_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
digit = nodes.new("GeometryNodeGroup")
digit.node_tree = digit_group
for socket in digit.inputs:
if socket.name in inputs:
socket.default_value = inputs[socket.name]
line = nodes.new("GeometryNodeMeshLine")
line.mode = "OFFSET"
line.count_mode = "TOTAL"
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
links.new(_socket_by_name(digit.outputs, "Result"), _socket_by_name(line.inputs, "Count"))
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _build_bool_count_wrapper(name: str, bool_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
bool_node = nodes.new("GeometryNodeGroup")
bool_node.node_tree = bool_group
for socket in bool_node.inputs:
if socket.name in inputs:
socket.default_value = inputs[socket.name]
switch_node = nodes.new("GeometryNodeSwitch")
switch_node.input_type = "INT"
_socket_by_name(switch_node.inputs, "False").default_value = 0
_socket_by_name(switch_node.inputs, "True").default_value = 1
links.new(_socket_by_name(bool_node.outputs, "result"), _socket_by_name(switch_node.inputs, "Switch"))
line = nodes.new("GeometryNodeMeshLine")
line.mode = "OFFSET"
line.count_mode = "TOTAL"
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
links.new(_socket_by_name(switch_node.outputs, "Output"), _socket_by_name(line.inputs, "Count"))
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _build_geometry_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object], output: str):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
group_node = nodes.new("GeometryNodeGroup")
group_node.node_tree = source_group
lower_inputs = {key.lower(): value for key, value in inputs.items()}
for socket in group_node.inputs:
if socket.name in inputs:
socket.default_value = inputs[socket.name]
elif socket.name.lower() in lower_inputs:
socket.default_value = lower_inputs[socket.name.lower()]
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(group_node.outputs, output), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _build_field_value_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
line = nodes.new("GeometryNodeMeshLine")
line.mode = "OFFSET"
line.count_mode = "TOTAL"
_socket_by_name(line.inputs, "Count").default_value = 2
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
group_node = nodes.new("GeometryNodeGroup")
group_node.node_tree = source_group
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry"))
for socket in group_node.inputs:
if socket.name not in inputs:
continue
try:
socket.default_value = inputs[socket.name]
except TypeError:
# Menu sockets without copied enum metadata can still use the
# source graph's default route for this baseline comparison.
pass
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(group_node.outputs, "Value"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _evaluated_vertex_count(group: bpy.types.GeometryNodeTree) -> int:
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
obj = bpy.data.objects.new(group.name + " Object", mesh)
bpy.context.collection.objects.link(obj)
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
modifier.node_group = group
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated = obj.evaluated_get(depsgraph)
evaluated_mesh = evaluated.to_mesh()
try:
return len(evaluated_mesh.vertices)
finally:
evaluated.to_mesh_clear()
def _evaluated_vertices(group: bpy.types.GeometryNodeTree) -> list[tuple[float, float, float]]:
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
obj = bpy.data.objects.new(group.name + " Object", mesh)
bpy.context.collection.objects.link(obj)
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
modifier.node_group = group
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated = obj.evaluated_get(depsgraph)
evaluated_mesh = evaluated.to_mesh()
try:
return [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
finally:
evaluated.to_mesh_clear()
def main() -> int:
_load_translation()
source = bpy.data.node_groups["Digit At"]
translated = bpy.data.node_groups["VG Digit At"]
results = []
source_decimal = _evaluated_vertices(
_build_geometry_wrapper("VG Verify Source Decimal", bpy.data.node_groups["Create Decimal"], DECIMAL_CASE, "Dot")
)
translated_decimal = _evaluated_vertices(
_build_geometry_wrapper("VG Verify Translated Decimal", bpy.data.node_groups["VG Create Decimal"], DECIMAL_CASE, "Dot")
)
results.append(
{
"case": "create_decimal",
"source_vertex_count": len(source_decimal),
"translated_vertex_count": len(translated_decimal),
"max_sorted_vertex_delta": max(
(math.dist(a, b) for a, b in zip(sorted(source_decimal), sorted(translated_decimal))),
default=0.0,
),
}
)
results[-1]["ok"] = (
results[-1]["source_vertex_count"] == results[-1]["translated_vertex_count"]
and results[-1]["max_sorted_vertex_delta"] <= 1e-6
)
for index, inputs in enumerate(SEGMENT_CASES):
source_segment = _evaluated_vertices(
_build_geometry_wrapper(
f"VG Verify Source Segment {index}",
bpy.data.node_groups["Create Segment"],
inputs,
"Segment",
)
)
translated_segment = _evaluated_vertices(
_build_geometry_wrapper(
f"VG Verify Translated Segment {index}",
bpy.data.node_groups["VG Create Segment"],
inputs,
"Segment",
)
)
max_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source_segment), sorted(translated_segment))),
default=0.0,
)
results.append(
{
"case": {"create_segment": index, **inputs},
"source_vertex_count": len(source_segment),
"translated_vertex_count": len(translated_segment),
"max_sorted_vertex_delta": max_delta,
"ok": len(source_segment) == len(translated_segment) and max_delta <= 1e-6,
}
)
source_seven_segments = _evaluated_vertices(
_build_geometry_wrapper(
"VG Verify Source Seven Segments",
bpy.data.node_groups["Seven Segments"],
SEVEN_SEGMENTS_CASE,
"bars",
)
)
translated_seven_segments = _evaluated_vertices(
_build_geometry_wrapper(
"VG Verify Translated Seven Segments",
bpy.data.node_groups["VG Seven Segments"],
SEVEN_SEGMENTS_CASE,
"bars",
)
)
seven_segments_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source_seven_segments), sorted(translated_seven_segments))),
default=0.0,
)
results.append(
{
"case": {"seven_segments": SEVEN_SEGMENTS_CASE},
"source_vertex_count": len(source_seven_segments),
"translated_vertex_count": len(translated_seven_segments),
"max_sorted_vertex_delta": seven_segments_delta,
"ok": len(source_seven_segments) == len(translated_seven_segments) and seven_segments_delta <= 1e-6,
}
)
source_field_value = _evaluated_vertices(
_build_field_value_wrapper("VG Verify Source Field Value", bpy.data.node_groups["Field Value"], FIELD_VALUE_CASE)
)
translated_field_value = _evaluated_vertices(
_build_field_value_wrapper("VG Verify Translated Field Value", bpy.data.node_groups["VG Field Value"], FIELD_VALUE_CASE)
)
field_value_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source_field_value), sorted(translated_field_value))),
default=0.0,
)
results.append(
{
"case": {"field_value": FIELD_VALUE_CASE},
"source_vertex_count": len(source_field_value),
"translated_vertex_count": len(translated_field_value),
"max_sorted_vertex_delta": field_value_delta,
"ok": len(source_field_value) == len(translated_field_value) and field_value_delta <= 1e-6,
}
)
for index, inputs in enumerate(CASES):
source_count = _evaluated_vertex_count(_build_count_wrapper(f"VG Verify Source Digit {index}", source, inputs))
translated_count = _evaluated_vertex_count(
_build_count_wrapper(f"VG Verify Translated Digit {index}", translated, inputs)
)
results.append(
{
"case": inputs,
"source_count": source_count,
"translated_count": translated_count,
"ok": source_count == translated_count,
}
)
source_next_digit = bpy.data.node_groups["Next Digit"]
translated_next_digit = bpy.data.node_groups["VG Next Digit"]
for index, inputs in enumerate(NEXT_DIGIT_CASES):
source_count = _evaluated_vertex_count(
_build_count_wrapper(f"VG Verify Source Next Digit {index}", source_next_digit, inputs)
)
translated_count = _evaluated_vertex_count(
_build_count_wrapper(f"VG Verify Translated Next Digit {index}", translated_next_digit, inputs)
)
results.append(
{
"case": {"next_digit": inputs},
"source_count": source_count,
"translated_count": translated_count,
"ok": source_count == translated_count,
}
)
delete_mismatches = []
source_delete = bpy.data.node_groups["Delete Segments"]
translated_delete = bpy.data.node_groups["VG Delete Segments"]
checked = 0
for digit in DELETE_DIGITS:
for segment_position in DELETE_SEGMENT_POSITIONS:
inputs = {"Digit": digit, "Segment Position": segment_position}
source_count = _evaluated_vertex_count(
_build_bool_count_wrapper(f"VG Verify Source Delete {digit}-{segment_position}", source_delete, inputs)
)
translated_count = _evaluated_vertex_count(
_build_bool_count_wrapper(
f"VG Verify Translated Delete {digit}-{segment_position}",
translated_delete,
inputs,
)
)
checked += 1
if source_count != translated_count:
delete_mismatches.append(
{
"digit": digit,
"segment_position": segment_position,
"source_count": source_count,
"translated_count": translated_count,
}
)
results.append(
{
"case": "delete_segments_grid",
"checked": checked,
"mismatches": delete_mismatches[:10],
"mismatch_count": len(delete_mismatches),
"ok": not delete_mismatches,
}
)
ok = all(result["ok"] for result in results)
print("VG_FIELDVALUE_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())