From 9bc09bb7c2ca1f65fdfc8a473f6b62262686c0e2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 4 May 2026 16:00:47 +0100 Subject: [PATCH] Expand verified geometry node corpus --- .gitignore | 1 + docs/research/geometry-script-fork-notes.md | 7 + docs/research/geonodes-translation-corpus.md | 31 +- docs/research/procedural-doctrine.md | 106 ++++ .../quellenform-curve-to-mesh-uv.md | 64 +++ .../shriinivas-fieldvalue-digit-at.md | 56 +- .../translations/shriinivas-paramnpolareq.md | 113 ++++ .../quellenform_curve_to_mesh_uv.py | 66 +++ .../geometry_script/shriinivas_fieldvalue.py | 543 ++++++++++++++++++ .../shriinivas_paramnpolareq.py | 189 ++++++ notes/fresh-workspace-handoff.md | 29 +- ...olerate-Blender-menu-socket-defaults.patch | 31 + state/evidence.jsonl | 2 + state/map.yaml | 4 +- tools/verify_curve_to_mesh_uv_translations.py | 116 ++++ tools/verify_fieldvalue_translations.py | 64 +++ tools/verify_paramnpolareq_translations.py | 182 ++++++ 17 files changed, 1577 insertions(+), 27 deletions(-) create mode 100644 docs/research/translations/quellenform-curve-to-mesh-uv.md create mode 100644 docs/research/translations/shriinivas-paramnpolareq.md create mode 100644 examples/geometry_script/quellenform_curve_to_mesh_uv.py create mode 100644 examples/geometry_script/shriinivas_paramnpolareq.py create mode 100644 patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch create mode 100644 tools/verify_curve_to_mesh_uv_translations.py create mode 100644 tools/verify_paramnpolareq_translations.py diff --git a/.gitignore b/.gitignore index 34af03b..5b2fa51 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ state/scratch-compaction-*.md experiments/renders/ experiments/blender-cache/ experiments/source-blends/ +experiments/source-repos/ experiments/inspection/ *.blend1 *.blend2 diff --git a/docs/research/geometry-script-fork-notes.md b/docs/research/geometry-script-fork-notes.md index ae865b3..9d2941d 100644 --- a/docs/research/geometry-script-fork-notes.md +++ b/docs/research/geometry-script-fork-notes.md @@ -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 diff --git a/docs/research/geonodes-translation-corpus.md b/docs/research/geonodes-translation-corpus.md index 131b796..2ed2424 100644 --- a/docs/research/geonodes-translation-corpus.md +++ b/docs/research/geonodes-translation-corpus.md @@ -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 diff --git a/docs/research/procedural-doctrine.md b/docs/research/procedural-doctrine.md index 68b43e7..882e0b4 100644 --- a/docs/research/procedural-doctrine.md +++ b/docs/research/procedural-doctrine.md @@ -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 diff --git a/docs/research/translations/quellenform-curve-to-mesh-uv.md b/docs/research/translations/quellenform-curve-to-mesh-uv.md new file mode 100644 index 0000000..50b4dc1 --- /dev/null +++ b/docs/research/translations/quellenform-curve-to-mesh-uv.md @@ -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. diff --git a/docs/research/translations/shriinivas-fieldvalue-digit-at.md b/docs/research/translations/shriinivas-fieldvalue-digit-at.md index b9038b9..940374a 100644 --- a/docs/research/translations/shriinivas-fieldvalue-digit-at.md +++ b/docs/research/translations/shriinivas-fieldvalue-digit-at.md @@ -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. diff --git a/docs/research/translations/shriinivas-paramnpolareq.md b/docs/research/translations/shriinivas-paramnpolareq.md new file mode 100644 index 0000000..bf63601 --- /dev/null +++ b/docs/research/translations/shriinivas-paramnpolareq.md @@ -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`. diff --git a/examples/geometry_script/quellenform_curve_to_mesh_uv.py b/examples/geometry_script/quellenform_curve_to_mesh_uv.py new file mode 100644 index 0000000..98d1003 --- /dev/null +++ b/examples/geometry_script/quellenform_curve_to_mesh_uv.py @@ -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)), + ) diff --git a/examples/geometry_script/shriinivas_fieldvalue.py b/examples/geometry_script/shriinivas_fieldvalue.py index d6d8266..0324d01 100644 --- a/examples/geometry_script/shriinivas_fieldvalue.py +++ b/examples/geometry_script/shriinivas_fieldvalue.py @@ -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: diff --git a/examples/geometry_script/shriinivas_paramnpolareq.py b/examples/geometry_script/shriinivas_paramnpolareq.py new file mode 100644 index 0000000..aaf4462 --- /dev/null +++ b/examples/geometry_script/shriinivas_paramnpolareq.py @@ -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)), + ) diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 606d5a7..61f393c 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch b/patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch new file mode 100644 index 0000000..901fe4a --- /dev/null +++ b/patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch @@ -0,0 +1,31 @@ +From 6fbebb8ff2ae13a97b7b87c7a25cf14d72825c07 Mon Sep 17 00:00:00 2001 +From: Meta Vens +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 + diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 1167fae..14f915d 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."} diff --git a/state/map.yaml b/state/map.yaml index c757038..a8d6b28 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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 diff --git a/tools/verify_curve_to_mesh_uv_translations.py b/tools/verify_curve_to_mesh_uv_translations.py new file mode 100644 index 0000000..9dfcf8a --- /dev/null +++ b/tools/verify_curve_to_mesh_uv_translations.py @@ -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()) diff --git a/tools/verify_fieldvalue_translations.py b/tools/verify_fieldvalue_translations.py index a723ef7..d80c440 100644 --- a/tools/verify_fieldvalue_translations.py +++ b/tools/verify_fieldvalue_translations.py @@ -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( diff --git a/tools/verify_paramnpolareq_translations.py b/tools/verify_paramnpolareq_translations.py new file mode 100644 index 0000000..48c4e99 --- /dev/null +++ b/tools/verify_paramnpolareq_translations.py @@ -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())