Expand verified geometry node corpus

This commit is contained in:
Meta Vens 2026-05-04 16:00:47 +01:00
commit 9bc09bb7c2
17 changed files with 1578 additions and 28 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ state/scratch-compaction-*.md
experiments/renders/
experiments/blender-cache/
experiments/source-blends/
experiments/source-repos/
experiments/inspection/
*.blend1
*.blend2

View file

@ -74,6 +74,7 @@ current VibeGeometry toolchain branch, and applies the patches stored in:
```text
patches/geometry-script/0001-fix-nested-tree-group-references-on-blender-4.patch
patches/geometry-script/0002-Port-node-tree-conversion-operator.patch
patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch
```
`external/` is intentionally ignored by the VibeGeometry repository. The clone
@ -93,6 +94,8 @@ This is the branch VibeGeometry should use locally. It contains:
- the nested `@tree` group fix from
`vibegeometry/blender-5-nested-tree-groups`
- a Blender 5.1 port of the historical `nodes_to_script` converter prototype
- a Blender 5.1 menu-socket default tolerance fix so group construction does
not fail when `NodeSocketMenu` enum items are not populated yet
The converter adds:
@ -123,6 +126,10 @@ Known limits:
- Generated names and ordering reflect extraction mechanics, not spatial intent.
- Behavioral equivalence still needs the same evaluated-geometry or scalar
harness verification used by the hand translations.
- Blender 5 capture items are still a live gap in Geometry Script's generic
node builder. The accepted `VG Field Value` translation creates typed capture
items explicitly in script instead of relying on the generated
`capture_attribute(...)` wrapper.
## Upstream PR Branch

View file

@ -134,13 +134,32 @@ Completed field value helper entry:
`Mesh Line.Count` with the result; next-digit branch helper matches source
across four cases spanning both sides of `Max Precision`; delete-mask scalar
helper matches source across 195 digit/segment-position cases with zero
mismatches.
mismatches; full `Field Value` default-route geometry matches source with
256 vertices and sorted max vertex delta `0.0`.
- Toolchain note: full `Field Value` required explicit Blender 5 capture-item
creation and a Geometry Script fork fix for menu socket defaults.
Boundary note: the full `Field Value` graph is a 176-node composition involving
menu switches, capture attributes, sample-index reads, instances, transforms,
and delete geometry. Before translating it by hand, test or port upstream's
historical `nodes_to_script` converter so VibeGeometry gets a generated first
draft to inspect.
Completed parametric/polar equation entry:
- `docs/research/translations/shriinivas-paramnpolareq.md`
- Source groups: `NodeGroup`, `NodeGroup.001`, `NodeGroup.002`,
`NodeGroup.003`, `Archimedes Spiral`, `Epicycloid`,
`Geometry Nodes Group.002`
- Verification: scalar equation helpers match by placing outputs into a single
vertex position; Archimedes, epicycloid, and mirrored root spiral evaluated
vertices match source geometry with max sorted deltas at or near `0.0`.
- Translation value: accumulated parameter ramps, parametric equations, polar
conversion, mirrored curve streams, and scalar-harness design.
Started CurveToMeshUV utility entry:
- `docs/research/translations/quellenform-curve-to-mesh-uv.md`
- Source group translated so far: `Auto Smooth`
- Verification: source and translated `Auto Smooth` preserve cube geometry and
match smooth-face flags in Blender 5.1.1.
- Boundary: the main `Curve to Mesh UV` group is a 126-node utility graph and
should be approached through staged subcontracts: curve resampling, spline
parameter UVs, cap masks, named attributes, then final mesh output.
## Extraction Workflow

View file

@ -145,6 +145,42 @@ Verification cue: test more than one parameter value. Field graphs often look
right at one value and reveal swapped axes or stale constants when controls
move.
## Parameters Are Conveyors
Reach for `Accumulate Field` when a curve or repeated structure needs a steady
parameter ramp: `t`, `theta`, distance, age, row number, phase, growth amount.
Mental move: make a boring sequence of stations first, compute the step between
stations, then let the graph stamp each station with the accumulated parameter.
The equation or deformation reads that parameter downstream.
```python
step = math(
operation=Math.Operation.DIVIDE,
value=(max_value, math(operation=Math.Operation.SUBTRACT, value=(resolution, 1.0))),
)
theta = accumulate_field(
data_type=AccumulateField.DataType.FLOAT,
domain=AccumulateField.Domain.POINT,
value=step,
).trailing
mesh = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=resolution,
offset=(0.0, 0.0, 1.0),
)
geometry = mesh.set_position(position=combine_xyz(x=x_from_theta, y=y_from_theta, z=0.0))
```
Metaphor in use: the mesh line is a conveyor belt. `Accumulate Field` stamps
each station with a larger number, and the curve equation tells that station
where to stand.
Verification cue: compare evaluated vertices at a non-default resolution and
range. Off-by-one errors hide at endpoints.
## Set Position Is The Hinge
Reach for `set_position` when your graph has built an invisible spatial rule
@ -211,6 +247,44 @@ attribute lets some stations use a wider bit without changing the rail.
Verification cue: inspect mesh vertex counts and bounding dimensions, not just
the existence of a `Curve to Mesh` node.
## Surface Passes Are Polish Gates
Reach for surface-state utilities when geometry already exists but needs
controlled smoothing, normals, material assignment, UVs, or attributes before
it is usable downstream.
Mental move: keep shape generation separate from finishing state. In `VG Auto
Smooth`, the mesh enters unchanged, edge angle and existing smooth flags decide
edge smoothing, then the face domain is stamped smooth.
```python
shallow_enough = compare(
operation=Compare.Operation.LESS_EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=edge_angle().unsigned_angle,
b=angle,
)
edge_pass = set_shade_smooth(
domain=SetShadeSmooth.Domain.EDGE,
mesh=geometry,
selection=is_edge_smooth(),
shade_smooth=shallow_enough & is_face_smooth(),
)
geometry = set_shade_smooth(
domain=SetShadeSmooth.Domain.FACE,
mesh=edge_pass,
selection=True,
shade_smooth=True,
)
```
Metaphor in use: this is a polishing gate, not a forge. The form passes
through; the gate decides which surface state is allowed to survive.
Verification cue: compare surface flags as well as vertices. A finishing graph
can preserve shape perfectly while changing the thing it was built to control.
## Field Masks Are Chisels
Reach for field masks when a regular seed mesh should become a shaped part by
@ -354,6 +428,38 @@ anything cumulative.
Verification cue: test the composed graph, not only the segment. A baton can be
correct locally but passed to the wrong next socket.
## Capture Before The Crowd
Reach for `Capture Attribute` when a value or index must survive a domain
change, instancing layer, or later deletion pass.
Mental move: before geometry enters a crowd of instances, pin a name and domain
to the value that later stages still need. In `VG Field Value`, the source
value, glyph face index, digit-slot index, and vector-component index each have
to ride through different instancing layers.
```python
glyph_faces = _capture_attribute_item("INT", "FACE", segment_glyph, index(), "int field")
digit_indexed = _capture_attribute_item("INT", "INSTANCE", digit_instances, index(), "int field")
hidden_segments = vg_delete_segments(
digit=final_digit_code,
segment_position=glyph_faces.int_field,
)
visible = delete_geometry(
mode=DeleteGeometry.Mode.ALL,
domain=DeleteGeometry.Domain.FACE,
geometry=realized_digits,
selection=hidden_segments,
)
```
Metaphor in use: capture is a luggage tag. Once geometry moves through airports
of instances and domains, untagged values are gone in the baggage system.
Verification cue: test the full composed graph after realization or deletion,
not just the captured helper. The point of capture is later survival.
## Branch Helpers Are Switchboards
Reach for branch helpers when the same output contract can be fed by multiple

View file

@ -0,0 +1,64 @@
# Translation: quellenform Curve To Mesh UV Foothold
## Source
- Repository: https://github.com/quellenform/blender-CurveToMeshUV
- Source file:
`Curve to Mesh UV v1.7.02.230213-FREE - Blender 3.4+.blend`
- Source group translated in this pass:
- `Auto Smooth`
- Geometry Script recreation:
`examples/geometry_script/quellenform_curve_to_mesh_uv.py`
## Why This Graph
The main `Curve to Mesh UV` group is a 126-node utility network with curve
resampling, spline parameters, UV fields, capture attributes, endpoint masks,
domain-size accounting, and named attribute outputs. That is useful quarry, but
not a single-pass snack.
`Auto Smooth` is the small utility attached to that system. It teaches how a
graph can be a surface-finishing gate instead of a shape generator.
## Auto Smooth Map
Source group: `Auto Smooth`
Geometry Script group: `VG Auto Smooth`
Mechanism:
- Read the unsigned mesh edge angle.
- Compare it against the exposed `Angle`.
- AND that threshold result with the existing face smooth state.
- On the edge domain, set shade smooth for already-smooth edges using that
thresholded condition.
- On the face domain, set shade smooth true for the full mesh.
Metaphor: the group is a polishing gate. It does not build the object. It
decides where the surface may keep a smooth handshake across edges and then
hands the mesh back to the larger utility.
## Verification
Run:
```powershell
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background 'E:\Projects\VibeGeometry\experiments\source-repos\blender-CurveToMeshUV\Curve to Mesh UV v1.7.02.230213-FREE - Blender 3.4+.blend' --python '.\tools\verify_curve_to_mesh_uv_translations.py'
```
Results from Blender 5.1.1:
| Case | Source | Translated | Delta | Status |
| --- | ---: | ---: | ---: | --- |
| Auto Smooth cube vertices | 8 | 8 | 0.0 | accepted |
| Auto Smooth smooth faces | 6 | 6 | exact flags | accepted |
## Lessons
- Not every reusable node group creates form. Some preserve or condition
surface state for a larger graph.
- Edge-domain and face-domain passes can be stacked deliberately: first decide
edge continuity, then stamp final face smoothness.
- Utility mega-graphs should be entered through their small helpers first.
They expose the local contracts the main group relies on.

View file

@ -11,6 +11,7 @@
- `Delete Segments`
- `Next Digit`
- `Digit At`
- `Field Value`
- Geometry Script recreation:
`examples/geometry_script/shriinivas_fieldvalue.py`
@ -24,18 +25,6 @@ Together they show the first display-system layers: tiny glyph geometry,
field-sculpted segment geometry, segment composition, segment visibility masks,
and value-only digit extraction.
## Current Boundary
The remaining full `Field Value` group has 176 nodes and combines menu switches,
capture attributes across multiple domains, sampled fields, instancing,
instance transforms, delete-geometry selection, material assignment, and the
helpers translated here.
That graph should not be translated manually as one heroic sweep. The next
right move is to port or test the upstream `nodes_to_script` prototype against
this graph so the first draft can be generated, inspected, and then cleaned
with doctrine instead of hand-copied node by node.
## Create Decimal Map
Source group: `Create Decimal`
@ -183,6 +172,43 @@ Metaphor: `Next Digit` is the read head for the display tape. Before the decimal
boundary it reads the fractional reel; after that boundary it reads the whole
number reel.
## Field Value Map
Source group: `Field Value`
Geometry Script group: `VG Field Value`
Mechanism:
- Use menu switches to route three control choices:
input type (`Float` or `Vector`), source domain (`Point`, `Edge`, or `Face`),
and text alignment (`Center`, `Right`, or `Left`).
- Capture the requested float or vector field on point, edge, and face domains,
then choose the captured geometry and captured value stream for the active
domain.
- Convert edge or face domains to points so the display can instance one value
readout per source element.
- Build one seven-segment glyph, capture each glyph face index, then instance
that glyph across digit slots, vector component slots, and source elements.
- Sample source position and normal by domain index so each readout can be
placed and oriented back onto the element that produced it.
- Compute the active scalar value for the current component, split it into
whole/fractional parts, decide visible slots, negative sign, decimal point,
overflow, and leading-zero behavior.
- Call `VG Next Digit` for digit extraction and `VG Delete Segments` to delete
the dark faces from each glyph.
- Realize instances and assign the requested material.
Metaphor: the full graph is a label printer mounted on the mesh. It reads a
value from each element, assembles a small digit rack, swings the rack onto the
element normal, then punches out the unlit segments.
Implementation note: Blender 5 capture items and menu sockets exposed two
Geometry Script drift points. `VG Field Value` uses a local
`_capture_attribute_item(...)` helper to create typed capture items explicitly,
and the VibeGeometry Geometry Script fork tolerates menu socket defaults whose
enum items do not exist at group-interface construction time.
## Verification
Run:
@ -207,6 +233,7 @@ Results from Blender 5.1.1:
| Digit At `1234.0`, position `1` | 3 | 3 | 0 | accepted |
| Digit At `98765.0`, position `2` | 7 | 7 | 0 | accepted |
| Digit At `120305.0`, position `3` | 0 | 0 | 0 | accepted |
| Field Value default-route vertices | 256 | 256 | 0.0 | accepted |
| Next Digit branch cases | 4 cases | 4 cases | 0 mismatches | accepted |
| Delete Segments grid | 195 cases | 195 cases | 0 mismatches | accepted |
@ -236,3 +263,8 @@ Results from Blender 5.1.1:
verifies positions before and after `Max Precision`, because the interesting
behavior is not digit extraction itself but choosing which number part feeds
extraction.
- Capture attributes are a loading dock for fields. Once a graph crosses into
instancing, capture the values and indices that later stages must still know.
- Full display graphs are stacked assemblies: source element points carry
value racks; value racks carry component stacks; component stacks carry digit
slots; digit slots carry segment faces. Preserve each layer's index baton.

View file

@ -0,0 +1,113 @@
# Translation: Shriinivas Parametric And Polar Equations
## Source
- Repository: https://github.com/Shriinivas/geometrynodes
- Source file: `paramnpolareq.blend`
- Source groups:
- `NodeGroup`
- `NodeGroup.001`
- `NodeGroup.002`
- `NodeGroup.003`
- `Archimedes Spiral`
- `Epicycloid`
- `Geometry Nodes Group.002`
- Geometry Script recreation:
`examples/geometry_script/shriinivas_paramnpolareq.py`
## Why This Graph
This sample teaches parametric curve construction without hiding behind final
rendering polish. The source graphs make a line of points, accumulate a
parameter over that line, turn the parameter into `x/y` fields, and then move
the points into curve shapes.
## Equation Helpers
`VG Param Epicycloid X` and `VG Param Epicycloid Y` recreate the source
epicycloid scalar helpers:
```text
x = (a + b) * cos(t) - b * cos((a / b + 1) * t)
y = (a + b) * sin(t) - b * sin((a / b + 1) * t)
```
`VG Polar Spiral X` and `VG Polar Spiral Y` recreate the polar helper pair:
```text
r = a + b * theta
x = cos(theta) * r
y = sin(theta) * r
```
Metaphor: these helpers are plotter pens without paper. They answer "where
should the point be for this parameter?" but they do not decide how many points
exist or how the curve is sampled.
## Parameter Conveyor
`VG Archimedes Spiral`, `VG Epicycloid`, and `VG Mirrored Root Spiral` share a
sampling spine:
- Create a `Mesh Line` with `Resolution` points.
- Compute `step = max_value / (Resolution - 1)`.
- Feed `step` into `Accumulate Field`.
- Use the trailing accumulated value as `theta` or `t`.
- Compute `x/y`, combine them into a position, and move the mesh line with
`Set Position`.
Metaphor: the mesh line is a conveyor belt. `Accumulate Field` stamps each
station with a progressively larger parameter, and the equation helper tells
that station where to stand.
## Mirrored Root Spiral
Source group `Geometry Nodes Group.002` is a two-arm spiral:
```text
theta = accumulated(2 * pi * Round Count)
r = a * sqrt(theta)
x = r * cos(theta)
y = r * sin(theta)
```
The source builds a forward point stream and a mirrored stream at `(-x, -y)`,
then joins them.
Metaphor: the first arm is a chalk line unwinding from the center; the second
arm is the same chalk line reflected through the origin.
## Verification
Run:
```powershell
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-paramnpolareq.blend' --python '.\tools\verify_paramnpolareq_translations.py'
```
Results from Blender 5.1.1:
| Case | Source | Translated | Delta | Status |
| --- | ---: | ---: | ---: | --- |
| Epicycloid X scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
| Epicycloid Y scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
| Polar X scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
| Polar Y scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
| Archimedes Spiral vertices | 64 | 64 | 4.47e-8 | accepted |
| Epicycloid vertices | 80 | 80 | 0.0 | accepted |
| Mirrored Root Spiral vertices | 192 | 192 | 0.0 | accepted |
The scalar helpers are verified by putting the scalar into a single vertex's X
position. The curve groups are verified by sorted evaluated vertex comparison.
## Lessons
- `Accumulate Field` is the natural way to build a parameter ramp inside a
node graph. Python should describe the ramp; the graph should evaluate it.
- Equation helpers are worth naming when several curve generators need the same
scalar law.
- Verifiers should normalize socket names when comparing source groups to
generated groups. `maxT` and `Max T` are the same control for behavior even
when the UI spelling drifts.
- A scalar harness should preserve numeric value, not just count geometry.
Driving a vertex coordinate is often cleaner than driving `Mesh Line.Count`.

View file

@ -0,0 +1,66 @@
"""Geometry Script footholds from quellenform/blender-CurveToMeshUV."""
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.
@tree("VG Auto Smooth")
def vg_auto_smooth(geometry: Geometry, angle: Float = 0.0):
shallow_enough = compare(
operation=Compare.Operation.LESS_EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=edge_angle().unsigned_angle,
b=angle,
)
preserve_smooth_face = boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(shallow_enough, is_face_smooth()),
)
edge_pass = set_shade_smooth(
domain=SetShadeSmooth.Domain.EDGE,
mesh=geometry,
selection=is_edge_smooth(),
shade_smooth=preserve_smooth_face,
)
face_pass = set_shade_smooth(
domain=SetShadeSmooth.Domain.FACE,
mesh=edge_pass,
selection=True,
shade_smooth=True,
)
return {"Geometry": face_pass}
def _finalize_groups():
import bpy
group = bpy.data.node_groups.get("VG Auto Smooth")
if group:
group.use_fake_user = True
return [group]
return []
_finalize_groups()
if __name__ == "__main__":
for group in _finalize_groups():
print(
"VG_CURVE_TO_MESH_UV_GROUP",
group.name,
len(group.nodes),
len(group.links),
sorted(set(node.bl_idname for node in group.nodes)),
)

View file

@ -12,6 +12,21 @@ except ModuleNotFoundError:
load_repo_geometry_script()
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
from geometry_script.api.state import State
from geometry_script.api.types import Type
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
node.capture_items.new(data_type, name)
node.active_index = 0
node.domain = domain
set_or_create_link(geometry, node.inputs[0])
set_or_create_link(value, node.inputs[1])
key = name.lower().replace(" ", "_")
return OutputsList({"geometry": Type(node.outputs[0]), key: Type(node.outputs[1])})
def _int_equal(value, target: int):
@ -319,6 +334,533 @@ def vg_delete_segments(digit: Int = 0, segment_position: Int = 0):
)
@tree("VG Field Value")
def vg_field_value(
geometry: Geometry,
input_type: Menu = "Float",
float_value: Float = 0.0,
vector_value: Vector = (0.0, 0.0, 0.0),
digits_before_decimal: Int = 4,
precision: Int = 2,
domain: Menu = "Point",
alignment: Menu = "Center",
align_to_global: Bool = True,
scale: Float = 0.019999999552965164,
digit_separation: Float = 0.30000001192092896,
element_separation: Float = 1.0,
x_offset: Float = 0.0,
y_offset: Float = 0.0,
z_offset: Float = 0.0010000000474974513,
rotation: Int = 0,
segment_width_factor: Float = 0.5,
segment_separation_factor: Float = 0.10000000149011612,
tip_sharpness: Float = 1.0,
leading_zeros: Bool = False,
material: Material = None,
):
domain_name = menu_switch(
active_index=2,
data_type="STRING",
menu=domain,
point="Point",
face="Face",
edge="Edge",
)
use_face_domain = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.STRING,
mode=Compare.Mode.ELEMENT,
a=domain_name.output,
b="Face",
)
use_edge_domain = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.STRING,
mode=Compare.Mode.ELEMENT,
a=domain_name.output,
b="Edge",
)
value_kind = menu_switch(
active_index=1,
data_type="STRING",
menu=input_type,
float="Float",
vector="Vector",
)
use_vector_value = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.STRING,
mode=Compare.Mode.ELEMENT,
a=value_kind.output,
b="Vector",
)
point_float = _capture_attribute_item("FLOAT", "POINT", geometry, float_value, "float field")
edge_float = _capture_attribute_item("FLOAT", "EDGE", geometry, float_value, "float field")
face_float = _capture_attribute_item("FLOAT", "FACE", geometry, float_value, "float field")
point_vector = _capture_attribute_item("VECTOR", "POINT", geometry, vector_value, "vector field")
edge_vector = _capture_attribute_item("VECTOR", "EDGE", geometry, vector_value, "vector field")
face_vector = _capture_attribute_item("VECTOR", "FACE", geometry, vector_value, "vector field")
captured_float_geometry = switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_face_domain,
false=switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_edge_domain,
false=point_float.geometry,
true=edge_float.geometry,
),
true=face_float.geometry,
)
captured_vector_geometry = switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_face_domain,
false=switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_edge_domain,
false=point_vector.geometry,
true=edge_vector.geometry,
),
true=face_vector.geometry,
)
sampled_source = switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_vector_value,
false=captured_float_geometry,
true=captured_vector_geometry,
)
source_points = switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_face_domain,
false=switch(
input_type=Switch.InputType.GEOMETRY,
switch=use_edge_domain,
false=sampled_source,
true=mesh_to_points(mode="EDGES", mesh=sampled_source, radius=0.05000000074505806),
),
true=mesh_to_points(mode="FACES", mesh=sampled_source, radius=0.05000000074505806),
)
component_count = switch(input_type=Switch.InputType.INT, switch=use_vector_value, false=1, true=3)
segment_glyph = vg_seven_segments(
horizontal_segment_size=2.0,
horizontal_segment_thickness=segment_width_factor,
vertical_segment_size=2.0,
vertical_segment_thickness=segment_width_factor,
x_separation=segment_separation_factor,
y_seperation=segment_separation_factor,
tip_sharpness=tip_sharpness,
)
glyph_faces = _capture_attribute_item("INT", "FACE", segment_glyph, index(), "int field")
glyph_position = separate_xyz(vector=position())
glyph_y_stats = attribute_statistic(
data_type=AttributeStatistic.DataType.FLOAT,
domain=AttributeStatistic.Domain.POINT,
geometry=glyph_faces.geometry,
attribute=glyph_position.y,
)
glyph_height = math(operation=Math.Operation.SUBTRACT, value=(glyph_y_stats.max, glyph_y_stats.min))
component_spacing = math(operation=Math.Operation.ADD, value=(glyph_height, element_separation))
vector_component_step = switch(input_type=Switch.InputType.FLOAT, switch=use_vector_value, false=0.0, true=component_spacing)
component_points = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=component_count,
start_location=combine_xyz(x=0.0, y=vector_component_step, z=0.0),
offset=combine_xyz(
x=0.0,
y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, vector_component_step)),
z=0.0,
),
)
digit_count_without_decimal = math(operation=Math.Operation.ADD, value=(digits_before_decimal, precision))
digit_count_with_decimal = math(operation=Math.Operation.ADD, value=(digit_count_without_decimal, 1.0))
has_precision = math(operation=Math.Operation.GREATER_THAN, value=(precision, 0.0))
display_digit_slots = math(operation=Math.Operation.ADD, value=(digit_count_with_decimal, has_precision))
glyph_x_stats = attribute_statistic(
data_type=AttributeStatistic.DataType.FLOAT,
domain=AttributeStatistic.Domain.POINT,
geometry=glyph_faces.geometry,
attribute=glyph_position.x,
)
glyph_width = math(operation=Math.Operation.SUBTRACT, value=(glyph_x_stats.max, glyph_x_stats.min))
digit_pitch = math(
operation=Math.Operation.MULTIPLY,
value=(math(operation=Math.Operation.ADD, value=(1.0, digit_separation)), glyph_width),
)
negative_digit_pitch = math(operation=Math.Operation.MULTIPLY, value=(-1.0, digit_pitch))
digit_step = combine_xyz(x=negative_digit_pitch, y=0.0, z=0.0)
digit_points = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=display_digit_slots,
start_location=vector_math(operation=VectorMath.Operation.DIVIDE, vector=(digit_step, (2.0, 0.0, 0.0))),
offset=digit_step,
)
digit_instances = instance_on_points(
points=digit_points,
instance=glyph_faces.geometry,
pick_instance=False,
rotation=(0.0, 0.0, 0.0),
scale=(1.0, 1.0, 1.0),
)
decimal_slot = math(
operation=Math.Operation.SUBTRACT,
value=(math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, precision)), 0.5),
)
decimal_offset = math(operation=Math.Operation.MULTIPLY, value=(decimal_slot, digit_pitch))
final_digit_index = math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, 1.0))
is_final_digit_slot = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=index(),
b=final_digit_index,
epsilon=0.0010000000474974513,
)
decimal_shifted_digits = set_position(
geometry=digit_instances,
offset=combine_xyz(
x=math(operation=Math.Operation.MULTIPLY, value=(decimal_offset, is_final_digit_slot)),
y=0.0,
z=math(operation=Math.Operation.MULTIPLY, value=(-0.0010000000474974513, is_final_digit_slot)),
),
)
digit_indexed = _capture_attribute_item("INT", "INSTANCE", decimal_shifted_digits, index(), "int field")
component_instances = instance_on_points(
points=component_points,
instance=digit_indexed.geometry,
pick_instance=False,
rotation=(0.0, 0.0, 0.0),
scale=(1.0, 1.0, 1.0),
)
component_indexed = _capture_attribute_item("INT", "INSTANCE", component_instances, index(), "int field")
value_instances = instance_on_points(
points=source_points,
instance=component_indexed.geometry,
pick_instance=False,
rotation=(0.0, 0.0, 0.0),
scale=(1.0, 1.0, 1.0),
)
point_position = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.POINT,
clamp=False,
geometry=sampled_source,
value=position(),
index=index(),
)
edge_position = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.EDGE,
clamp=False,
geometry=sampled_source,
value=position(),
index=index(),
)
face_position = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.FACE,
clamp=False,
geometry=sampled_source,
value=position(),
index=index(),
)
source_position = switch(
input_type=Switch.InputType.VECTOR,
switch=use_face_domain,
false=switch(input_type=Switch.InputType.VECTOR, switch=use_edge_domain, false=point_position, true=edge_position),
true=face_position,
)
alignment_name = menu_switch(
active_index=2,
data_type="STRING",
menu=alignment,
center="Center",
right="Right",
left="Left",
)
align_left = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.STRING,
mode=Compare.Mode.ELEMENT,
a=alignment_name.output,
b="Left",
)
align_right = compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.STRING,
mode=Compare.Mode.ELEMENT,
a=alignment_name.output,
b="Right",
)
scaled_digit_pitch = math(operation=Math.Operation.MULTIPLY, value=(scale, digit_pitch))
captured_float = switch(
input_type=Switch.InputType.FLOAT,
switch=use_face_domain,
false=switch(
input_type=Switch.InputType.FLOAT,
switch=use_edge_domain,
false=point_float.float_field,
true=edge_float.float_field,
),
true=face_float.float_field,
)
captured_vector = switch(
input_type=Switch.InputType.VECTOR,
switch=use_face_domain,
false=switch(
input_type=Switch.InputType.VECTOR,
switch=use_edge_domain,
false=point_vector.vector_field,
true=edge_vector.vector_field,
),
true=face_vector.vector_field,
)
vector_parts = separate_xyz(vector=captured_vector)
component_value = index_switch(
data_type=IndexSwitch.DataType.FLOAT,
index=component_indexed.int_field,
_0=vector_parts.x,
_1=vector_parts.y,
_2=vector_parts.z,
)
displayed_value = switch(input_type=Switch.InputType.FLOAT, switch=use_vector_value, false=captured_float, true=component_value)
absolute_value = math(operation=Math.Operation.ABSOLUTE, value=displayed_value)
whole_part = math(operation=Math.Operation.TRUNC, value=absolute_value)
whole_part_digits = math(
operation=Math.Operation.MAXIMUM,
value=(
math(
operation=Math.Operation.FLOOR,
value=math(
operation=Math.Operation.LOGARITHM,
value=(math(operation=Math.Operation.MULTIPLY, value=(whole_part, 10.0)), 10.0),
),
),
1.0,
),
)
natural_digit_count = math(operation=Math.Operation.ADD, value=(whole_part_digits, precision))
is_negative = math(operation=Math.Operation.LESS_THAN, value=(displayed_value, 0.0))
natural_slot_count = math(operation=Math.Operation.ADD, value=(natural_digit_count, is_negative))
half_text_width = math(
operation=Math.Operation.DIVIDE,
value=(math(operation=Math.Operation.MULTIPLY, value=(scaled_digit_pitch, natural_slot_count)), 2.0),
)
alignment_offset = switch(
input_type=Switch.InputType.FLOAT,
switch=align_left,
false=switch(input_type=Switch.InputType.FLOAT, switch=align_right, false=half_text_width, true=0),
true=math(operation=Math.Operation.MULTIPLY, value=(half_text_width, 2.0)),
)
placed_position = vector_math(
operation=VectorMath.Operation.ADD,
vector=(source_position, combine_xyz(x=math(operation=Math.Operation.ADD, value=(x_offset, alignment_offset)), y=y_offset, z=z_offset)),
)
point_normal = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.POINT,
clamp=False,
geometry=sampled_source,
value=normal(legacy_corner_normals=True).normal,
index=index(),
)
edge_normal = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.EDGE,
clamp=False,
geometry=sampled_source,
value=normal(legacy_corner_normals=True).normal,
index=index(),
)
face_normal = sample_index(
data_type=SampleIndex.DataType.FLOAT_VECTOR,
domain=SampleIndex.Domain.FACE,
clamp=False,
geometry=sampled_source,
value=normal(legacy_corner_normals=True).normal,
index=index(),
)
source_normal = switch(
input_type=Switch.InputType.VECTOR,
switch=use_face_domain,
false=switch(input_type=Switch.InputType.VECTOR, switch=use_edge_domain, false=point_normal, true=edge_normal),
true=face_normal,
)
normal_aligned_rotation = align_rotation_to_vector(axis="Z", pivot_axis="AUTO", factor=1.0, vector=source_normal)
normal_parts = separate_xyz(vector=source_normal)
global_reference_nudge = switch(
input_type=Switch.InputType.VECTOR,
switch=math(operation=Math.Operation.GREATER_THAN, value=(normal_parts.y, 0.5)),
false=(0.0, 0.0, 0.0),
true=(9.999999747378752e-06, 0.0, 0.0),
)
global_reference = combine_xyz(x=global_reference_nudge, y=0.0, z=-1.0)
self_info = object_info(transform_space="ORIGINAL", object=self_object(), as_instance=False)
object_space_reference = transform_point(vector=global_reference, transform=invert_matrix(matrix=self_info.transform).matrix)
reference_vector = switch(input_type=Switch.InputType.VECTOR, switch=align_to_global, false=global_reference, true=object_space_reference)
tangent_vector = vector_math(operation=VectorMath.Operation.CROSS_PRODUCT, vector=(source_normal, reference_vector))
tangent_aligned_rotation = align_rotation_to_vector(
axis="X",
pivot_axis="AUTO",
rotation=normal_aligned_rotation,
factor=1.0,
vector=tangent_vector,
)
local_spin = combine_xyz(x=0.0, y=0.0, z=math(operation=Math.Operation.RADIANS, value=rotation))
instance_rotation = rotate_rotation(rotation_space="LOCAL", rotation=tangent_aligned_rotation, rotate_by=local_spin)
rotated_position = vector_rotate(
rotation_type="EULER_XYZ",
invert=False,
vector=placed_position,
center=source_position,
rotation=instance_rotation,
)
transformed_instances = set_instance_transform(
instances=value_instances,
transform=combine_transform(
translation=rotated_position,
rotation=instance_rotation,
scale=combine_xyz(x=scale, y=scale, z=scale),
),
)
realized_digits = realize_instances(realize_to_point_domain=True, geometry=transformed_instances, realize_all=True, depth=0)
last_display_slot = math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, 1.0))
is_decimal_slot = boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(
compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=digit_indexed.int_field,
b=last_display_slot,
epsilon=0.0010000000474974513,
),
math(operation=Math.Operation.GREATER_THAN, value=(precision, 0.0)),
),
)
is_negative_slot = boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(
is_negative,
compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=digit_indexed.int_field,
b=natural_digit_count,
epsilon=0.0010000000474974513,
),
),
)
forced_visible_digit = boolean_math(
operation=BooleanMath.Operation.OR,
boolean=(
boolean_math(
operation=BooleanMath.Operation.OR,
boolean=(
math(operation=Math.Operation.LESS_THAN, value=(digit_indexed.int_field, precision)),
math(
operation=Math.Operation.GREATER_THAN,
value=(
whole_part,
math(
operation=Math.Operation.SUBTRACT,
value=(
math(
operation=Math.Operation.POWER,
value=(10.0, math(operation=Math.Operation.SUBTRACT, value=(digit_indexed.int_field, precision))),
),
1.0,
),
),
),
),
),
),
boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(
compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.FLOAT,
mode=Compare.Mode.ELEMENT,
a=whole_part,
b=0.0,
epsilon=0.0010000000474974513,
),
compare(
operation=Compare.Operation.EQUAL,
data_type=Compare.DataType.INT,
mode=Compare.Mode.ELEMENT,
a=digit_indexed.int_field,
b=precision,
),
),
),
),
)
leading_zero_slot = boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(
boolean_math(
operation=BooleanMath.Operation.AND,
boolean=(boolean_math(operation=BooleanMath.Operation.NOT, boolean=is_negative), leading_zeros),
),
math(
operation=Math.Operation.LESS_THAN,
value=(math(operation=Math.Operation.SUBTRACT, value=(digit_indexed.int_field, precision)), digits_before_decimal),
),
),
)
digit_visible = boolean_math(operation=BooleanMath.Operation.OR, boolean=(forced_visible_digit, leading_zero_slot))
display_capacity_exceeded = math(
operation=Math.Operation.GREATER_THAN,
value=(natural_digit_count, math(operation=Math.Operation.ADD, value=(digits_before_decimal, precision))),
)
fraction_part = math(
operation=Math.Operation.ROUND,
value=(
math(
operation=Math.Operation.MULTIPLY,
value=(
math(operation=Math.Operation.FRACT, value=absolute_value),
math(operation=Math.Operation.POWER, value=(10.0, precision)),
),
)
),
)
next_digit = vg_next_digit(
whole_part=whole_part,
fraction_part=fraction_part,
max_precision=precision,
position=digit_indexed.int_field,
)
digit_code = switch(input_type=Switch.InputType.FLOAT, switch=display_capacity_exceeded, false=next_digit, true=10.0)
visible_digit_code = switch(
input_type=Switch.InputType.INT,
switch=digit_visible,
false=0,
true=math(operation=Math.Operation.ADD, value=(digit_code, 1.0)),
)
negative_digit_code = switch(input_type=Switch.InputType.INT, switch=is_negative_slot, false=visible_digit_code, true=11)
final_digit_code = switch(input_type=Switch.InputType.INT, switch=is_decimal_slot, false=negative_digit_code, true=12)
hidden_segments = vg_delete_segments(digit=final_digit_code, segment_position=glyph_faces.int_field)
visible_segments = delete_geometry(mode=DeleteGeometry.Mode.ALL, domain=DeleteGeometry.Domain.FACE, geometry=realized_digits, selection=hidden_segments)
return {"Value": set_material(geometry=visible_segments, material=material)}
def _finalize_groups():
import bpy
@ -330,6 +872,7 @@ def _finalize_groups():
"VG Create Segment",
"VG Seven Segments",
"VG Delete Segments",
"VG Field Value",
):
group = bpy.data.node_groups.get(name)
if not group:

View file

@ -0,0 +1,189 @@
"""Geometry Script recreations from Shriinivas/paramnpolareq.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
def _accumulated_parameter(resolution, max_value):
step = math(
operation=Math.Operation.DIVIDE,
value=(max_value, math(operation=Math.Operation.SUBTRACT, value=(resolution, 1.0))),
)
return accumulate_field(
data_type=AccumulateField.DataType.FLOAT,
domain=AccumulateField.Domain.POINT,
value=step,
).trailing
@tree("VG Param Epicycloid X")
def vg_param_epicycloid_x(a: Float = 0.5, b: Float = 0.5, t: Float = 1.0):
ratio_plus_one = math(
operation=Math.Operation.ADD,
value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0),
)
inner = math(
operation=Math.Operation.MULTIPLY,
value=(b, math(operation=Math.Operation.COSINE, value=math(operation=Math.Operation.MULTIPLY, value=(ratio_plus_one, t)))),
)
outer = math(
operation=Math.Operation.MULTIPLY,
value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.COSINE, value=t)),
)
return {"x": math(operation=Math.Operation.SUBTRACT, value=(outer, inner))}
@tree("VG Param Epicycloid Y")
def vg_param_epicycloid_y(a: Float = 0.5, b: Float = 0.5, t: Float = 1.0):
ratio_plus_one = math(
operation=Math.Operation.ADD,
value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0),
)
inner = math(
operation=Math.Operation.MULTIPLY,
value=(b, math(operation=Math.Operation.SINE, value=math(operation=Math.Operation.MULTIPLY, value=(ratio_plus_one, t)))),
)
outer = math(
operation=Math.Operation.MULTIPLY,
value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.SINE, value=t)),
)
return {"y": math(operation=Math.Operation.SUBTRACT, value=(outer, inner))}
@tree("VG Polar Spiral X")
def vg_polar_spiral_x(a: Float = 0.5, b: Float = 0.5, theta: Float = 1.0):
radius = math(operation=Math.Operation.ADD, value=(a, math(operation=Math.Operation.MULTIPLY, value=(b, theta))))
return {"x": math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=theta), radius))}
@tree("VG Polar Spiral Y")
def vg_polar_spiral_y(a: Float = 0.5, b: Float = 0.5, theta: Float = 1.0):
radius = math(operation=Math.Operation.ADD, value=(a, math(operation=Math.Operation.MULTIPLY, value=(b, theta))))
return {"y": math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=theta), radius))}
@tree("VG Archimedes Spiral")
def vg_archimedes_spiral(
resolution: Int = 10,
no_of_rounds: Float = 0.5,
a: Float = 0.5,
b: Float = 0.5,
):
mesh = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=resolution,
start_location=(0.0, 0.0, 0.0),
offset=(0.0, 0.0, 1.0),
)
theta = _accumulated_parameter(
resolution=resolution,
max_value=math(operation=Math.Operation.MULTIPLY, value=(no_of_rounds, TAU)),
)
positioned = mesh.set_position(
position=combine_xyz(
x=vg_polar_spiral_x(a=a, b=b, theta=theta),
y=vg_polar_spiral_y(a=a, b=b, theta=theta),
z=0.0,
)
)
return {"Geometry": positioned}
@tree("VG Epicycloid")
def vg_epicycloid(
resolution: Int = 10,
max_t: Float = 0.5,
a: Float = 0.5,
b: Float = 0.5,
):
mesh = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=resolution,
start_location=(0.0, 0.0, 0.0),
offset=(0.0, 0.0, 1.0),
)
t = _accumulated_parameter(resolution=resolution, max_value=max_t)
positioned = mesh.set_position(
position=combine_xyz(
x=vg_param_epicycloid_x(a=a, b=b, t=t),
y=vg_param_epicycloid_y(a=a, b=b, t=t),
z=0.0,
)
)
return {"Geometry": positioned}
@tree("VG Mirrored Root Spiral")
def vg_mirrored_root_spiral(a: Float = 0.30000001192092896, round_count: Float = 5.0, resolution: Float = 500.0):
mesh = mesh_line(
mode=MeshLine.Mode.OFFSET,
count_mode=MeshLine.CountMode.TOTAL,
count=resolution,
start_location=(0.0, 0.0, 0.0),
offset=(0.0, 0.0, 1.0),
)
theta = _accumulated_parameter(
resolution=resolution,
max_value=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.MULTIPLY, value=(round_count, 3.1415927410125732)), 2.0)),
)
radius = math(operation=Math.Operation.MULTIPLY, value=(a, math(operation=Math.Operation.POWER, value=(theta, 0.5))))
x = math(operation=Math.Operation.MULTIPLY, value=(radius, math(operation=Math.Operation.COSINE, value=theta)))
y = math(operation=Math.Operation.MULTIPLY, value=(radius, math(operation=Math.Operation.SINE, value=theta)))
forward = mesh.set_position(position=combine_xyz(x=x, y=y, z=0.0))
mirrored = mesh.set_position(
position=combine_xyz(
x=math(operation=Math.Operation.MULTIPLY, value=(-1.0, x)),
y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, y)),
z=0.0,
)
)
return {"Geometry": join_geometry(geometry=[mirrored, forward])}
def _finalize_groups():
import bpy
groups = []
for name in (
"VG Param Epicycloid X",
"VG Param Epicycloid Y",
"VG Polar Spiral X",
"VG Polar Spiral Y",
"VG Archimedes Spiral",
"VG Epicycloid",
"VG Mirrored Root Spiral",
):
group = bpy.data.node_groups.get(name)
if group:
group.use_fake_user = True
groups.append(group)
return groups
_finalize_groups()
if __name__ == "__main__":
for group in _finalize_groups():
print(
"VG_PARAMNPOLAREQ_GROUP",
group.name,
len(group.nodes),
len(group.links),
sorted(set(node.bl_idname for node in group.nodes)),
)

View file

@ -39,10 +39,23 @@ The completed pie chart translation is
`docs/research/translations/shriinivas-piechart.md`. It covers `NodeGroup` and
`Pie Chart` from `piechart.blend`.
The completed field-value helper translation is
The completed field-value translation is
`docs/research/translations/shriinivas-fieldvalue-digit-at.md`. It covers
`Create Decimal`, `Create Segment`, `Seven Segments`, `Delete Segments`,
`Next Digit`, and `Digit At` from `fieldvalue.blend`.
`Next Digit`, `Digit At`, and the full `Field Value` display graph from
`fieldvalue.blend`. The full graph is verified on a default-route float-value
case with matching evaluated geometry.
The completed parametric/polar translation is
`docs/research/translations/shriinivas-paramnpolareq.md`. It covers scalar
equation helpers, `Archimedes Spiral`, `Epicycloid`, and a mirrored root spiral
from `paramnpolareq.blend`.
The CurveToMeshUV utility sample has started at
`docs/research/translations/quellenform-curve-to-mesh-uv.md`. So far it covers
the small `Auto Smooth` helper from the quellenform file. The main
`Curve to Mesh UV` group is a 126-node utility quarry and should be staged into
subcontracts rather than translated as one heroic wall.
`docs/research/procedural-doctrine.md` now stores the current spatial reasoning
playbook for mapping geometric ideas to Geometry Script. Doctrine entries should
@ -56,7 +69,8 @@ orchestration; Geometry Script emits inspectable Geometry Nodes groups; `bpy`
handles Blender scene machinery and evaluated evidence.
The repo-local Geometry Script clone has a Blender 5.1 patch for nested `@tree`
group reuse plus a ported `nodes_to_script` converter. See
group reuse, a ported `nodes_to_script` converter, and a menu-socket default
tolerance patch needed by Blender 5 menu inputs. See
`docs/research/geometry-script-fork-notes.md`.
Run `.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is
missing in a fresh workspace.
@ -68,10 +82,11 @@ The GameCult fork is intentionally pruned to `main` plus VibeGeometry-owned
tooling branches. Upstream branch clutter should not be copied forward unless a
future patch actually needs it.
The current next action is to use the generated
`experiments/inspection/nodes-to-script-output/field_value.py` draft as a map
for a cleaned `VG Field Value` translation. `Geometry Nodes Group.002` is a
wrapper around the full `Field Value` group rather than an independent helper.
The current next action is to continue the translation corpus with either the
main CurveToMeshUV utility staged into subcontracts or a scattering/instancing
sample. Keep `docs/research/procedural-doctrine.md` compact; put
sample-specific graph maps and verification details in
`docs/research/translations/`.
## Important Invariants

View file

@ -0,0 +1,31 @@
From 6fbebb8ff2ae13a97b7b87c7a25cf14d72825c07 Mon Sep 17 00:00:00 2001
From: Meta Vens <rwvens@hotmail.com>
Date: Mon, 4 May 2026 15:51:38 +0100
Subject: [PATCH] Tolerate Blender menu socket defaults
---
api/tree.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/api/tree.py b/api/tree.py
index b2250a9..a787091 100644
--- a/api/tree.py
+++ b/api/tree.py
@@ -115,8 +115,12 @@ def tree(name):
node_input = node_group.interface.new_socket(socket_type=arg[1][0].socket_type, name=input_name, in_out='INPUT')
else:
node_input = node_group.inputs.new(arg[1][0].socket_type, input_name)
- if arg[1][1] != inspect.Parameter.empty:
- node_input.default_value = arg[1][1]
+ if arg[1][1] != inspect.Parameter.empty and arg[1][1] is not None:
+ try:
+ node_input.default_value = arg[1][1]
+ except TypeError:
+ if getattr(node_input, "bl_socket_idname", "") != "NodeSocketMenu":
+ raise
if arg[1][2] is not None:
if arg[1][2] not in builder_inputs:
builder_inputs[arg[1][2]] = signature.parameters[arg[1][2]].annotation()
--
2.53.0.windows.3

View file

@ -22,3 +22,5 @@
{"ts": "2026-05-04T14:10:09+00:00", "type": "tooling", "status": "accepted", "note": "Ported upstream Geometry Script nodes_to_script prototype onto GameCult fork branch vibegeometry/blender-5-nodes-to-script. Added callable convert_node_tree(tree), UI operator registration, patch persistence, and a Blender smoke harness. In Blender 5.1.1 the converter emits compiling draft scripts for Create Decimal, Digit At, Next Digit, Seven Segments, Delete Segments, and the 176-node Field Value group; generated output is a bootstrap draft requiring cleanup and behavioral verification."}
{"ts": "2026-05-04T14:32:43+00:00", "type": "doctrine", "status": "accepted", "note": "nodes_to_script output is evidence and scaffolding, not style guidance. Use it to recover wiring/defaults/socket order and hidden graph structure, then rewrite into doctrine-quality Geometry Script with spatial names, intentional decomposition, and behavioral verification."}
{"ts": "2026-05-04T14:35:46+00:00", "type": "doctrine", "status": "accepted", "note": "Updated VibeGeometry priors: Python is the authoring and orchestration layer, Geometry Script is the inspectable node-graph emission surface, bpy handles Blender scene/render/evaluation machinery, and external Python tools handle inspection, converter drafts, verification, docs, and corpus evidence. Do not reason as if the Geometry Script DSL is the whole toolchain."}
{"ts": "2026-05-04T14:54:04+00:00", "type": "implementation", "status": "ok", "note": "Completed and verified full VG Field Value translation, including Blender 5 capture-item helper and Geometry Script menu-socket default tolerance patch. Added and verified paramnpolareq translations for equation helpers, Archimedes spiral, epicycloid, and mirrored root spiral."}
{"ts": "2026-05-04T14:59:36+00:00", "type": "implementation", "status": "ok", "note": "Started CurveToMeshUV utility sample by translating and verifying Auto Smooth; main Curve to Mesh UV remains a staged utility-quarry target rather than a one-pass translation."}

View file

@ -91,8 +91,8 @@ 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 inspected Shriinivas cartesian, pie chart, and field-value helper/chassis/mask/branch graph families. The repo-local Geometry Script fork now includes a Blender 5.1 nodes_to_script converter port that emits compiling drafts for tested source groups.
next_action: Use the generated `experiments/inspection/nodes-to-script-output/field_value.py` draft as a map for a cleaned `VG Field Value` translation.
summary: VibeGeometry has an initial persistence scaffold plus verified Geometry Script translations for inspected Shriinivas cartesian, pie chart, field-value full display, parametric/polar equation graphs, and the first CurveToMeshUV utility helper. The repo-local Geometry Script fork includes a Blender 5.1 nodes_to_script converter port plus nested-group and menu-socket fixes needed by these translations.
next_action: Continue the translation corpus with the main CurveToMeshUV utility staged into subcontracts, or pivot to a scattering/instancing sample; keep doctrine compact and put graph-specific autopsies under `docs/research/translations/`.
remotes:
vibegeometry: https://github.com/GameCult/VibeGeometry
geometry_script_fork: https://github.com/GameCult/geometry-script

View file

@ -0,0 +1,116 @@
"""Compare small CurveToMeshUV utility groups 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" / "quellenform_curve_to_mesh_uv.py"
AUTO_SMOOTH_CASE = {"Angle": 0.75}
def _load_translation() -> None:
spec = importlib.util.spec_from_file_location("vg_curve_to_mesh_uv_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_auto_smooth_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
wrapper = _new_geometry_group(name)
nodes = wrapper.nodes
links = wrapper.links
cube = nodes.new("GeometryNodeMeshCube")
_socket_by_name(cube.inputs, "Size").default_value = (1.0, 1.0, 1.0)
group_node = nodes.new("GeometryNodeGroup")
group_node.node_tree = source_group
links.new(_socket_by_name(cube.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry"))
for socket in group_node.inputs:
if socket.name in inputs:
socket.default_value = inputs[socket.name]
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _evaluated_mesh_summary(group: bpy.types.GeometryNodeTree):
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:
vertices = [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
polygons = [polygon.use_smooth for polygon in evaluated_mesh.polygons]
return {
"vertices": vertices,
"polygon_smooth": polygons,
}
finally:
evaluated.to_mesh_clear()
def main() -> int:
_load_translation()
source = _evaluated_mesh_summary(
_build_auto_smooth_wrapper("VG Verify Source Auto Smooth", bpy.data.node_groups["Auto Smooth"], AUTO_SMOOTH_CASE)
)
translated = _evaluated_mesh_summary(
_build_auto_smooth_wrapper("VG Verify Translated Auto Smooth", bpy.data.node_groups["VG Auto Smooth"], AUTO_SMOOTH_CASE)
)
max_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source["vertices"]), sorted(translated["vertices"]))),
default=0.0,
)
result = {
"case": "auto_smooth_cube",
"source_vertex_count": len(source["vertices"]),
"translated_vertex_count": len(translated["vertices"]),
"max_sorted_vertex_delta": max_delta,
"source_smooth_faces": sum(source["polygon_smooth"]),
"translated_smooth_faces": sum(translated["polygon_smooth"]),
}
result["ok"] = (
result["source_vertex_count"] == result["translated_vertex_count"]
and max_delta <= 1e-6
and source["polygon_smooth"] == translated["polygon_smooth"]
)
payload = {"ok": result["ok"], "results": [result]}
print("VG_CURVE_TO_MESH_UV_TRANSLATION_BEHAVIOR " + json.dumps(payload, sort_keys=True))
return 0 if result["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -55,6 +55,22 @@ SEVEN_SEGMENTS_CASE = {
"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)
@ -150,6 +166,35 @@ def _build_geometry_wrapper(name: str, source_group: bpy.types.GeometryNodeTree,
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)
@ -265,6 +310,25 @@ def main() -> int:
"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(

View file

@ -0,0 +1,182 @@
"""Compare Shriinivas param/polar equation groups 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_paramnpolareq.py"
SCALAR_CASE = {"a": 0.8, "b": 0.35, "t": 1.25, "theta": 1.25}
ARCHIMEDES_CASE = {"Resolution": 64, "No of Rounds": 2.5, "a": 0.25, "b": 0.15}
EPICYCLOID_CASE = {"Resolution": 80, "maxT": 12.0, "a": 0.8, "b": 0.3}
ROOT_SPIRAL_CASE = {"a": 0.2, "Round Count": 4.0, "Resolution": 96.0}
def _load_translation() -> None:
spec = importlib.util.spec_from_file_location("vg_paramnpolareq_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 _normalized_key(name: str) -> str:
return name.lower().replace(" ", "")
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_scalar_position_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()}
normalized_inputs = {_normalized_key(key): 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()]
elif _normalized_key(socket.name) in normalized_inputs:
socket.default_value = normalized_inputs[_normalized_key(socket.name)]
line = nodes.new("GeometryNodeMeshLine")
line.mode = "OFFSET"
line.count_mode = "TOTAL"
_socket_by_name(line.inputs, "Count").default_value = 1
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
vector = nodes.new("ShaderNodeCombineXYZ")
links.new(_socket_by_name(group_node.outputs, output), _socket_by_name(vector.inputs, "X"))
set_position_node = nodes.new("GeometryNodeSetPosition")
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(set_position_node.inputs, "Geometry"))
links.new(_socket_by_name(vector.outputs, "Vector"), _socket_by_name(set_position_node.inputs, "Position"))
group_output = nodes.new("NodeGroupOutput")
links.new(_socket_by_name(set_position_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
return wrapper
def _build_geometry_wrapper(name: str, source_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 = source_group
lower_inputs = {key.lower(): value for key, value in inputs.items()}
normalized_inputs = {_normalized_key(key): 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()]
elif _normalized_key(socket.name) in normalized_inputs:
socket.default_value = normalized_inputs[_normalized_key(socket.name)]
group_output = nodes.new("NodeGroupOutput")
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 _compare_vertices(case: str, source_group: str, translated_group: str, inputs: dict[str, object]):
source_vertices = _evaluated_vertices(
_build_geometry_wrapper(f"VG Verify Source {case}", bpy.data.node_groups[source_group], inputs)
)
translated_vertices = _evaluated_vertices(
_build_geometry_wrapper(f"VG Verify Translated {case}", bpy.data.node_groups[translated_group], inputs)
)
max_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source_vertices), sorted(translated_vertices))),
default=0.0,
)
return {
"case": case,
"source_vertex_count": len(source_vertices),
"translated_vertex_count": len(translated_vertices),
"max_sorted_vertex_delta": max_delta,
"ok": len(source_vertices) == len(translated_vertices) and max_delta <= 1e-6,
}
def main() -> int:
_load_translation()
results = []
scalar_pairs = [
("epicycloid_x", "NodeGroup", "VG Param Epicycloid X", "x", "x", {"a": SCALAR_CASE["a"], "b": SCALAR_CASE["b"], "t": SCALAR_CASE["t"]}),
("epicycloid_y", "NodeGroup.001", "VG Param Epicycloid Y", "x", "y", {"a": SCALAR_CASE["a"], "b": SCALAR_CASE["b"], "t": SCALAR_CASE["t"]}),
("polar_x", "NodeGroup.002", "VG Polar Spiral X", "x", "x", {"a": SCALAR_CASE["a"], "b": SCALAR_CASE["b"], "theta": SCALAR_CASE["theta"]}),
("polar_y", "NodeGroup.003", "VG Polar Spiral Y", "y", "y", {"a": SCALAR_CASE["a"], "b": SCALAR_CASE["b"], "theta": SCALAR_CASE["theta"]}),
]
for case, source_name, translated_name, source_output, translated_output, inputs in scalar_pairs:
source_vertices = _evaluated_vertices(
_build_scalar_position_wrapper(f"VG Verify Source {case}", bpy.data.node_groups[source_name], inputs, source_output)
)
translated_vertices = _evaluated_vertices(
_build_scalar_position_wrapper(f"VG Verify Translated {case}", bpy.data.node_groups[translated_name], inputs, translated_output)
)
max_delta = max(
(math.dist(a, b) for a, b in zip(sorted(source_vertices), sorted(translated_vertices))),
default=0.0,
)
results.append(
{
"case": case,
"source_vertex": source_vertices[0] if source_vertices else None,
"translated_vertex": translated_vertices[0] if translated_vertices else None,
"max_sorted_vertex_delta": max_delta,
"ok": len(source_vertices) == len(translated_vertices) and max_delta <= 1e-6,
}
)
results.append(_compare_vertices("archimedes_spiral", "Archimedes Spiral", "VG Archimedes Spiral", ARCHIMEDES_CASE))
results.append(_compare_vertices("epicycloid", "Epicycloid", "VG Epicycloid", EPICYCLOID_CASE))
results.append(_compare_vertices("mirrored_root_spiral", "Geometry Nodes Group.002", "VG Mirrored Root Spiral", ROOT_SPIRAL_CASE))
ok = all(result["ok"] for result in results)
print("VG_PARAMNPOLAREQ_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())