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
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`): (1) **coplanar_tol** = max ⊥ distance to best-fit plane (3 pts → 0); (2) **max pairwise contact-normal angle** (same `a`); connect opposite-side nodes that are not near-colinear and that avoid slivers. 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**. 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`.
64 changes: 64 additions & 0 deletions barmesh/barmesh.gd
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,70 @@ func live_bars() -> Array:
return out


## Right-hand cell ring of seed (Julian: GetBarBackRight walk). {ok, nodes, bars}.
func cell_ring_right(seed: BMBar) -> Dictionary:
if seed == null or seed.bbardeleted or seed.barforeright == null:
return {"ok": false}
var ring_bars: Array = []
var ring_nodes: Array = [seed.nodeback]
var at: BMNode = seed.nodeback
var cur: BMBar = seed
for _i in range(1000):
ring_bars.append(cur)
var bfore := cur.nodeback == at
at = cur.get_node_fore(bfore)
if at == seed.nodeback:
if ring_nodes.size() < 3:
return {"ok": false}
return {"ok": true, "nodes": ring_nodes, "bars": ring_bars, "seed": seed}
ring_nodes.append(at)
cur = cur.get_fore_right_bl(bfore)
if cur == null or cur.bbardeleted:
return {"ok": false}
return {"ok": false}


func d_test_colinearity_f(node1: BMNode, bar1: BMBar, node2: BMNode, _bar2: BMBar) -> bool:
var bar1a: BMBar = bar1.get_fore_right_bl(bar1.nodefore == node1)
if bar1a == null:
return false
var lbar: BMBar = bar1a
var lnode: BMNode = bar1a.get_node_fore(bar1a.nodeback == node1)
if lnode == node2:
return true
var ref: Vector3 = bar1a.barvecN
for _i in range(1000):
if lnode == node1:
return false
lbar = lbar.get_fore_right_bl(lbar.nodefore == lnode)
if lbar == null:
return false
lnode = lbar.get_node_fore(lbar.nodeback == lnode)
if lbar.barvecN != ref and lbar.barvecN != -ref:
return false
if lnode == node2:
return true
return false


## Insert a bar across a cell between node1 and node2 (vendor MakeBarBetweenNodesF).
func make_bar_between_nodes_f(node1: BMNode, bar1: BMBar, node2: BMNode, bar2: BMBar) -> BMBar:
assert(not bar1.bbardeleted and not bar2.bbardeleted)
assert(node1.i < node2.i)
assert(not d_test_colinearity_f(node1, bar1, node2, bar2))
assert(not d_test_colinearity_f(node2, bar2, node1, bar1))
var bar1a: BMBar = bar1.get_fore_right_bl(bar1.nodefore == node1)
var bar2a: BMBar = bar2.get_fore_right_bl(bar2.nodefore == node2)
assert(bar1a != null and bar2a != null)
var newbar := BMBar.new(node1, node2)
newbar.set_fore_right_bl(false, bar1a)
newbar.set_fore_right_bl(true, bar2a)
bar1.set_fore_right_bl(bar1.nodefore == node1, newbar)
bar2.set_fore_right_bl(bar2.nodefore == node2, newbar)
bars.append(newbar)
return newbar


func insert_node_into_bar_f(bar: BMBar, newnode: BMNode) -> BMNode:
assert(newnode.p != bar.nodeback.p and newnode.p != bar.nodefore.p)
assert(newnode in nodes)
Expand Down
29 changes: 29 additions & 0 deletions barmesh/draw.gd
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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
for _pass in range(max_refine_passes):
if my_run != _run_id:
return
Expand All @@ -144,6 +145,34 @@ 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)


func _refine_cells(bm: BarMesh, params: Subdiv.Params, my_run: int) -> void:
for _pass in range(max_refine_passes):
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)):
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 _grid_parts(lo: float, hi: float) -> int:
Expand Down
193 changes: 145 additions & 48 deletions barmesh/subdiv.gd
Original file line number Diff line number Diff line change
Expand Up @@ -42,78 +42,175 @@ static func bar_needs_split(bar: BarMesh.BMBar, params: Params) -> bool:
return need_len or need_ang


## Face/cell (MakeBarBetweenNodesF) — Julian CNC 127/132:
## Prefer largest cells whose contact points stay near-coplanar; shrink XY when
## contact normals span a wide range. Connect opposite-side nodes that are not
## near-colinear and that avoid narrow slivers.
## Future (Julian 134): an extra stop-over-subdivide rule will land here as its
## own predicate so it can be tweaked without touching topology.
## Face/cell (MakeBarBetweenNodesF) — Julian CNC 127/132/144:
## Split when avg-normal/avg-point plane residual > coplanar_tol, or contact-normal
## span > a (and XY > ε). Future anti-over-subdivide rule stays separate here.
static func cell_needs_split(cell_nodes: Array, params: Params) -> bool:
if cell_nodes.size() < 3:
return false
var xmin := INF
var xmax := -INF
var ymin := INF
var ymax := -INF
var normals: Array[Vector3] = []
var points: Array[Vector3] = []
for n in cell_nodes:
var node: BarMesh.BMNode = n
xmin = minf(xmin, node.p.x)
xmax = maxf(xmax, node.p.x)
ymin = minf(ymin, node.p.y)
ymax = maxf(ymax, node.p.y)
if node.contact_normal.length_squared() > 0.25:
normals.append(node.contact_normal.normalized())
if node.contact_point != Vector3.ZERO or node.contact_kind != BarMesh.BMNode.ContactFeature.NONE:
points.append(node.contact_point)
var dxy := maxf(xmax - xmin, ymax - ymin)
if dxy <= params.epsilon_m:
if maxf(xmax - xmin, ymax - ymin) <= params.epsilon_m:
return false
# Wide normal range → keep this cell from staying large in XY.
if _normals_span_exceeds(normals, params.angle_deg):
if cell_planar_residual(cell_nodes) > params.coplanar_tol_m:
return true
# Contact points not close to coplanar → subdivide.
if points.size() >= 4 and not _points_near_coplanar(points, params.coplanar_tol_m):
return true
return false
var normals: Array[Vector3] = []
for n2 in cell_nodes:
var nd: BarMesh.BMNode = n2
if nd.contact_normal.length_squared() > 0.25:
normals.append(nd.contact_normal.normalized())
return _normals_span_exceeds(normals, params.angle_deg)


## Among candidate opposite-side node pairs, pick one that is not near-colinear
## with a cell edge and that maximises the smaller of the two resulting face
## areas (sliver avoidance). Returns [node_a, node_b] or empty.
static func pick_cell_split_pair(cell_nodes: Array, params: Params) -> Array:
var n: int = cell_nodes.size()
if n < 4:
return []
var best: Array = []
var best_score := -INF
var cos_colin := cos(deg_to_rad(maxf(180.0 - params.angle_deg, 1.0)))
for i in n:
# Opposite-ish: about halfway around the ring.
var j := (i + n / 2) % n
if j == i:
## Julian 144: plane through avg(contact_points) with normal avg(contact_normals);
## residual = max |⊥ distance|. Three points → 0 if normals define a plane.
static func cell_planar_residual(cell_nodes: Array) -> float:
if cell_nodes.size() <= 3:
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
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:
return 0.0
c /= float(n_pts)
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)))
return worst


## Unique right-hand cells (one seed bar each). Dedupes so bars > cells.
static func unique_cell_seeds(bm: BarMesh) -> Array:
var seen: Dictionary = {}
var seeds: Array = []
for bar in bm.live_bars():
var ring: Dictionary = bm.cell_ring_right(bar)
if not bool(ring.get("ok", false)):
continue
var a: BarMesh.BMNode = cell_nodes[i]
var b: BarMesh.BMNode = cell_nodes[j]
var ab := Vector2(b.p.x - a.p.x, b.p.y - a.p.y)
if ab.length() <= params.epsilon_m:
var key := _cell_key(ring["nodes"])
if seen.has(key):
continue
var abn := ab.normalized()
# Reject if nearly colinear with either adjacent edge at a or b.
var prev_a: BarMesh.BMNode = cell_nodes[(i - 1 + n) % n]
var next_a: BarMesh.BMNode = cell_nodes[(i + 1) % n]
if _edge_dir_xy(prev_a, a).dot(abn) > cos_colin:
seen[key] = true
seeds.append(bar)
return seeds


## Cell farthest from its avg-normal/avg-point plane (among those needing split).
static func find_worst_cell_seed(bm: BarMesh, params: Params) -> Dictionary:
var worst: Dictionary = {"ok": false, "residual": -1.0}
for seed in unique_cell_seeds(bm):
var ring: Dictionary = bm.cell_ring_right(seed)
if not bool(ring.get("ok", false)):
continue
var nodes: Array = ring["nodes"]
if not cell_needs_split(nodes, params):
continue
if _edge_dir_xy(a, next_a).dot(abn) > cos_colin:
var r: float = cell_planar_residual(nodes)
if bool(worst.get("ok", false)) and r <= float(worst["residual"]):
continue
var area_score := _split_min_poly_area_xy(cell_nodes, i, j)
if area_score > best_score:
best_score = area_score
best = [a, b]
worst = {
"ok": true,
"seed": seed,
"nodes": nodes,
"bars": ring["bars"],
"residual": r,
}
return worst


## Pick node/bar pair for MakeBarBetweenNodesF that minimises max child planar residual.
static func pick_cell_split_for_make_bar(ring_nodes: Array, ring_bars: Array, params: Params) -> Dictionary:
var n: int = ring_nodes.size()
if n < 4 or ring_bars.size() != n:
return {"ok": false}
var best: Dictionary = {"ok": false, "score": INF}
for i in n:
for j in range(i + 2, n):
# Skip adjacent wrap (i=0,j=n-1).
if i == 0 and j == n - 1:
continue
if (j - i) < 2 or (n - (j - i)) < 2:
continue
var a: BarMesh.BMNode = ring_nodes[i]
var b: BarMesh.BMNode = ring_nodes[j]
if Vector2(b.p.x - a.p.x, b.p.y - a.p.y).length() <= params.epsilon_m:
continue
var left: Array = _ring_slice(ring_nodes, i, j)
var right: Array = _ring_slice(ring_nodes, j, i)
var score: float = maxf(cell_planar_residual(left), cell_planar_residual(right))
# Mild sliver guard: reject tiny min face area.
if _split_min_poly_area_xy(ring_nodes, i, j) < params.epsilon_m * params.epsilon_m:
continue
if score < float(best.get("score", INF)):
var bar_a: BarMesh.BMBar = ring_bars[(i - 1 + n) % n]
var bar_b: BarMesh.BMBar = ring_bars[(j - 1 + n) % n]
var n1: BarMesh.BMNode = a
var n2: BarMesh.BMNode = b
var b1: BarMesh.BMBar = bar_a
var b2: BarMesh.BMBar = bar_b
if n2.i < n1.i:
var tn = n1
n1 = n2
n2 = tn
var tb = b1
b1 = b2
b2 = tb
best = {
"ok": true,
"score": score,
"node1": n1,
"bar1": b1,
"node2": n2,
"bar2": b2,
}
return best


static func _cell_key(nodes: Array) -> String:
var ids: Array = []
for n in nodes:
var node: BarMesh.BMNode = n
ids.append(node.i)
ids.sort()
var parts := PackedStringArray()
for id in ids:
parts.append(str(id))
return ",".join(parts)


static func _ring_slice(nodes: Array, i: int, j: int) -> Array:
var out: Array = []
var n: int = nodes.size()
var k := i
while true:
out.append(nodes[k])
if k == j:
break
k = (k + 1) % n
return out


static func _edge_dir_xy(a: BarMesh.BMNode, b: BarMesh.BMNode) -> Vector2:
var d := Vector2(b.p.x - a.p.x, b.p.y - a.p.y)
if d.length_squared() < 1e-24:
Expand Down