Skip to content

Commit 72c9023

Browse files
authored
Merge pull request #177 from jgphilpott/copilot/fix-orphaned-twig-components
Fix orphaned twig segments and strengthen twig/branch joint in tree support generation
2 parents ae0776b + af6e2e5 commit 72c9023

File tree

6 files changed

+115
-23
lines changed

6 files changed

+115
-23
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:5c2173be17318e515d2e639076c878399254fb1119185a0f206089f71aba510c
3-
size 749497
2+
oid sha256:622d8a7279c72c522ed06489ce5256be09f35baf45ad18e1ab8627a241ae9d2f
3+
size 930444
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:14ab35c8b1c1258ac00816a6436e644e298e200c0bac80a1e55ff6586cf3c3ae
3-
size 1056412
2+
oid sha256:0203ffb424f0563d6a488c14f40a2ae1030f507e5cbd255e6fc0658ae1a9adc5
3+
size 1408356
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:3779e0ab50cb3e2fcfc30905af786d31dfe2eff0d8617e0fa92a42e15d25574c
3-
size 929963
2+
oid sha256:986560f336ffb9603158d5b64dc1007940e6c54971527a065d091462dc420650
3+
size 1054325
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:b0b308cfbe67b23265c70981a7d13160f7517f4f9c54b22d3ac1c5fa38050904
3-
size 1159493
2+
oid sha256:09d860964fbee929f4ab37c4d6bf2fc7d214e3a7d0dffd3fa1113b0ca4438a04
3+
size 1419308

src/slicer/support/tree/tree.coffee

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,15 @@ CIRCLE_SEGMENTS = 12
3434
# Chord length = 2 * radius * CIRCLE_CHORD_SIN_FACTOR.
3535
CIRCLE_CHORD_SIN_FACTOR = Math.sin(Math.PI / CIRCLE_SEGMENTS)
3636

37+
# Number of layers by which each twig overlaps its parent branch.
38+
# Twigs start this many layers below the branch endpoint so that branch and twig
39+
# material are printed at the same Z level, physically bonding the joint.
40+
TWIG_OVERLAP_LAYERS = 1
41+
3742
module.exports =
3843

44+
TWIG_OVERLAP_LAYERS: TWIG_OVERLAP_LAYERS
45+
3946
# Return the interpolated face Z at (x, y) by searching all region faces.
4047
# Returns null if the point lies outside every face's 2D XY projection.
4148
getFaceZAtPoint: (x, y, faces) ->
@@ -146,18 +153,35 @@ module.exports =
146153

147154
cx = 0
148155
cy = 0
149-
maxZ = -Infinity
150156

151157
for tip in clusterTips
152158

153159
cx += tip.x
154160
cy += tip.y
155-
maxZ = Math.max(maxZ, tip.z)
156161

157162
cx /= clusterTips.length
158163
cy /= clusterTips.length
159164

160-
branchNodes.push({ x: cx, y: cy, z: maxZ, tips: clusterTips })
165+
# Compute the branch node Z from the 45-degree constraint applied to each tip.
166+
# nodeZ = min(tip.z - tdist) guarantees every twig rises at ≤ 45 degrees from
167+
# the branch endpoint to its contact tip, eliminating orphaned twig segments.
168+
nodeZ = Infinity
169+
170+
for tip in clusterTips
171+
172+
tdx = tip.x - cx
173+
tdy = tip.y - cy
174+
tdist = Math.sqrt(tdx * tdx + tdy * tdy)
175+
nodeZ = Math.min(nodeZ, tip.z - tdist)
176+
177+
nodeZ = Math.max(nodeZ, buildPlateZ + layerHeight)
178+
179+
# Round to 0.1 µm to avoid floating-point edge cases in the twig-emission
180+
# condition (node.z < tip.z - layerHeight) when large absolute coordinates
181+
# accumulate tiny rounding errors that differ from small-coordinate equivalents.
182+
nodeZ = Math.round(nodeZ * 10000) / 10000
183+
184+
branchNodes.push({ x: cx, y: cy, z: nodeZ, tips: clusterTips })
161185

162186
# Compute branch root heights: where each branch diverges from the shared trunk.
163187
# The ideal root height is determined by the 45° angle constraint
@@ -195,6 +219,13 @@ module.exports =
195219
node = branchNodes[nodeIdx]
196220
branchRootZ = branchRootZs[nodeIdx]
197221

222+
# Guarantee the branch spans at least one printable layer.
223+
# When nodeZ was clamped to buildPlateZ + layerHeight and branchRootZ was also
224+
# clamped to the same value, the branch segment collapses to zero height and
225+
# never intersects any layer Z plane. Nudging node.z up by layerHeight ensures
226+
# a non-zero segment without changing the already-computed branchRootZ.
227+
node.z = Math.max(node.z, Math.round((branchRootZ + layerHeight) * 10000) / 10000)
228+
198229
# Branch segment: angled from trunk toward the branch node.
199230
segments.push({
200231
x1: trunkX, y1: trunkY, z1: branchRootZ
@@ -203,19 +234,18 @@ module.exports =
203234
})
204235

205236
# Twig segments: fine sub-branches from cluster node to individual contact tips.
206-
for tip in node.tips
237+
# Twigs start TWIG_OVERLAP_LAYERS below the branch endpoint so that both the
238+
# branch (nearing its end) and the twig (at its root) are printed at the same Z,
239+
# creating a physical bond that strengthens the twig/branch joint.
240+
# Floor is branchRootZ (not buildPlateZ) so twigs never start below the branch.
241+
twigStartZ = Math.max(node.z - TWIG_OVERLAP_LAYERS * layerHeight, branchRootZ)
207242

208-
tdx = tip.x - node.x
209-
tdy = tip.y - node.y
210-
tdist = Math.sqrt(tdx * tdx + tdy * tdy)
211-
212-
twigRootZ = tip.z - tdist
213-
twigRootZ = Math.max(twigRootZ, branchRootZ + layerHeight)
243+
for tip in node.tips
214244

215-
if twigRootZ < tip.z - layerHeight
245+
if twigStartZ < tip.z - layerHeight
216246

217247
segments.push({
218-
x1: node.x, y1: node.y, z1: twigRootZ
248+
x1: node.x, y1: node.y, z1: twigStartZ
219249
x2: tip.x, y2: tip.y, z2: tip.z
220250
type: 'twig'
221251
})

src/slicer/support/tree/tree.test.coffee

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,72 @@ describe 'Tree Support Module', ->
359359
segsOrigin = treeSupport.buildTreeStructure(regionAtOrigin, 0.4, 0, 0.2)
360360
segsOffset = treeSupport.buildTreeStructure(regionOffset, 0.4, 0, 0.2)
361361

362-
# The two structures should produce the same segment counts.
363-
expect(segsOffset.length).toBe(segsOrigin.length)
362+
# The number of branch nodes (and therefore branch segments) must be identical.
363+
branchOrigin = segsOrigin.filter (s) -> s.type is 'branch'
364+
branchOffset = segsOffset.filter (s) -> s.type is 'branch'
364365

365-
describe 'generateTreePattern', ->
366+
expect(branchOffset.length).toBe(branchOrigin.length)
367+
368+
test 'should not produce orphaned twigs - each twig start must match a branch endpoint', ->
369+
370+
# A twig is orphaned when it has no structural connection to any branch.
371+
# Each twig's XY root must coincide with a branch endpoint's XY, and the
372+
# twig's Z root must be within TWIG_OVERLAP_LAYERS layers below the branch
373+
# endpoint (the twig starts slightly lower to create a physical bond).
374+
layerHeight = 0.2
375+
overlapZ = treeSupport.TWIG_OVERLAP_LAYERS * layerHeight
376+
region = makeRegion(-10, 10, -10, 10, 20)
377+
segments = treeSupport.buildTreeStructure(region, 0.4, 0, layerHeight)
378+
379+
twigSegments = segments.filter (s) -> s.type is 'twig'
380+
branchSegments = segments.filter (s) -> s.type is 'branch'
381+
382+
expect(twigSegments.length).toBeGreaterThan(0)
383+
384+
for twig in twigSegments
385+
386+
# Every twig must have a branch whose XY endpoint matches the twig XY root,
387+
# with the twig starting at most TWIG_OVERLAP_LAYERS layers below the branch end.
388+
matchingBranch = branchSegments.some (branch) ->
389+
Math.abs(branch.x2 - twig.x1) < 0.001 and
390+
Math.abs(branch.y2 - twig.y1) < 0.001 and
391+
twig.z1 <= branch.z2 + 0.001 and
392+
twig.z1 >= branch.z2 - overlapZ - 0.001
393+
394+
expect(matchingBranch).toBe(true)
395+
396+
return
397+
398+
test 'should produce valid branches and twigs for low overhang near the build plate', ->
399+
400+
# A wide region at low Z forces nodeZ onto its clamp (buildPlateZ + layerHeight),
401+
# which previously caused branchRootZ == nodeZ (zero-height branch) and
402+
# twigStartZ to fall below branchRootZ. Both must now be handled correctly.
403+
layerHeight = 0.2
404+
overlapZ = treeSupport.TWIG_OVERLAP_LAYERS * layerHeight
405+
region = makeRegion(-10, 10, -10, 10, 2)
406+
segments = treeSupport.buildTreeStructure(region, 0.4, 0, layerHeight)
407+
408+
branchSegments = segments.filter (s) -> s.type is 'branch'
409+
twigSegments = segments.filter (s) -> s.type is 'twig'
410+
411+
# Every branch must span at least one printable layer (non-zero height).
412+
for branch in branchSegments
413+
414+
expect(branch.z2).toBeGreaterThan(branch.z1)
415+
416+
# Every twig must connect to a branch and start within branchRootZ..nodeZ.
417+
for twig in twigSegments
418+
419+
matchingBranch = branchSegments.some (branch) ->
420+
Math.abs(branch.x2 - twig.x1) < 0.001 and
421+
Math.abs(branch.y2 - twig.y1) < 0.001 and
422+
twig.z1 >= branch.z1 - 0.001 and
423+
twig.z1 <= branch.z2 + 0.001
424+
425+
expect(matchingBranch).toBe(true)
426+
427+
return
366428

367429
# Helper: create a minimal slicer-like object for testing.
368430
makeSlicer = ->

0 commit comments

Comments
 (0)