Translate pie chart graph family

This commit is contained in:
Meta Vens 2026-05-04 13:48:32 +01:00
commit 50c2079ed3
8 changed files with 481 additions and 4 deletions

View file

@ -114,6 +114,13 @@ Completed Cartesian function graph entry:
- Toolchain note: `external/geometry-script` is patched so nested `@tree` calls
create `GeometryNodeGroup` nodes under Blender 5.1.
Completed pie chart entry:
- `docs/research/translations/shriinivas-piechart.md`
- Source groups: `NodeGroup`, `Pie Chart`
- Verification: translated segment and three-slice chart match evaluated source
geometry with sorted max vertex delta `0.0` in Blender 5.1.1.
## Extraction Workflow
1. Download one source `.blend` into an ignored experiment folder.

View file

@ -96,6 +96,24 @@ This is a good control pattern. A graph can expose a broad global control like
Use this when a graph should work plainly by default but become more expressive
when upstream geometry brings richer attributes.
## Groups Can Pass Batons
Reusable node groups do not have to output geometry alone. They can output
state: an angle, count, bound, accumulated offset, or any other value the next
group needs.
The pie chart segment returns:
```text
Curve: visible wedge geometry
End: the angle where the next wedge should begin
```
That makes the chart group a relay. Each segment runs its stretch, then passes
the end angle forward. This is the pattern for cumulative structures: charts,
stairs, chains, segmented shells, stacked floors, ribs, vertebrae, and anything
where part N+1 depends on where part N stopped.
## Stack Abstractions Carefully
There are two abstraction layers available:

View file

@ -0,0 +1,127 @@
# Translation: Shriinivas Pie Chart
## Source
- Repository: https://github.com/Shriinivas/geometrynodes
- Source file: `piechart.blend`
- Source groups:
- `NodeGroup`
- `Pie Chart`
- Geometry Script recreation:
`examples/geometry_script/shriinivas_piechart.py`
## Graph Family
The small pie chart is a nested state-passing graph. Each segment receives:
```text
start angle
value
total
shift distance
materials
```
It outputs:
```text
segment geometry
end angle
```
The chart group chains three segment calls:
```text
A starts at 0
B starts at A.end
C starts at B.end
```
This is the new abstraction lesson: a node group can return geometry and a
control value. The geometry is the visible slice; the control value is the baton
passed to the next runner.
## Segment Map
Source group: `NodeGroup`
Geometry Script group: `VG Pie Segment`
Mechanism:
- Compute `fraction = Value / Total`.
- Compute `sweep = fraction * tau`.
- Compute `End = Start + sweep`.
- Build a connected-center `Arc` from `Start` through `sweep`.
- Fill the arc into a mesh face.
- Extrude the face downward for pie thickness.
- Convert `fraction * 100` to a string and append `%`.
- Convert that string to curves, fill it, and extrude it lightly into text.
- Use `Attribute Statistic` over the slice face's `Position` to find a center
offset for the text.
- Compute a shift vector at the segment midpoint:
```text
mid = Start + sweep / 2
offset = (cos(mid) * Shift, sin(mid) * Shift, 0)
```
- Join text, extruded slice, and cap face.
- Offset the joined geometry by the shift vector.
- Return geometry plus `End`.
Metaphor: the segment group is a pastry cutter with a receipt printer attached.
It cuts one wedge, stamps the percentage label near its middle, nudges the whole
wedge outward if requested, and hands the next cutter the angle where it stopped.
## Chart Map
Source group: `Pie Chart`
Geometry Script group: `VG Pie Chart`
Mechanism:
- Compute `Total = A + B + C`.
- Call `VG Pie Segment` for A with `Start = 0`.
- Call `VG Pie Segment` for B with `Start = A.End`.
- Call `VG Pie Segment` for C with `Start = B.End`.
- Join the three segment geometries.
Metaphor: the chart group is not a shape maker. It is an accountant-conductor:
it totals the values, cues each segment in order, and makes sure every slice
starts where the previous one ended.
## Verification
Run:
```powershell
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-piechart.blend' --python '.\tools\verify_piechart_translations.py'
```
Results from Blender 5.1.1:
| Case | Vertices | Sorted max delta | Status |
| --- | ---: | ---: | --- |
| Segment | 319 | 0.0 | accepted |
| Chart | 957 | 0.0 | accepted |
Generated groups saved and reinspected:
- `VG Pie Segment`: 30 nodes, 48 links.
- `VG Pie Chart`: 8 nodes, 25 links.
## Lessons
- A reusable node group can output both visible geometry and invisible state.
That state can drive later graph calls.
- `Arc` with `connect_center=True` is a wedge skeleton. `Fill Curve` gives it a
face; `Extrude Mesh` gives it thickness.
- `Attribute Statistic` can summarize geometry to produce placement handles.
Here, the mean position of the wedge face becomes a label anchor.
- Text is geometry too: string operations feed `String to Curves`, then the same
fill/extrude/material pipeline applies.
- Charts and other sequential structures often need cumulative scalar flow.
In Geometry Script, nested `@tree` calls make that flow readable as ordinary
Python variable passing.

View file

@ -0,0 +1,145 @@
"""Geometry Script recreation of the small pie chart from Shriinivas/piechart.blend."""
try:
from tools.geometry_script_loader import load_repo_geometry_script
except ModuleNotFoundError:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from tools.geometry_script_loader import load_repo_geometry_script
load_repo_geometry_script()
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
TAU = 6.2831854820251465
@tree("VG Pie Segment")
def vg_pie_segment(
start: Float = TAU,
value: Float = 0.5,
total: Float = 0.5,
shift: Float = 0.5,
segment_material: Material = None,
text_material: Material = None,
):
fraction = math(operation=Math.Operation.DIVIDE, value=(value, total))
sweep = math(operation=Math.Operation.MULTIPLY, value=(fraction, TAU))
end = math(operation=Math.Operation.ADD, value=(start, sweep))
percentage = math(operation=Math.Operation.MULTIPLY, value=(fraction, 100.0))
percentage_string = value_to_string(data_type=ValueToString.DataType.FLOAT, value=percentage, decimals=2)
label = join_strings(delimiter="", strings=[percentage_string, string(string="%")])
arc_curve = arc(
mode=Arc.Mode.RADIUS,
resolution=64,
radius=1.0,
start_angle=start,
sweep_angle=sweep,
connect_center=True,
)
face = arc_curve.fill_curve()
extruded_face = face.extrude_mesh(mode=ExtrudeMesh.Mode.FACES, offset_scale=-0.1)
text_curve = string_to_curves(string=label, size=0.2, align_x="Center", align_y="Middle").curve_instances
text_face = text_curve.fill_curve()
text_mesh = text_face.extrude_mesh(mode=ExtrudeMesh.Mode.FACES, offset_scale=0.025).mesh
face_center = attribute_statistic(
data_type=AttributeStatistic.DataType.FLOAT_VECTOR,
domain=AttributeStatistic.Domain.FACE,
geometry=face,
attribute=position(),
).mean
positioned_text = text_mesh.set_position(offset=face_center).set_material(material=text_material)
mid_angle = math(operation=Math.Operation.ADD, value=(start, math(operation=Math.Operation.DIVIDE, value=(sweep, 2.0))))
shift_vector = combine_xyz(
x=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=mid_angle), shift)),
y=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=mid_angle), shift)),
z=0.0,
)
segment = extruded_face.mesh.set_material(material=segment_material)
cap = face.set_material(material=segment_material)
geometry = join_geometry(geometry=[positioned_text, segment, cap]).set_position(offset=shift_vector)
return {"Curve": geometry, "End": end}
@tree("VG Pie Chart")
def vg_pie_chart(
geometry: Geometry,
a: Float = 0.5,
a_shift: Float = 0.5,
a_material: Material = None,
b: Float = 0.5,
b_shift: Float = 0.0,
b_material: Material = None,
c: Float = 0.5,
c_shift: Float = 0.0,
c_material: Material = None,
text_material: Material = None,
):
del geometry
total = math(operation=Math.Operation.ADD, value=(math(operation=Math.Operation.ADD, value=(a, b)), c))
a_geometry, a_end = vg_pie_segment(
start=0.0,
value=a,
total=total,
shift=a_shift,
segment_material=a_material,
text_material=text_material,
)
b_geometry, b_end = vg_pie_segment(
start=a_end,
value=b,
total=total,
shift=b_shift,
segment_material=b_material,
text_material=text_material,
)
c_geometry, _c_end = vg_pie_segment(
start=b_end,
value=c,
total=total,
shift=c_shift,
segment_material=c_material,
text_material=text_material,
)
return join_geometry(geometry=[a_geometry, b_geometry, c_geometry])
def _finalize_groups():
import bpy
groups = []
for name in ("VG Pie Segment", "VG Pie Chart"):
group = bpy.data.node_groups.get(name)
if not group:
continue
group.use_fake_user = True
if hasattr(group, "interface"):
for item in group.interface.items_tree:
if item.item_type == "SOCKET" and item.in_out == "OUTPUT" and item.name == "Result":
item.name = "Geometry"
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in {"A", "B", "C"}:
item.name = item.name.lower()
groups.append(group)
return groups
_finalize_groups()
if __name__ == "__main__":
for group in _finalize_groups():
print(
"VG_PIE_GROUP",
group.name,
len(group.nodes),
len(group.links),
sorted(set(node.bl_idname for node in group.nodes)),
)

View file

@ -35,6 +35,10 @@ The completed Cartesian function translation is
`Geometry Nodes Line`, `Geometry Nodes Parabola`, and `Geometry Nodes Circle`
from `cartesian.blend`.
The completed pie chart translation is
`docs/research/translations/shriinivas-piechart.md`. It covers `NodeGroup` and
`Pie Chart` from `piechart.blend`.
`docs/research/procedural-doctrine.md` now stores the current intuitive model
for mapping geometric ideas to Geometry Script: curves as rails, fields as
weather, `Set Position` as the hinge, profiles as body, attributes as optional
@ -46,8 +50,8 @@ Run `.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is
missing in a fresh workspace.
The current next action is to select the next Tier 1 source graph from
`Shriinivas/geometrynodes`, preferably `piechart.blend` or `fieldvalue.blend`,
inspect its node groups, and add the next doctrine-backed translation entry.
`Shriinivas/geometrynodes`, preferably `fieldvalue.blend`, inspect its node
groups, and add the next doctrine-backed translation entry.
## Important Invariants

View file

@ -6,3 +6,4 @@
{"ts": "2026-05-04T12:24:11+00:00", "type": "verification", "status": "accepted", "note": "Verified translated Shriinivas cartesian helper behavior against source NodeGroup.001 by wrapping both helpers as Curve+Vector -> Set Position -> Curve to Mesh and comparing evaluated mesh vertices. Test used Count=25, Start=(-0.45,0,0), End=(0.45,0,0), r=0.6, Value=1.25; source and translation both produced 200 vertices with max delta 0.0."}
{"ts": "2026-05-04T12:42:20+00:00", "type": "implementation", "status": "accepted", "note": "Translated and verified all four inspected Shriinivas/cartesian.blend graph groups: helper, line, parabola, and circle. Verification compares evaluated meshes in Blender 5.1.1; helper/line/parabola match direct vertex order with max delta 0.0, circle matches sorted vertex set with max sorted delta 0.0."}
{"ts": "2026-05-04T12:42:20+00:00", "type": "tooling", "status": "accepted", "note": "Reverted direct AppData Geometry Script surgery. Cloned upstream Geometry Script into ignored external/geometry-script, committed local branch vibegeometry/blender-5-nested-tree-groups with nested @tree group fix, stored patch under patches/geometry-script, and added setup_geometry_script_clone.ps1 plus loader so VibeGeometry uses the repo-local patched clone."}
{"ts": "2026-05-04T12:48:18+00:00", "type": "implementation", "status": "accepted", "note": "Translated and verified Shriinivas/piechart.blend small graph family: NodeGroup -> VG Pie Segment and Pie Chart -> VG Pie Chart. Segment and chart evaluated mesh comparisons both passed in Blender 5.1.1 with sorted max vertex delta 0.0. Doctrine added: reusable groups can pass invisible scalar state such as End angle alongside visible geometry."}

View file

@ -82,5 +82,5 @@ data_flow:
- If state grows faster than understanding, cut it back.
current_status:
summary: VibeGeometry has an initial persistence scaffold plus verified Geometry Script translations for all four inspected Shriinivas/cartesian.blend groups: helper, line, parabola, and circle.
next_action: Select the next Tier 1 source graph from Shriinivas/geometrynodes, preferably `piechart.blend` or `fieldvalue.blend`, inspect its node groups, and add the next doctrine-backed translation entry.
summary: VibeGeometry has an initial persistence scaffold plus verified Geometry Script translations for the inspected Shriinivas cartesian and pie chart graph families.
next_action: Select the next Tier 1 source graph from Shriinivas/geometrynodes, preferably `fieldvalue.blend`, inspect its node groups, and add the next doctrine-backed translation entry.

View file

@ -0,0 +1,175 @@
"""Compare Shriinivas pie chart source graphs against Geometry Script translations."""
from __future__ import annotations
import importlib.util
import json
import math
import sys
from pathlib import Path
from typing import Any
import bpy
ROOT = Path(__file__).resolve().parents[1]
TRANSLATION = ROOT / "examples" / "geometry_script" / "shriinivas_piechart.py"
def _load_translation() -> None:
spec = importlib.util.spec_from_file_location("vg_piechart_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: Any, 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 _set_inputs(group_node: bpy.types.GeometryNode, values: dict[str, object]) -> None:
lower_values = {name.lower(): value for name, value in values.items()}
for socket in group_node.inputs:
if socket.name in values:
socket.default_value = values[socket.name]
elif socket.name.lower() in lower_values:
socket.default_value = lower_values[socket.name.lower()]
def _build_segment_wrapper(name: str, group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
group_node = nodes.new("GeometryNodeGroup")
group_node.node_tree = group
_set_inputs(group_node, inputs)
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(group_node.outputs, "Curve"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _build_chart_wrapper(name: str, group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
group_input = nodes.new("NodeGroupInput")
group_node = nodes.new("GeometryNodeGroup")
group_node.node_tree = group
_set_inputs(group_node, inputs)
group_output = nodes.new("NodeGroupOutput")
if any(socket.name == "Geometry" for socket in group_node.inputs):
links.new(_socket_by_name(group_input.outputs, "Geometry"), _socket_by_name(group_node.inputs, "Geometry"))
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
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 _bounds(vertices: list[tuple[float, float, float]]) -> dict[str, list[float]]:
return {
"min": [min(vertex[i] for vertex in vertices) for i in range(3)],
"max": [max(vertex[i] for vertex in vertices) for i in range(3)],
}
def _compare(case: str, source_vertices, translated_vertices) -> dict[str, object]:
if len(source_vertices) != len(translated_vertices):
return {
"case": case,
"ok": False,
"reason": "vertex_count_mismatch",
"source_vertex_count": len(source_vertices),
"translated_vertex_count": len(translated_vertices),
"source_bounds": _bounds(source_vertices) if source_vertices else None,
"translated_bounds": _bounds(translated_vertices) if translated_vertices else None,
}
max_delta = 0.0
for source, translated in zip(sorted(source_vertices), sorted(translated_vertices)):
max_delta = max(max_delta, math.dist(source, translated))
return {
"case": case,
"ok": max_delta <= 1e-6,
"source_vertex_count": len(source_vertices),
"translated_vertex_count": len(translated_vertices),
"max_sorted_vertex_delta": max_delta,
"source_bounds": _bounds(source_vertices),
"translated_bounds": _bounds(translated_vertices),
}
def main() -> int:
_load_translation()
text_material = bpy.data.materials.get("Text Material") or bpy.data.materials.new("Text Material")
mat1 = bpy.data.materials.get("Material 1") or bpy.data.materials.new("Material 1")
mat2 = bpy.data.materials.get("Material 2") or bpy.data.materials.new("Material 2")
mat3 = bpy.data.materials.get("Material 3") or bpy.data.materials.new("Material 3")
segment_inputs = {
"Start": 0.35,
"Value": 0.8,
"Total": 2.4,
"Shift": 0.2,
"Segment Material": mat1,
"Text Material": text_material,
}
chart_inputs = {
"A": 0.8,
"A Shift": 0.2,
"A Material": mat1,
"B": 1.1,
"B Shift": 0.05,
"B Material": mat2,
"C": 0.5,
"C Shift": 0.0,
"C Material": mat3,
"Text Material": text_material,
}
cases = [
(
"segment",
_build_segment_wrapper("VG Verify Source Pie Segment", bpy.data.node_groups["NodeGroup"], segment_inputs),
_build_segment_wrapper("VG Verify Translated Pie Segment", bpy.data.node_groups["VG Pie Segment"], segment_inputs),
),
(
"chart",
_build_chart_wrapper("VG Verify Source Pie Chart", bpy.data.node_groups["Pie Chart"], chart_inputs),
_build_chart_wrapper("VG Verify Translated Pie Chart", bpy.data.node_groups["VG Pie Chart"], chart_inputs),
),
]
results = [_compare(case, _evaluated_vertices(source), _evaluated_vertices(translated)) for case, source, translated in cases]
ok = all(result["ok"] for result in results)
print("VG_PIECHART_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())