From d580624c91dc0721033a64b58968768ecbc39a02 Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Sat, 22 Aug 2026 11:15:47 +0100 Subject: [PATCH] Add button to subdivide the next N worst cells. Julian asked for progressive cell splits under UI control after seeing the first auto splits on main. Assisted-by: Cursor --- barmesh/README.md | 2 +- barmesh/draw.gd | 78 +++++++++++++++++++++++++++---------- scenes/main.tscn | 17 +++++++- scripts/toolpath_ui.gd | 16 +++++++- scripts/toolsurface_host.gd | 17 ++++++++ 5 files changed, 107 insertions(+), 23 deletions(-) diff --git a/barmesh/README.md b/barmesh/README.md index a8a65e0..e7bb71f 100644 --- a/barmesh/README.md +++ b/barmesh/README.md @@ -9,6 +9,6 @@ Julian’s 3-axis tool-surface mesh. **All geometry here is Z-up** (CAD). | `tool_contact.gd` | Ball-nose drop along **tool axis −Z** from a point above | | `draw.gd` | ImmediateMesh; **only** place that converts CAD Z-up → Godot Y-up `(x, z, y)` | -**Conditions** live in `subdiv.gd` (shared ε / stepover / a). Split a live `Bar` with `InsertNodeIntoBarF` when **XY > epsilon** (default 0.01 mm) **and** (**3D length > stepover** 1 mm **or** contact-normal angle **> a** 15°). Cell splits (`MakeBarBetweenNodesF`): walk right-hand rings via `GetBarBackRight`; pick the cell with worst residual to the plane through avg(contact points) with normal avg(contact normals); split with the chord that minimises the worse child residual. Tol: **coplanar_tol** (⊥) + max pairwise normal angle **a**. Insertion XY defaults to midpoint bisection; optional plane-intersect guess brackets z/normal discontinuities along the bar. +**Conditions** live in `subdiv.gd` (shared ε / stepover / a). Split a live `Bar` with `InsertNodeIntoBarF` when **XY > epsilon** (default 0.01 mm) **and** (**3D length > stepover** 1 mm **or** contact-normal angle **> a** 15°). Cell splits (`MakeBarBetweenNodesF`): walk right-hand rings via `GetBarBackRight`; pick the cell with worst residual to the plane through avg(contact points) with normal avg(contact normals); split with the chord that minimises the worse child residual. Tol: **coplanar_tol** (⊥) + max pairwise normal angle **a**. Interactive builds leave cell splits to **Subdivide next N cells** (default N=10); headless/CI still auto-runs up to `max_refine_passes`. Insertion XY defaults to midpoint bisection; optional plane-intersect guess brackets z/normal discontinuities along the bar. Do not assign `Node.p` to a `Node3D.transform` without `draw.cad_to_godot`. diff --git a/barmesh/draw.gd b/barmesh/draw.gd index 056ff2f..9c25879 100644 --- a/barmesh/draw.gd +++ b/barmesh/draw.gd @@ -14,6 +14,8 @@ const Contact = preload("res://barmesh/tool_contact.gd") @export var stepover_mm: float = 6.0 @export var angle_deg: float = 15.0 @export var max_refine_passes: int = 12 +## Auto cell splits after bar refine. Interactive default 0 (Julian 149: button + N). +@export var auto_cell_refine_passes: int = 0 @export var show_bars: bool = true @export var show_normals: bool = true @export var include_base_plane: bool = true @@ -22,6 +24,7 @@ var _playing: bool = false var _run_id: int = 0 var _last_bm: BarMesh var _last_tris: Array = [] +var _last_params: Subdiv.Params var _normals_mi: MeshInstance3D @@ -57,6 +60,8 @@ func _ready() -> void: add_child(_normals_mi) if DisplayServer.get_name() == "headless": row_delay_s = 0.0 + # CI still needs progressive cell splits without a UI. + auto_cell_refine_passes = max_refine_passes call_deferred("play_over_part") @@ -145,36 +150,69 @@ func _refine_barmesh(bm: BarMesh, R: float, tris: Array, z_plane: float, z_above _draw_barmesh(bm) if row_delay_s > 0.0: await get_tree().create_timer(row_delay_s).timeout - # Cells after bars (Julian 144): worst planar residual → MakeBarBetweenNodesF. - await _refine_cells(bm, params, my_run) + _last_params = params + # Cells after bars (Julian 144/149): optional auto; interactive uses button + N. + await _refine_cells(bm, params, my_run, auto_cell_refine_passes) + + +## Split up to `count` worst out-of-tolerance cells (Julian CNC 149). +func subdivide_next_cells(count: int) -> int: + if _playing or _last_bm == null or count <= 0: + return 0 + var params: Subdiv.Params = _last_params + if params == null: + params = _make_params() + _last_params = params + var done := 0 + for _i in range(count): + if not _try_one_cell_split(_last_bm, params): + break + done += 1 + _draw_barmesh(_last_bm) + return done -func _refine_cells(bm: BarMesh, params: Subdiv.Params, my_run: int) -> void: - for _pass in range(max_refine_passes): +func _make_params() -> Subdiv.Params: + var params := Subdiv.Params.new() + params.epsilon_m = epsilon_mm * 0.001 + params.stepover_m = stepover_mm * 0.001 + params.angle_deg = angle_deg + params.coplanar_tol_m = epsilon_mm * 0.001 + return params + + +func _refine_cells(bm: BarMesh, params: Subdiv.Params, my_run: int, passes: int) -> void: + for _pass in range(maxi(passes, 0)): if my_run != _run_id: return - if bm.nodes.size() > 8000: - break - var worst: Dictionary = Subdiv.find_worst_cell_seed(bm, params) - if not bool(worst.get("ok", false)): - break - var pick: Dictionary = Subdiv.pick_cell_split_for_make_bar(worst["nodes"], worst["bars"], params) - if not bool(pick.get("ok", false)): + if not _try_one_cell_split(bm, params): break - var n1: BarMesh.BMNode = pick["node1"] - var n2: BarMesh.BMNode = pick["node2"] - var b1: BarMesh.BMBar = pick["bar1"] - var b2: BarMesh.BMBar = pick["bar2"] - if b1.bbardeleted or b2.bbardeleted: - break - if bm.d_test_colinearity_f(n1, b1, n2, b2) or bm.d_test_colinearity_f(n2, b2, n1, b1): - break - bm.make_bar_between_nodes_f(n1, b1, n2, b2) _draw_barmesh(bm) if row_delay_s > 0.0: await get_tree().create_timer(row_delay_s).timeout +func _try_one_cell_split(bm: BarMesh, params: Subdiv.Params) -> bool: + if bm.nodes.size() > 8000: + return false + var worst: Dictionary = Subdiv.find_worst_cell_seed(bm, params) + if not bool(worst.get("ok", false)): + return false + var pick: Dictionary = Subdiv.pick_cell_split_for_make_bar(worst["nodes"], worst["bars"], params) + if not bool(pick.get("ok", false)): + return false + var n1: BarMesh.BMNode = pick["node1"] + var n2: BarMesh.BMNode = pick["node2"] + var b1: BarMesh.BMBar = pick["bar1"] + var b2: BarMesh.BMBar = pick["bar2"] + if b1.bbardeleted or b2.bbardeleted: + return false + if bm.d_test_colinearity_f(n1, b1, n2, b2) or bm.d_test_colinearity_f(n2, b2, n1, b1): + return false + bm.make_bar_between_nodes_f(n1, b1, n2, b2) + return true + + func _grid_parts(lo: float, hi: float) -> int: var gs: float = maxf(stepover_mm * 0.001, 0.0002) return clampi(int(ceil((hi - lo) / gs)), 1, 48) diff --git a/scenes/main.tscn b/scenes/main.tscn index c2f895e..01b7d82 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -72,7 +72,7 @@ script = ExtResource("6") offset_left = 16.0 offset_top = 16.0 offset_right = 400.0 -offset_bottom = 420.0 +offset_bottom = 520.0 [node name="VBox" type="VBoxContainer" parent="ToolpathUI/Panel"] layout_mode = 2 @@ -156,6 +156,21 @@ text = "Debug folded 2-tris (dihedral)" layout_mode = 2 text = "Build tool surface" +[node name="CellSplitLabel" type="Label" parent="ToolpathUI/Panel/VBox"] +layout_mode = 2 +text = "Next cell splits" + +[node name="CellSplitCount" type="SpinBox" parent="ToolpathUI/Panel/VBox"] +layout_mode = 2 +min_value = 1.0 +max_value = 200.0 +step = 1.0 +value = 10.0 + +[node name="CellSplit" type="Button" parent="ToolpathUI/Panel/VBox"] +layout_mode = 2 +text = "Subdivide next N cells" + [node name="Status" type="Label" parent="ToolpathUI/Panel/VBox"] layout_mode = 2 text = "BarMesh only. MMB orbit / Shift+MMB pan." diff --git a/scripts/toolpath_ui.gd b/scripts/toolpath_ui.gd index 66c6c9e..40a7c71 100644 --- a/scripts/toolpath_ui.gd +++ b/scripts/toolpath_ui.gd @@ -8,6 +8,7 @@ signal bake_requested( sample_step: float, z_stepdown: float ) +signal cell_subdivide_requested(count: int) @onready var strategy: OptionButton = $Panel/VBox/Strategy @onready var tool_radius: SpinBox = $Panel/VBox/ToolRadius @@ -16,6 +17,7 @@ signal bake_requested( @onready var sample_step: SpinBox = $Panel/VBox/SampleStep @onready var z_stepdown_label: Label = $Panel/VBox/ZStepdownLabel @onready var z_stepdown: SpinBox = $Panel/VBox/ZStepdown +@onready var cell_split_count: SpinBox = $Panel/VBox/CellSplitCount @onready var status: Label = $Panel/VBox/Status func _ready() -> void: @@ -45,8 +47,13 @@ func _ready() -> void: sample_step.value = 0.01 $Panel/VBox/Title.text = "Tool surface (CAD Z-up)" $Panel/VBox/Bake.text = "Build tool surface" - status.text = "BarMesh refine: epsilon / stepover / angle." + status.text = "BarMesh refine: epsilon / stepover / angle. Cell splits: button." $Panel/VBox/Bake.pressed.connect(_on_bake) + cell_split_count.min_value = 1 + cell_split_count.max_value = 200 + cell_split_count.step = 1 + cell_split_count.value = 10 + $Panel/VBox/CellSplit.pressed.connect(_on_cell_split) var bars := $Panel/VBox/ShowBars as CheckBox var norms := $Panel/VBox/ShowNormals as CheckBox var ortho := $Panel/VBox/Ortho as CheckBox @@ -90,5 +97,12 @@ func _on_bake() -> void: status.text = "Building…" bake_requested.emit("barmesh", tool_radius.value, stepover.value, sample_step.value, z_stepdown.value) + +func _on_cell_split() -> void: + var n := int(cell_split_count.value) + status.text = "Subdividing next %d cells…" % n + cell_subdivide_requested.emit(n) + + func set_status(text: String) -> void: status.text = text diff --git a/scripts/toolsurface_host.gd b/scripts/toolsurface_host.gd index 6f90b8c..088b233 100644 --- a/scripts/toolsurface_host.gd +++ b/scripts/toolsurface_host.gd @@ -13,6 +13,8 @@ func _ready() -> void: var ui := get_node_or_null(ui_path) if ui and ui.has_signal("bake_requested"): ui.bake_requested.connect(_on_bake_requested) + if ui and ui.has_signal("cell_subdivide_requested"): + ui.cell_subdivide_requested.connect(_on_cell_subdivide_requested) if DisplayServer.get_name() != "headless": call_deferred("_on_bake_requested", "barmesh", 5.0, 6.0, 0.01, 15.0) @@ -27,6 +29,21 @@ func _on_bake_requested( await bake_strategy("barmesh", tool_radius_mm, stepover_mm, epsilon_mm, angle_deg) +func _on_cell_subdivide_requested(count: int) -> void: + var ui := get_node_or_null(ui_path) + var viz := get_node_or_null(barmesh_preview_path) + if viz == null or not viz.has_method("subdivide_next_cells"): + if ui: + ui.set_status("No BarMesh to subdivide yet.") + return + var done: int = int(viz.subdivide_next_cells(count)) + if ui: + if done <= 0: + ui.set_status("No more out-of-tolerance cells to split.") + else: + ui.set_status("Split %d cell(s)." % done) + + func bake_strategy( strategy: String, tool_radius_mm: float = 5.0,