From fdda6184e8c089e3ee85fd831486a78a7a7aebdb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 4 May 2026 20:01:58 +0100 Subject: [PATCH] Add IRCSS gothic cone translation --- docs/research/geonodes-translation-corpus.md | 13 +- docs/research/procedural-doctrine.md | 43 ++++++ .../translations/ircss-french-houses.md | 42 +++++- .../geometry_script/ircss_french_houses.py | 136 +++++++++++++++++- notes/fresh-workspace-handoff.md | 13 +- state/evidence.jsonl | 1 + state/map.yaml | 2 +- ...verify_ircss_french_houses_translations.py | 89 +++++++++++- 8 files changed, 320 insertions(+), 19 deletions(-) diff --git a/docs/research/geonodes-translation-corpus.md b/docs/research/geonodes-translation-corpus.md index fe7a891..b7e5ea1 100644 --- a/docs/research/geonodes-translation-corpus.md +++ b/docs/research/geonodes-translation-corpus.md @@ -258,12 +258,15 @@ Completed Blender raycast minigame foothold: Completed IRCSS French Houses foothold: - `docs/research/translations/ircss-french-houses.md` -- Source group: `MakeSpiral` -- Verification: output curve converted to points matches source with ordered - max vertex delta `0.0`. +- Source groups: `MakeSpiral`, `PointyGothicCone` +- Verification: spiral output curve converted to points matches source with + ordered max vertex delta `0.0`; plain and ornamented gothic cone cases match + source evaluated mesh vertices with sorted max vertex delta `0.0`. - Translation value: procedural-city corpus triage, spiral architectural - spines, and explicit blockers for repeat-zone feedback, repeat `Top` outputs, - foreach zones, gizmo nodes, and converter duplicate-key failures. + spines, roof-cap ornament grown from structural ribs, captured source points + through instancing, and explicit blockers for repeat-zone feedback, repeat + `Top` outputs, foreach zones, gizmo nodes, and converter duplicate-key + failures. Inspected but deferred official demo files: diff --git a/docs/research/procedural-doctrine.md b/docs/research/procedural-doctrine.md index 515e578..c88a8f6 100644 --- a/docs/research/procedural-doctrine.md +++ b/docs/research/procedural-doctrine.md @@ -547,6 +547,49 @@ copies and become individual geometry you can bend, cut, or shade. Verification cue: test more than one seed/control set. Scatter graphs can match counts at one setting while density, scale, or captured-root behavior is wrong. +## Ornament Grows On Structure + +Reach for this when a form has a big readable architectural body plus smaller +detail that should cling to that body: gothic caps, roof ribs, crenellations, +trim, bolts, vines, scales, and surface greebles. + +Mental move: build the host silhouette first, capture the host features that +detail needs, then sample or convert those features into stations for the +ornament. Do not scatter decorative pieces in empty space and hope they look +attached. + +```python +point_positions = _capture_attribute_item("VECTOR", "POINT", geometry, position(), "source position") +cone_seed = cylinder(fill_type="NGON", vertices=6, side_segments=1, fill_segments=1, radius=1.0, depth=1.0) +tapered_cone = scale_elements( + domain="FACE", + geometry=set_position(geometry=cone_seed.mesh, offset=(0.0, 0.0, 0.5)), + selection=cone_seed.top, + scale=0.10000002384185791, + scale_mode="Uniform", +) +edge_flags = _capture_attribute_item("BOOLEAN", "EDGE", tapered_cone, cap_or_body_edge, "edge trim") +cone_instances = instance_on_points(points=point_positions.geometry, instance=cone_with_skirt.mesh, scale=cone_scale) +realized_cones = realize_instances(geometry=cone_instances, realize_all=True, depth=0) + +detail_edges = delete_geometry(mode="ALL", domain="EDGE", geometry=realized_cones, selection=edge_flags.edge_trim) +detail_points = resample_curve( + keep_last_segment=True, + curve=mesh_to_curve(mode="EDGES", mesh=detail_edges), + mode="Length", + length=gothic_detail_density, +) +details = instance_on_points(points=detail_points, instance=stretched_detail, rotation=detail_rotation, scale=detail_scale) +``` + +Metaphor in use: the big form is scaffolding, and ornament is ivy on that +scaffolding. If the scaffold is not built first, the ivy floats there looking +confident and wrong. + +Verification cue: test the plain host and the ornamented host separately. The +IRCSS `PointyGothicCone` translation verifies both `SpawnDetails=False` and +`SpawnDetails=True` against evaluated source geometry. + ## Constraint Helpers Are Bumpers Reach for constraint helpers when a procedural system has a small physical rule diff --git a/docs/research/translations/ircss-french-houses.md b/docs/research/translations/ircss-french-houses.md index 52ae411..cc4ec0f 100644 --- a/docs/research/translations/ircss-french-houses.md +++ b/docs/research/translations/ircss-french-houses.md @@ -1,12 +1,13 @@ -# Translation: IRCSS French Houses Foothold +# Translation: IRCSS French Houses Footholds ## Source - Repository: https://github.com/IRCSS/Blender-Geometry-Node-French-Houses - Source file: `GeometryNodesFrenchHous.blend` -- Source group translated in this pass: +- Source groups translated so far: - `MakeSpiral` + - `PointyGothicCone` - Geometry Script recreation: `examples/geometry_script/ircss_french_houses.py` @@ -28,7 +29,7 @@ parts, stalls, cliffs, roads, rivers, vegetation, banners, windows, doors, and roof systems. This is the right quarry for procedural-city doctrine, but the first accepted bite has to be small enough to verify. -## First Accepted Graph +## Accepted Graph: MakeSpiral Source group: `MakeSpiral` @@ -63,9 +64,35 @@ Results from Blender 5.1.1: | Case | Source vertices | Translated vertices | Delta | Status | | --- | ---: | ---: | ---: | --- | | MakeSpiral curve points | 200 | 200 | 0.0 | accepted | +| PointyGothicCone plain | 36 | 36 | 0.0 | accepted | +| PointyGothicCone with details | 138 | 138 | 0.0 | accepted | -The verifier converts the output curve to evaluated points and compares ordered -vertex positions. +## Accepted Graph: PointyGothicCone + +Source group: `PointyGothicCone` + +Geometry Script group: `VG Pointy Gothic Cone` + +Mechanism: + +- Capture each input point position before instancing starts. +- Build a tapered six-sided cone seed from a cylinder. +- Capture an edge-domain trim mask that marks cone cap/body edges. +- Instance one scaled cone at each input point. +- When `SpawnDetails` is enabled, delete the marked cone edges, convert the + remaining ribs to curves, resample them into detail stations, and instance + small tapered five-sided spikes along those ribs. +- Orient each detail spike toward the horizontal direction from the source + point to the current rib point. + +Metaphor: the input points are roof pegs. The main cone is the roof cap hung on +each peg; the optional details grow from the cap ribs after the cap is already +real enough to have edges. Build the big readable silhouette first, then let +ornament use that structure. + +The spiral case converts the output curve to evaluated points and compares +ordered vertex positions. Cone cases feed identical synthetic point meshes into +the source and translation, then compare sorted evaluated vertices. ## Blockers Exposed @@ -90,6 +117,11 @@ vertex positions. - City-scale graphs are hierarchies of organs. Start with reusable spines, tiles, rails, façade parts, and topology generators before translating a thousand-node composer. +- Ornament often needs a structural host. `PointyGothicCone` first creates the + readable cap silhouette, then derives detail placement from the cap's own + realized ribs. +- Captures are not optional bookkeeping when instancing starts. The original + point position and cone edge mask both have to survive later domain changes. - Converter drafts are still useful for triage: they quickly separate clean helpers, repeat-zone blockers, foreach-zone blockers, gizmo-specific groups, and false symbol names. diff --git a/examples/geometry_script/ircss_french_houses.py b/examples/geometry_script/ircss_french_houses.py index 7e374bd..ae6500b 100644 --- a/examples/geometry_script/ircss_french_houses.py +++ b/examples/geometry_script/ircss_french_houses.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]) + return OutputsList({"geometry": Type(node.outputs[0]), name.lower().replace(" ", "_"): Type(node.outputs[1])}) + + @tree("VG Make Spiral") def vg_make_spiral( frequency: Float = 12.0, @@ -40,11 +55,130 @@ def vg_make_spiral( return {"Geometry": set_position(geometry=samples, position=combine_xyz(x=x, y=y, z=z), offset=(0.0, 0.0, 0.0))} +@tree("VG Pointy Gothic Cone") +def vg_pointy_gothic_cone( + geometry: Geometry, + column_radius: Float = 0.3, + peak_height: Float = 1.0, + gothic_detail_density: Float = 0.6, + detail_scale: Float = 0.1, + spawn_details: Bool = True, + rotation: Rotation = (0.0, 0.0, 0.0), +): + point_positions = _capture_attribute_item("VECTOR", "POINT", geometry, position(), "source position") + + cone_seed = cylinder(fill_type="NGON", vertices=6, side_segments=1, fill_segments=1, radius=1.0, depth=1.0) + raised_cone = set_position(geometry=cone_seed.mesh, offset=(0.0, 0.0, 0.5)) + tapered_cone = scale_elements( + domain="FACE", + geometry=raised_cone, + selection=cone_seed.top, + scale=0.10000002384185791, + scale_mode="Uniform", + axis=(1.0, 0.0, 0.0), + ) + + not_side = boolean_math(operation="NOT", boolean=cone_seed.side) + cap_or_body_edge = boolean_math( + operation="OR", + boolean=(boolean_math(operation="OR", boolean=(not_side, cone_seed.top)), cone_seed.bottom), + ) + cone_edge_flags = _capture_attribute_item("BOOLEAN", "EDGE", tapered_cone, cap_or_body_edge, "edge trim") + + scaled_cone_position = vector_math( + operation="MULTIPLY", + vector=(position(), combine_xyz(x=1.0, y=1.0, z=peak_height)), + ) + shaped_cone = set_position(geometry=cone_edge_flags.geometry, position=scaled_cone_position, offset=(0.0, 0.0, 0.0)) + + skirt_depth = math( + operation="MINIMUM", + value=(math(operation="MULTIPLY", value=(peak_height, 0.20000000298023224)), 1.0), + ) + cone_with_skirt = extrude_mesh( + mode="FACES", + mesh=shaped_cone, + selection=cone_seed.bottom, + offset_scale=skirt_depth, + individual=False, + ) + + cone_scale = combine_xyz(x=column_radius, y=column_radius, z=1.0) + cone_instances = instance_on_points( + points=point_positions.geometry, + instance=cone_with_skirt.mesh, + pick_instance=False, + rotation=rotation, + scale=cone_scale, + ) + realized_cones = realize_instances( + realize_to_point_domain=True, + geometry=cone_instances, + realize_all=True, + depth=0, + ) + + details_disabled = boolean_math(operation="NOT", boolean=spawn_details) + cone_for_detail_edges = delete_geometry(mode="ALL", domain="POINT", geometry=realized_cones, selection=details_disabled) + cone_ribs = delete_geometry(mode="ALL", domain="EDGE", geometry=cone_for_detail_edges, selection=cone_edge_flags.edge_trim) + rib_curve = mesh_to_curve(mode="EDGES", mesh=cone_ribs) + detail_points = resample_curve( + keep_last_segment=True, + curve=rib_curve, + mode="Length", + count=5, + length=gothic_detail_density, + ) + + from_source_point = vector_math(operation="SUBTRACT", vector=(position(), point_positions.source_position)) + z_from_source = separate_xyz(vector=from_source_point) + below_peak = compare( + operation="LESS_THAN", + data_type="FLOAT", + mode="ELEMENT", + a=math(operation="SUBTRACT", value=(math(operation="ABSOLUTE", value=z_from_source.z), peak_height)), + b=-0.09999924898147583, + ) + + detail_seed = cylinder(fill_type="NGON", vertices=5, side_segments=1, fill_segments=1, radius=1.0, depth=1.0) + detail_taper = scale_elements( + domain="FACE", + geometry=detail_seed.mesh, + selection=detail_seed.top, + scale=0.20000001788139343, + scale_mode="Uniform", + axis=(1.0, 0.0, 0.0), + ) + raised_detail = set_position(geometry=detail_taper, offset=(0.0, 0.0, 0.30000001192092896)) + stretched_detail = set_position( + geometry=raised_detail, + position=vector_math(operation="MULTIPLY", vector=(position(), (1.0, 1.0, 2.0))), + offset=(0.0, 0.0, 0.0), + ) + horizontal_rib_direction = vector_math(operation="MULTIPLY", vector=(from_source_point, (1.0, 1.0, 0.0))) + detail_rotation = align_rotation_to_vector(axis="Z", pivot_axis="AUTO", factor=1.0, vector=horizontal_rib_direction) + detail_instances = instance_on_points( + points=detail_points, + selection=below_peak, + instance=stretched_detail, + pick_instance=False, + rotation=detail_rotation, + scale=detail_scale, + ) + realized_details = realize_instances( + realize_to_point_domain=True, + geometry=detail_instances, + realize_all=True, + depth=0, + ) + return {"Geometry": join_geometry(geometry=[realized_cones, realized_details])} + + def _finalize_groups(): import bpy groups = [] - for name in ("VG Make Spiral",): + for name in ("VG Make Spiral", "VG Pointy Gothic Cone"): group = bpy.data.node_groups.get(name) if group: group.use_fake_user = True diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 64a76d1..977244a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -39,14 +39,15 @@ Accepted corpus families so far: - Quellenform `CurveToMeshUV` utility sample. - Official Blender demo tranche: instance attributes, index-of-nearest, shortest-path labyrinth, and raycast minigame helpers. -- IRCSS French Houses foothold: `MakeSpiral -> VG Make Spiral`, verified with - 200 ordered curve points matching source geometry at max delta `0.0`. +- IRCSS French Houses footholds: `MakeSpiral -> VG Make Spiral` and + `PointyGothicCone -> VG Pointy Gothic Cone`, verified against source + evaluated geometry with max delta `0.0`. Current frontier: IRCSS French Houses city-scale graph work. The accepted -foothold is `MakeSpiral`. Mapped blockers are repeat-zone feedback state, -repeat `Top` boolean outputs, foreach zones, gizmo nodes, `nodes_to_script` -duplicate-key failures around axes-to-rotation, and a rejected empty-output -harness for `WindowBeams`. +footholds are `MakeSpiral` and `PointyGothicCone`. Mapped blockers are +repeat-zone feedback state, repeat `Top` boolean outputs, foreach zones, gizmo +nodes, `nodes_to_script` duplicate-key failures around axes-to-rotation, and a +rejected empty-output harness for `WindowBeams`. Current authoring prior: use the full Python stack. Python structures intent, tables, loops, variants, cleanup, validation, docs, and scene/render diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 13273f7..8c93586 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -28,3 +28,4 @@ {"ts": "2026-05-04T16:56:12+00:00", "type": "implementation", "status": "accepted", "note": "Expanded the verified Geometry Nodes translation corpus with official Blender demo samples: instance attributes grass tuft, index-of-nearest helpers, shortest-path labyrinth, and raycast minigame helpers. All new verifiers pass in Blender 5.1.1 with sorted max vertex delta 0.0 across their harness cases. Deferred official samples are classified as legacy-node, gizmo-node, simulation-zone, or repeat-zone targets rather than padded into doctrine."} {"ts": "2026-05-04T17:15:09+00:00", "type": "research", "status": "accepted", "note": "Started IRCSS French Houses city-scale corpus target. Inspected 108 geometry node groups and verified first foothold MakeSpiral -> VG Make Spiral with 200 ordered curve points matching source at max delta 0.0. Recorded blockers from larger city graphs: repeat-zone feedback state, repeat Top output sockets, foreach zones, gizmo nodes, converter duplicate-key failures, and empty-output harness rejection."} {"ts": "2026-05-04T18:01:03+00:00", "type": "maintenance", "status": "accepted", "note": "Distilled persistent state: handoff now points to corpus docs instead of duplicating completed graph lists; implementation plan reflects post-corpus contract and IRCSS frontier; compaction helper warns on bloated handoff and stale first-corpus planning."} +{"ts": "2026-05-04T19:01:27+00:00", "type": "verification", "status": "accepted", "note": "Added IRCSS PointyGothicCone translation as VG Pointy Gothic Cone; plain and ornamented harness cases match source evaluated geometry with sorted max vertex delta 0.0, proving ornament can be grown from captured structural ribs after instancing."} diff --git a/state/map.yaml b/state/map.yaml index 4d285db..d5f6708 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -91,7 +91,7 @@ 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 local math/utility samples, a first official Blender demo tranche, and an initial IRCSS French Houses city-scale foothold. The IRCSS pass verified MakeSpiral and mapped blockers in larger architectural systems: repeat-zone feedback, repeat Top outputs, foreach zones, gizmo nodes, and converter duplicate-key failures. 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. + summary: VibeGeometry has an initial persistence scaffold plus verified Geometry Script translations for local math/utility samples, a first official Blender demo tranche, and two IRCSS French Houses city-scale footholds. The IRCSS pass verified MakeSpiral as an architectural spine and PointyGothicCone as ornament grown from a structural host, while mapping blockers in larger systems: repeat-zone feedback, repeat Top outputs, foreach zones, gizmo nodes, and converter duplicate-key failures. 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 IRCSS French Houses by choosing the next city-organ target after either fixing repeat-zone/foreach/gizmo toolchain blockers or selecting another non-zone architectural helper with a non-empty behavioral harness. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry diff --git a/tools/verify_ircss_french_houses_translations.py b/tools/verify_ircss_french_houses_translations.py index 1df3c9d..6157273 100644 --- a/tools/verify_ircss_french_houses_translations.py +++ b/tools/verify_ircss_french_houses_translations.py @@ -19,6 +19,32 @@ CASE = { "Height": 3.0, "HeightRadiusGain": 0.25, } +CONE_CASES = [ + { + "case": "pointy_gothic_cone_plain", + "inputs": { + "ColumnRadius": 0.45, + "PeakHeight": 1.4, + "GothicDetailDensity": 0.6, + "DetailScale": 0.1, + "SpawnDetails": False, + }, + "point_count": 2, + "offset": (1.25, 0.0, 0.0), + }, + { + "case": "pointy_gothic_cone_with_details", + "inputs": { + "ColumnRadius": 0.35, + "PeakHeight": 1.2, + "GothicDetailDensity": 0.55, + "DetailScale": 0.08, + "SpawnDetails": True, + }, + "point_count": 1, + "offset": (0.0, 0.0, 0.0), + }, +] def _load_translation() -> None: @@ -78,6 +104,31 @@ def _build_spiral_wrapper(name: str, group: bpy.types.GeometryNodeTree): return wrapper +def _add_mesh_line(nodes, *, count: int, offset): + line = nodes.new("GeometryNodeMeshLine") + line.mode = "OFFSET" + line.count_mode = "TOTAL" + _socket_by_name(line.inputs, "Count").default_value = count + _socket_by_name(line.inputs, "Offset").default_value = offset + return line + + +def _build_pointy_cone_wrapper(name: str, group: bpy.types.GeometryNodeTree, inputs: dict[str, object], point_count: int, offset): + wrapper = _new_geometry_group(name) + nodes = wrapper.nodes + links = wrapper.links + line = _add_mesh_line(nodes, count=point_count, offset=offset) + + group_node = nodes.new("GeometryNodeGroup") + group_node.node_tree = group + _set_inputs(group_node, inputs) + links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry")) + + 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): mesh = bpy.data.meshes.new(group.name + " Input Mesh") obj = bpy.data.objects.new(group.name + " Object", mesh) @@ -111,7 +162,43 @@ def main() -> int: "max_ordered_vertex_delta": max_delta, } result["ok"] = len(source) == len(translated) and max_delta <= 1e-6 - payload = {"ok": result["ok"], "results": [result]} + results = [result] + + for case in CONE_CASES: + source = _evaluated_vertices( + _build_pointy_cone_wrapper( + f"VG Verify Source {case['case']}", + bpy.data.node_groups["PointyGothicCone"], + case["inputs"], + case["point_count"], + case["offset"], + ) + ) + translated = _evaluated_vertices( + _build_pointy_cone_wrapper( + f"VG Verify Translated {case['case']}", + bpy.data.node_groups["VG Pointy Gothic Cone"], + case["inputs"], + case["point_count"], + case["offset"], + ) + ) + max_delta = ( + max((math.dist(a, b) for a, b in zip(sorted(source), sorted(translated))), default=0.0) + if len(source) == len(translated) + else math.inf + ) + results.append( + { + "case": case["case"], + "source_vertex_count": len(source), + "translated_vertex_count": len(translated), + "max_sorted_vertex_delta": max_delta, + "ok": len(source) == len(translated) and max_delta <= 1e-6, + } + ) + + payload = {"ok": all(item["ok"] for item in results), "results": results} print("VG_IRCSS_FRENCH_HOUSES_TRANSLATION_BEHAVIOR " + json.dumps(payload, sort_keys=True)) return 0 if payload["ok"] else 1