/Hardware/WebbingClip.foocad
Various parts for attaching webbing, found on ruck sacks etc. All parts are customisable, for use with various thickness of webbing.
I started off creating the common quick-release clip, however, while checking out existing solutions, I realised that the "Double D" is much strong, and simpler than the quick-release clip.
So despite spending quick a long time perfecting the quick-release clip, I doubt I'll use it. Double D is stronger and velco is quicker ;-)
Both halves of the quick-release clip are printed flat on the bed, so that the forces do NOT act across the layer lines. The prongs snapping in tension is the expected failure mode.
The male part of the quick-release clip must NOT use PLA (it is too stiff, and brittle). I've been using ABS during testing, but PETG should work well too.
import static uk.co.nickthecoder.foocad.screws.v3.Screws.* import static uk.co.nickthecoder.foocad.extras.v1.Extras.* import static uk.co.nickthecoder.foocad.arrange.v1.Arrange.* import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.* class WebbingClip : ModelWithSetup { @Custom var webbingWidth = 25 @Custom var slotSize = 3.8 @Custom var maleThickness = 4 @Custom var femaleThickness = Vector2(3, 1.75) @Custom( about="X for the clearance of 2D shapes, Y for the clearance in the 3rd dimension" ) var clearance = Vector2(0.15, 0.3) var prongLength = 20 var guideWidth = 5.0 var chamfer = 0.75 var tieType = 1 var _svg : SVGDocument = null var _scale = 1.0 var _offset = Vector2(0,0) override fun setup() { _svg = SVGDocument( "WebbingClip.svg" ) _scale = prongLength / _svg["prong"].size.x _offset = Vector2( -_svg["prong"].left, -_svg["prongGeometry"].back) } fun fromSVG( name : String ) = _svg[name].translate(_offset).scale(_scale).toPolygon() fun tie() = if (tieType == 1) singleTie() else doubleTie() fun singleTie() : Shape2d { val outside = Square( slotSize + maleThickness * 2, webbingWidth + maleThickness*2 ) .centerY() .roundAllCorners(maleThickness*0.5) .rightTo(0.01) return outside - outside.offset( - maleThickness ).roundAllCorners(chamfer) } fun doubleTie() : Shape2d { return singleTie().translateX( -maleThickness - slotSize ).also() } fun alignY( shapeName : String ) : Shape2d { return fromSVG( shapeName ).translateY( webbingWidth/2 ) } @Piece fun male() : Shape3d { val tie = tie() val prongs = alignY("prong").mirrorY().also() val guide = Square( prongs.size.x, guideWidth ).centerY() .roundCorners( listOf<int>(1,2), guideWidth/2-0.5 ) val shape = (prongs + guide + tie) return shape.chamferedExtrude(maleThickness,chamfer) } @Piece fun female() : Shape3d { val height = femaleThickness.y*2 + maleThickness + clearance.y * 2 val width = fromSVG("prong").size.x + femaleThickness.x + 1 val geometry = alignY( "prongGeometry" ) val bodyRect = Square(width, (geometry.back + clearance.x + femaleThickness.x)*2) .centerY() val base = bodyRect.roundAllCorners(2) .chamferedExtrude( femaleThickness.y, chamfer, 0 ) + bodyRect.scaleX(0.5).roundAllCorners(2) .chamferedExtrude( femaleThickness.y + maleThickness + clearance.y*2, chamfer, 0) val cutout = alignY( "femaleCutout" ) .mirrorY().also() .chamferedExtrude( height + 2, -chamfer ).bottomTo(-0.01) val middleRect = Square( bodyRect.size.x + 2, bodyRect.size.y -femaleThickness.x*2 ) .centerY() .leftTo( base.left ) val middleCorners = Square(7).rotate(-110) .translateX( middleRect.left ).translateY( middleRect.back + femaleThickness.x*0.7) .mirrorY().also() val middle = (middleRect + middleCorners) .extrude( maleThickness + clearance.y * 2 ) .bottomTo( femaleThickness.y ) //return middle val guideShape = Square( base.size.x, femaleThickness.x) .frontTo( guideWidth/2 + clearance.x ) .mirrorY().also() val guide = guideShape .extrude( height - femaleThickness.y ).bottomTo(femaleThickness.y/2) val topShape = guideShape.boundingSquare() + (bodyRect.roundAllCorners(2) intersection Square( cutout.left+chamfer, 100 ).centerY()) val top = topShape .chamferedExtrude( femaleThickness.y, 0, chamfer ) .topTo( height ) // This allows the piece to be printed flat, so that the piece is strong. // (printing sitting up would be very weak). val cutoutTopShape = Square( base.right - femaleThickness.x - cutout.left, guide.front + base.back ) .leftTo( cutout.left + chamfer ).frontTo( -guide.front ) val cutoutTop = cutoutTopShape.mirrorY().also() .chamferedExtrude(femaleThickness.y+0.02, 0, -chamfer) .bottomTo(height - femaleThickness.y-0.01) .color("Blue") val tie = tie().mirrorX().leftTo(base.right - femaleThickness.x) val tieThickness = Math.min( top.top, maleThickness*1.5) val tie3d = tie .chamferedExtrude( tieThickness, chamfer ) val topBridgeShape = Square(bodyRect.size.x, alignY( "femaleCutout" ).front*2).roundAllCorners(2) .centerY() val topBridges = topBridgeShape .chamferedExtrude(femaleThickness.y-0.5, 0, chamfer) .topTo(height) + (topBridgeShape intersection tie.boundingSquare()).extrude( 0.5 + height - femaleThickness.y ) return base - middle + guide + tie3d + top + topBridges - cutout //+ cutoutTop // - tieOverhang } @Piece fun maleAndFemale() = arrangeX(2, male(), female() ) @Piece fun previewMaleAndFemale() : Shape3d { val male = male().translateZ( femaleThickness.y + clearance.y ).color("Green") val female = female() return male + female } @Piece fun tensioner() : Shape3d { val profile = doubleTie().center() val half = profile.chamferedExtrude( maleThickness, 0, chamfer ).rotateY(12) .intersection( Cube( 100 ).centerY() ) return half.mirrorX().also() } @Piece fun tensioner2() = tensioner( 6, false ) @Piece fun tensioner3() = tensioner( 6, true ) fun tensioner( height : double, extraPost : bool ) : Shape3d { val extraSize : double = if (extraPost) { slotSize + maleThickness } else { slotSize } val side = Square( slotSize * 2 + maleThickness * 3 + extraSize, height ) .roundCorners(listOf<int>( 0, 3 ), 1) .chamferedExtrude( femaleThickness.x, 1, 0 ) .rotateX(90) .bottomTo(0) .frontTo( webbingWidth/2 ) .leftTo( -slotSize - maleThickness*2 ) val sides = side.mirrorY().also() val postA = Square(maleThickness, maleThickness ) .roundAllCorners(1) .extrude(webbingWidth) .rotateX(90) .centerXY() .rightTo( - slotSize - maleThickness ).centerZTo(side.middle.z) val postB = if ( extraPost ) { postA.rightTo( 0 ).scaleZ(0.5).centerZTo(side.middle.z) } else { Cube(0) } val postD = Triangle( maleThickness, height ) .extrude(webbingWidth) .rotateX(90) .centerXY() .mirrorX() .rightTo( sides.right ) .mirrorZ().bottomTo(0) val postC = Triangle( maleThickness * 1.2 ).scaleY(0.6) .extrude(webbingWidth) .rotateX(90) .centerXY() .rightTo( postD.left - slotSize ) val lever = Circle( webbingWidth/2 ) .chamferedExtrude( femaleThickness.y, 0.75, 0 ) //.translateX( ) .intersection( Cube( 100 ).centerY().translateX( webbingWidth * 0.25 ) ) .leftTo(postD.right) return sides + postA + postB + postC + postD + lever } @Piece fun d() : Shape3d { val radius = 1 val shape = PolygonBuilder().apply { moveTo( 0, webbingWidth/2 ) circularArcTo( Vector2(0, -webbingWidth/2), webbingWidth*0.7, false, false ) lineBy( -webbingWidth*0.3, 0 ) lineBy( 0, webbingWidth ) }.build() Quality.increase(2) val curve = shape .roundCorner(-1, radius) .roundCorner(2, radius) .roundCorner(1, radius) .roundCorner(0, radius) val outer = curve.offset( maleThickness ) Quality.reset() return (outer - curve).chamferedExtrude( maleThickness, chamfer ) } @Piece fun doubleD() = d().rightTo(0).leftTo(1).also() @Piece fun sideHook() : Shape3d { val regular = Square( slotSize* 2.5 + maleThickness * 3, webbingWidth + maleThickness*3 ) .centerY().translateY( -maleThickness/2 ) .roundAllCorners(3) val slot = Square( slotSize, webbingWidth ) .centerY() .roundAllCorners(1) .leftTo( regular.left + maleThickness ) val slot2 = Square( slotSize*1.5, webbingWidth ) .centerY() .roundAllCorners(1) .rightTo( regular.right - maleThickness ) val foo = 1 val gap = Triangle(foo,maleThickness*3).mirrorY().mirrorX().also() .centerXTo(slot2.left+foo/4) .backTo( regular.back + maleThickness ) val hump = Circle(maleThickness*2) .frontTo(regular.front) .leftTo(regular.right - maleThickness*2 ) return (regular.hull(hump) - slot - slot2 - gap ) .chamferedExtrude( maleThickness*2, 0.5 ) } @Piece fun passthrough() : Shape3d { val tie = tensioner() val inside = ( Square( maleThickness + clearance.x*2, tie.size.y + clearance.x*2 ).center() + Square( maleThickness + clearance.x*2 + slotSize*2, webbingWidth + clearance.x*2 ).center() ).roundAllCorners(1) val outside = Square( inside.size.x + maleThickness*2 + clearance.x*2, inside.size.y + maleThickness*2 + clearance.x*2 ) .roundAllCorners(3) .centerTo( inside.middle ) val extra = (outside - inside).leftTo( tie.right - maleThickness ) return tie + extra.chamferedExtrude( maleThickness, chamfer ) } @Piece fun bridge() : Shape3d { return bridge( 15, 20, 0 ) } @Piece fun bridgeWithGuide() = bridge( 15, 20, 8 ).mirrorZ().bottomTo(0) fun bridge( extra : double, width : double, guideHeight : double ) : Shape3d { val main = Square( width, webbingWidth + extra*2 ).center() .roundAllCorners( width/3 ) .chamferedExtrude( slotSize + maleThickness + guideHeight, chamfer, 0 ) val slotChamfer = 3 val gap = Square( slotSize, webbingWidth ) .center() .chamferedExtrude( width + slotChamfer, -slotChamfer ) .rotateY(90) .centerX() .topTo( main.top + slotChamfer + 0.01 ) val guideGap = if (guideHeight > 0) { Square( guideHeight, webbingWidth ) .center() .chamferedExtrude( width + slotChamfer, -slotChamfer ) .rotateY(90) .centerX() .bottomTo( -slotChamfer - 0.01 ) } else { null } val screwHole = Countersink() .translateY(main.size.y / 2 - extra /2 ) .mirrorZ() .mirrorY().also() return main - gap - guideGap - screwHole } // Similar to 'bridge', but attached the other way up, so that the webbing // just site between the posts, rather than *threaded* through a slot. @Piece fun guide() = guide( Vector3(15, 10, 8 ) ) fun guide( size : Vector3 ) : Shape3d { val post = Square( size.x, size.y ).center() .roundAllCorners(2) .extrude( size.z ) val hole = Countersink() .topTo(post.top) val base = Square( size.x, webbingWidth + 4 ).center() .extrude(1) val posts = (post - hole).frontTo( webbingWidth/2 + 0.5 ) .mirrorY().also() return base + posts } override fun build() : Shape3d { return arrangeY(10, arrangeX( 10, bridge(), guide(), bridgeWithGuide() ).centerX(), arrangeX( 10, male(), female() ).centerX(), arrangeX( 10, tensioner(), tensioner2(), tensioner3() ).centerX(), arrangeX( 10, doubleD(), sideHook() ).centerX() ).centerY() } }