Expand verified geometry node corpus
This commit is contained in:
parent
dbf72e1f09
commit
9bc09bb7c2
17 changed files with 1578 additions and 28 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,6 +7,7 @@ state/scratch-compaction-*.md
|
|||
experiments/renders/
|
||||
experiments/blender-cache/
|
||||
experiments/source-blends/
|
||||
experiments/source-repos/
|
||||
experiments/inspection/
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
64
docs/research/translations/quellenform-curve-to-mesh-uv.md
Normal file
64
docs/research/translations/quellenform-curve-to-mesh-uv.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
113
docs/research/translations/shriinivas-paramnpolareq.md
Normal file
113
docs/research/translations/shriinivas-paramnpolareq.md
Normal 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`.
|
||||
66
examples/geometry_script/quellenform_curve_to_mesh_uv.py
Normal file
66
examples/geometry_script/quellenform_curve_to_mesh_uv.py
Normal 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)),
|
||||
)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
189
examples/geometry_script/shriinivas_paramnpolareq.py
Normal file
189
examples/geometry_script/shriinivas_paramnpolareq.py
Normal 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)),
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
116
tools/verify_curve_to_mesh_uv_translations.py
Normal file
116
tools/verify_curve_to_mesh_uv_translations.py
Normal 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())
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
182
tools/verify_paramnpolareq_translations.py
Normal file
182
tools/verify_paramnpolareq_translations.py
Normal 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())
|
||||
Loading…
Reference in a new issue