Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/godot-headless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,22 @@ jobs:
fi
grep -q "EARTIP_QUALITY_OK" "$EOUT"
[ "$ERC" -eq 0 ]
POUT=$(mktemp)
set +e
godot --headless --path /src -s res://tools/test_planar_residual.gd 2>&1 | tee "$POUT"
PRC=${PIPESTATUS[0]}
set -e
if grep -E "Parse Error|Failed to load script|PLANAR_FAIL" "$POUT"; then
echo "::error::Godot planar residual (CL vs contact) failure"
exit 1
fi
grep -q "PLANAR_OK" "$POUT"
[ "$PRC" -eq 0 ]
'

- name: Summarize
if: always()
run: |
echo "### Godot CI" >> "$GITHUB_STEP_SUMMARY"
echo "bake + contact + drop_quality (folded/roof) + **eartip penetration** (\`test_eartip_quality.gd\`)" >> "$GITHUB_STEP_SUMMARY"
echo "bake + contact + drop_quality + eartip + **planar residual CL** (\`test_planar_residual.gd\`)" >> "$GITHUB_STEP_SUMMARY"
echo "Status: ${{ job.status }}" >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion barmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`; rank cells by continuous **planar tolerance** (⊥ residual to avg-normal/avg-point plane Julian 155); split the worst, then **subdivide the new bar to tolerance** (Julian 156). Stop when all cells are in tolerance or XY extent is too small. 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.
**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`; rank cells by continuous **planar tolerance** of **cutter CL points** (`node.p`) vs avg-contact-normal plane (Julian 144/155/162 — not `contact_point`); split the worst, then **subdivide the new bar to tolerance** (Julian 156). Stop when all cells are in tolerance or XY extent is too small. Tol: separate **planar_tol** (⊥, UI) + 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`.
13 changes: 7 additions & 6 deletions barmesh/draw.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const Contact = preload("res://barmesh/tool_contact.gd")
@export var epsilon_mm: float = 0.01
@export var stepover_mm: float = 6.0
@export var angle_deg: float = 15.0
## Cell planar tolerance (mm) — separate from XY epsilon (Julian 154).
@export var planar_tol_mm: float = 0.01
@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
Expand Down Expand Up @@ -134,7 +136,7 @@ func _refine_barmesh(bm: BarMesh, R: float, tris: Array, z_plane: float, z_above
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
params.coplanar_tol_m = planar_tol_mm * 0.001
for _pass in range(max_refine_passes):
if my_run != _run_id:
return
Expand Down Expand Up @@ -162,13 +164,12 @@ func _refine_barmesh(bm: BarMesh, R: float, tris: Array, z_plane: float, z_above


## Split up to `count` worst out-of-tolerance cells (Julian CNC 149).
## Rebuilds params each call so a tightened planar_tol_mm applies without rebake (154).
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 params: Subdiv.Params = _make_params()
_last_params = params
var done := 0
for _i in range(count):
if not _try_one_cell_split(_last_bm, params):
Expand All @@ -183,7 +184,7 @@ func _make_params() -> Subdiv.Params:
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
params.coplanar_tol_m = planar_tol_mm * 0.001
return params


Expand Down
24 changes: 12 additions & 12 deletions barmesh/subdiv.gd
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,30 @@ static func cell_planar_tolerance(cell_nodes: Array) -> float:
return cell_planar_residual(cell_nodes)


## Julian 144: plane through avg(contact_points) with normal avg(contact_normals);
## residual = max |⊥ distance|. Three points → 0 if normals define a plane.
## Julian 144/162: plane through avg(CL points = node.p) with normal
## avg(contact_normals); residual = max |⊥ distance| of those CL points.
## Use tool centres, not contact_point — valley cells can look flat on the
## part while the CL lattice is badly warped (msg 162).
static func cell_planar_residual(cell_nodes: Array) -> float:
if cell_nodes.size() <= 3:
if cell_nodes.size() < 4:
return 0.0
var c := Vector3.ZERO
var nsum := Vector3.ZERO
var n_pts := 0
var n_nrm := 0
for n in cell_nodes:
var node: BarMesh.BMNode = n
if node.contact_kind != BarMesh.BMNode.ContactFeature.NONE:
c += node.contact_point
n_pts += 1
c += node.p
if node.contact_normal.length_squared() > 0.25:
nsum += node.contact_normal.normalized()
n_nrm += 1
if n_pts < 3 or n_nrm < 1 or nsum.length_squared() < 1e-12:
if n_nrm < 1 or nsum.length_squared() < 1e-12:
return 0.0
c /= float(n_pts)
c /= float(cell_nodes.size())
var normal: Vector3 = nsum.normalized()
var worst := 0.0
for n2 in cell_nodes:
var node2: BarMesh.BMNode = n2
if node2.contact_kind == BarMesh.BMNode.ContactFeature.NONE:
continue
worst = maxf(worst, absf(normal.dot(node2.contact_point - c)))
worst = maxf(worst, absf(normal.dot(node2.p - c)))
return worst


Expand Down Expand Up @@ -136,6 +133,9 @@ static func find_worst_cell_seed(bm: BarMesh, params: Params) -> Dictionary:
if not bool(ring.get("ok", false)):
continue
var nodes: Array = ring["nodes"]
# MakeBarBetweenNodesF needs ≥4 nodes; triangles stall the worst-first loop.
if nodes.size() < 4:
continue
if cell_xy_too_small(nodes, params):
continue
var r: float = cell_planar_tolerance(nodes)
Expand Down
11 changes: 11 additions & 0 deletions scenes/main.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ max_value = 10.0
step = 0.1
value = 1.0

[node name="PlanarTolLabel" type="Label" parent="ToolpathUI/Panel/VBox"]
layout_mode = 2
text = "Planar tolerance (mm)"

[node name="PlanarTol" type="SpinBox" parent="ToolpathUI/Panel/VBox"]
layout_mode = 2
min_value = 0.001
max_value = 5.0
step = 0.001
value = 0.01

[node name="ShowBars" type="CheckBox" parent="ToolpathUI/Panel/VBox"]
layout_mode = 2
button_pressed = true
Expand Down
25 changes: 22 additions & 3 deletions scripts/toolpath_ui.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ signal bake_requested(
tool_radius: float,
stepover: float,
sample_step: float,
z_stepdown: float
z_stepdown: float,
planar_tol: float
)
signal cell_subdivide_requested(count: int)

Expand All @@ -17,6 +18,7 @@ signal cell_subdivide_requested(count: int)
@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 planar_tol: SpinBox = $Panel/VBox/PlanarTol
@onready var cell_split_count: SpinBox = $Panel/VBox/CellSplitCount
@onready var status: Label = $Panel/VBox/Status

Expand Down Expand Up @@ -45,9 +47,15 @@ func _ready() -> void:
sample_step.min_value = 0.001
sample_step.step = 0.001
sample_step.value = 0.01
$Panel/VBox/PlanarTolLabel.visible = true
planar_tol.visible = true
planar_tol.min_value = 0.001
planar_tol.max_value = 5.0
planar_tol.step = 0.001
planar_tol.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. Cell splits: button."
status.text = "BarMesh refine: epsilon / stepover / angle / planar tol. Cell splits: button."
$Panel/VBox/Bake.pressed.connect(_on_bake)
cell_split_count.min_value = 1
cell_split_count.max_value = 200
Expand Down Expand Up @@ -95,12 +103,23 @@ func _preview() -> Node:

func _on_bake() -> void:
status.text = "Building…"
bake_requested.emit("barmesh", tool_radius.value, stepover.value, sample_step.value, z_stepdown.value)
bake_requested.emit(
"barmesh",
tool_radius.value,
stepover.value,
sample_step.value,
z_stepdown.value,
planar_tol.value
)


func _on_cell_split() -> void:
var n := int(cell_split_count.value)
status.text = "Subdividing next %d cells…" % n
# Push current planar tol so tighten-without-rebake works (Julian 154).
var viz := _preview()
if viz and viz.get("planar_tol_mm") != null:
viz.planar_tol_mm = planar_tol.value
cell_subdivide_requested.emit(n)


Expand Down
11 changes: 7 additions & 4 deletions scripts/toolsurface_host.gd
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ func _ready() -> void:
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)
call_deferred("_on_bake_requested", "barmesh", 5.0, 6.0, 0.01, 15.0, 0.01)


func _on_bake_requested(
_strategy: String,
tool_radius_mm: float,
stepover_mm: float,
epsilon_mm: float,
angle_deg: float
angle_deg: float,
planar_tol_mm: float = 0.01
) -> void:
await bake_strategy("barmesh", tool_radius_mm, stepover_mm, epsilon_mm, angle_deg)
await bake_strategy("barmesh", tool_radius_mm, stepover_mm, epsilon_mm, angle_deg, planar_tol_mm)


func _on_cell_subdivide_requested(count: int) -> void:
Expand All @@ -49,7 +50,8 @@ func bake_strategy(
tool_radius_mm: float = 5.0,
stepover_mm: float = 6.0,
epsilon_mm: float = 0.01,
angle_deg: float = 15.0
angle_deg: float = 15.0,
planar_tol_mm: float = 0.01
) -> Dictionary:
if strategy != "barmesh":
push_error("Toolpaths archived; only barmesh/toolsurface is active: " + strategy)
Expand All @@ -76,6 +78,7 @@ func bake_strategy(
viz.stepover_mm = stepover_mm
viz.epsilon_mm = epsilon_mm
viz.angle_deg = angle_deg
viz.planar_tol_mm = planar_tol_mm
await viz.play_over_part(tool_radius)
if ui:
ui.set_status("BarMesh tool-contact lattice (CAD Z-up).")
Expand Down
60 changes: 60 additions & 0 deletions tools/test_planar_residual.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
extends SceneTree
## Julian 162: planar residual must use CL (node.p), not contact_point.
## Headless: godot --headless --path . -s res://tools/test_planar_residual.gd

const BarMeshGD = preload("res://barmesh/barmesh.gd")
const Subdiv = preload("res://barmesh/subdiv.gd")


func _init() -> void:
call_deferred("_run")


func _fail(msg: String) -> void:
push_error("PLANAR_FAIL " + msg)
quit(1)


func _run() -> void:
# Four CLs over a valley: contact points nearly coplanar on z=0, CLs bowed.
var nodes: Array = []
var pts := [
Vector3(0.0, 0.0, 0.005),
Vector3(0.02, 0.0, 0.005),
Vector3(0.02, 0.02, 0.001),
Vector3(0.0, 0.02, 0.005),
]
var contacts := [
Vector3(0.0, 0.0, 0.0),
Vector3(0.02, 0.0, 0.0),
Vector3(0.02, 0.02, 0.0),
Vector3(0.0, 0.02, 0.0),
]
for i in 4:
var n := BarMeshGD.BMNode.new(pts[i], i)
n.contact_point = contacts[i]
n.contact_normal = Vector3(0, 0, 1)
n.contact_kind = BarMeshGD.BMNode.ContactFeature.FACE
nodes.append(n)

var r: float = Subdiv.cell_planar_residual(nodes)
# Avg CL z=0.004; valley node at 0.001 → residual 0.003 (contact plane stays 0).
if r < 0.0025:
_fail("valley CL residual too small: %s (expected ~0.003)" % r)
return
# Sanity: if we only looked at contact_point, residual would be ~0.
var csum := Vector3.ZERO
for n2 in nodes:
csum += (n2 as BarMeshGD.BMNode).contact_point
csum /= 4.0
var c_worst := 0.0
for n3 in nodes:
c_worst = maxf(c_worst, absf(Vector3(0, 0, 1).dot((n3 as BarMeshGD.BMNode).contact_point - csum)))
if c_worst > 1e-6:
_fail("test setup: contact points not coplanar")
return
if r <= c_worst + 1e-6:
_fail("residual collapsed to contact_point measure")
return
print("PLANAR_OK residual=", r)
quit(0)