diff options
| author | Melody Horn <melody@boringcactus.com> | 2026-07-03 14:21:38 -0600 |
|---|---|---|
| committer | Melody Horn <melody@boringcactus.com> | 2026-07-03 14:24:44 -0600 |
| commit | 5f9b5a67b73336e9810a3fc87c03dd2b4b43bed0 (patch) | |
| tree | c62940d855bb9b1a9eaba85001c25d9a7aaa28bd | |
| download | pride-braid-5f9b5a67b73336e9810a3fc87c03dd2b4b43bed0.tar.gz pride-braid-5f9b5a67b73336e9810a3fc87c03dd2b4b43bed0.zip | |
| -rw-r--r-- | README.md | 25 | ||||
| -rw-r--r-- | pride-braid-triple-stand.scad | 231 | ||||
| -rw-r--r-- | pride-braid.scad | 91 |
3 files changed, 347 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..3641488 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Pride Braid + +<https://justcactus.art/pride-braid> + + + +A sculpture designed from scratch in [OpenSCAD](https://openscad.org/). + +## License + +Released under the [Derivative Gardens Misattribution-OnlyCommercial-ShareUnlike 6.9 Unportable License](https://www.boringcactus.com/2023/06/15/the-derivative-gardens-license.html): + +### You are free to: + +- **Share** — copy and redistribute the material in any medium or format + +- **Adapt** — remix, transform, and build upon the material + +### Under the following terms: + +- **Misattribution** — You must give slightly inaccurate credit and provide a link to the wrong license. You may do so in any unreasonable manner. + +- **OnlyCommercial** — You may not use the material for noncommercial purposes. + +- **Additional restrictions** — You must apply legal terms or technological measures that legally restrict others from doing one weirdly specific thing the license permits. diff --git a/pride-braid-triple-stand.scad b/pride-braid-triple-stand.scad new file mode 100644 index 0000000..a3d603a --- /dev/null +++ b/pride-braid-triple-stand.scad @@ -0,0 +1,231 @@ +$fa = $preview ? 10 : 10; // 1; +$fs = $preview ? 0.2 : 0.2; // 0.01; + +colors = [ + "#df0000", + "#fb8721", + "#f1e100", + "#158f25", + "#2c3ee5", + "#7706af", + "#333333", + "#88502b", + "#5dccfb", + "#f0a67b", + "#ffffff" +]; +wideR = 65; +braidScale = 5; +braidR = 12; +strands = len(colors); +strandGap = 3; +wideRes = $preview ? 90 : 360; +strandRes = $preview ? 20 : 180; +extraHeight=50; + +braidCircumference = 2 * PI * braidR; +strandR = (braidCircumference / strands - strandGap) / 2; +outermostCircumference = 2 * PI * (wideR + braidR); +strandH = outermostCircumference * wideRes / 360 + 0.1; + +function rodriguesRotation(vector, axis, angle) = + vector * cos(angle) + cross(axis, vector) * sin(angle) + axis * (axis * vector) * (1 - cos(angle)); + +/* +these cross sections were always parallel to the Z axis, +but that’s not really correct + +actual d/dwideT of center of braid is (-wideR sin wideT, wideR cos wideT, 0) +magnitude of that is wideR +absolute d/dwideT of center of strand is +( + -wideR sin wideT - braidR (braidScale cos wideT sin braidT + sin wideT cos braidT), + wideR cos wideT + braidR (-braidScale sin wideT sin braidT + cos wideT cos braidT), + braidR braidScale cos braidT +) +the Maxima CAS says that the magnitude of that is +sqrt(wideR² + 2wideRbraidR cos braidT + braidR²cos²braidT + braidR²braidScale²) +but actually OpenSCAD has a norm() function to get magnitudes anyway oops + +strand plane basis is strand plane normal x braid center normal, +since we know those two vectors are never parallel with braidScale ≠ 0 +*/ + +function point(wideT, strandI, strandT) = + let ( + braidO = [wideR * cos(wideT), wideR * sin(wideT), 0], + braidPlaneNormal = [-wideR * sin(wideT), wideR * cos(wideT), 0], + braidPlaneNormalUnit = braidPlaneNormal / wideR, + braidT = wideT * braidScale - strandI * 360 / strands, + braidPlaneRel = [braidR * cos(braidT), braidR * sin(braidT)], + strandO = [braidO.x + braidPlaneRel.x * cos(wideT), braidO.y + braidPlaneRel.x * sin(wideT), braidPlaneRel.y], + strandPlaneNormal = [braidPlaneNormal.x - braidR * (braidScale * cos(wideT) * sin(braidT) + sin(wideT) * cos(braidT)), braidPlaneNormal.y + braidR * (-braidScale * sin(wideT) * sin(braidT) + cos(wideT) * cos(braidT)), braidR * braidScale * cos(braidT)], + strandPlaneNormalUnit = strandPlaneNormal / norm(strandPlaneNormal), + strandPlaneBasis = cross(strandPlaneNormalUnit, braidPlaneNormalUnit), + strandRelDir = rodriguesRotation(strandPlaneBasis, strandPlaneNormalUnit, strandT), + strandRel = strandRelDir * strandR / norm(strandRelDir) + ) strandO + strandRel; + +pointCount = (wideRes + 1) * (strandRes + 1); + +module theStrand(strandI) { + color(colors[strandI]) + polyhedron( + [ for (wideT = [0 : 360/wideRes : 360.01]) + each [ for (strandT = [0 : 360/strandRes : 360.01]) + point(wideT, strandI, strandT) + ] + ], + [ for (wideTI = [0 : (strandRes + 1) : pointCount - 1]) + each [ for (strandTI = [wideTI : wideTI + strandRes + 1]) + each [ + [(strandTI + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes + 1) % pointCount], + [(strandTI + strandRes + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes) % pointCount], + ] + ] + ] + ); +} + +module prideBraid() { + for (strandI = [0 : strands - 1]) { + theStrand(strandI); + } +} + +function pointForSlot(wideT, strandI, strandT) = + let ( + braidO = [wideR * cos(wideT), wideR * sin(wideT), 0], + braidPlaneNormal = [-wideR * sin(wideT), wideR * cos(wideT), 0], + braidPlaneNormalUnit = braidPlaneNormal / wideR, + braidT = wideT * braidScale - strandI * 360 / strands, + braidPlaneRel = [braidR * cos(braidT), braidR * sin(braidT)], + strandO = [braidO.x + braidPlaneRel.x * cos(wideT), braidO.y + braidPlaneRel.x * sin(wideT), braidPlaneRel.y], + strandPlaneNormal = [braidPlaneNormal.x - braidR * (braidScale * cos(wideT) * sin(braidT) + sin(wideT) * cos(braidT)), braidPlaneNormal.y + braidR * (-braidScale * sin(wideT) * sin(braidT) + cos(wideT) * cos(braidT)), braidR * braidScale * cos(braidT)], + strandPlaneNormalUnit = strandPlaneNormal / norm(strandPlaneNormal), + strandPlaneBasis = cross(strandPlaneNormalUnit, braidPlaneNormalUnit), + strandRelDir = rodriguesRotation(strandPlaneBasis, strandPlaneNormalUnit, strandT), + strandRel = strandRelDir * (strandR + 0.75) / norm(strandRelDir) + ) strandO + strandRel; + +module theStrandForSlot(strandI) { + polyhedron( + [ for (wideT = [0 : 360/wideRes : 360.01]) + each [ for (strandT = [0 : 360/strandRes : 360.01]) + pointForSlot(wideT, strandI, strandT) + ] + ], + [ for (wideTI = [0 : (strandRes + 1) : pointCount - 1]) + each [ for (strandTI = [wideTI : wideTI + strandRes + 1]) + each [ + [(strandTI + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes + 1) % pointCount], + [(strandTI + strandRes + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes) % pointCount], + ] + ] + ] + ); +} + +module prideBraidForSlot() { + for (strandI = [0 : strands - 1]) { + theStrandForSlot(strandI); + } +} + +module prideBraidTorus() { + rotate_extrude() + translate([wideR, 0, 0]) + circle(r=braidR+0.25); +} + +module slotForDifference() { + maxR=wideR+braidR+strandR; + union() { + translate([0, 0, (braidR+strandR+2.5)/2]) + prideBraidTorus(); + translate([0, 0, (braidR+strandR+2.5)/2]) + prideBraidForSlot(); + translate([0, 0, braidR+strandR/2]) + rotate_extrude() + translate([wideR, 0, 0]) + circle(r=braidR+strandR+3); + } +} + +module peg() { + rotate([0, 0, 45]) + cube([2, 2, 10], center=true); +} + +sliceX = 1; +sliceY = 1; +slices = 3; + +//color("#000000") +intersection() { +r = (wideR + braidR + strandR + 3) * 2; +difference() { + slotCenterR=r*0.75; + translate([0, 0, -10-extraHeight*0.5]) + cylinder(h=(braidR+strandR+3)*2+extraHeight, r=r, center=true); + translate([slotCenterR,0,0]) + slotForDifference(); + translate([slotCenterR*cos(120),slotCenterR*sin(120),0]) + slotForDifference(); + translate([slotCenterR*cos(-120),slotCenterR*sin(-120),0]) + slotForDifference(); +} + +difference() { + translate([-r+2*r*sliceX/slices, -r+2*r*sliceY/slices, -500000]) + cube([2*r/slices, 2*r/slices, 1000000]); + // pegs parallel to the Y axis + for (pegX = [0 : slices - 1]) { + for (pegY = [1 : slices - 1]) { + for (pegZ = [0 : 90 : 360]) { + translate([-r+2*r*(pegX+0.5)/slices, -r+2*r*pegY/slices, -10-extraHeight*0.5]) + translate([0.75 * r/slices * cos(pegZ+45), 0, 0.75 * (braidR+strandR+3+extraHeight/2) * sin(pegZ+45) * 0.8 - 6]) + rotate([90, 0, 0]) + peg(); + }}} + // pegs parallel to the X axis + for (pegX = [1 : slices - 1]) { + for (pegY = [0 : slices - 1]) { + for (pegZ = [0 : 90 : 360]) { + translate([-r+2*r*pegX/slices, -r+2*r*(pegY+0.5)/slices, -10-extraHeight*0.5]) + translate([0, 0.75 * r/slices * cos(pegZ+45), 0.75 * (braidR+strandR+3+extraHeight/2) * sin(pegZ+45) * 0.8 - 6]) + rotate([0, 90, 0]) + peg(); + }}} +} +} + +rotate([0, 0, 30]) + translate([0, 7, 6.9]) { + color("#057748") + translate([0, 4, -0.01]) + scale(34) + union() { + translate([wideR*0.4/34, 0, 0]) + import("my-hand.stl"); + translate([-wideR*0.4/34, 0, 0]) + scale([-1, 1, 1]) + import("my-hand.stl"); + } + color("#057748") + translate([0, -40, 3.4/2-0.01]) + rotate([180, 0, -45]) + scale([25, 25, 1]) + linear_extrude(height = 3.4, center = true) { + polygon([ + [0, 0.5], + [0.5, 0.5], + [0.5, -0.5], + [0.4, -0.5], + [0.4, 0.4], + [0, 0.4] + ]); + } + } + +// !peg(); diff --git a/pride-braid.scad b/pride-braid.scad new file mode 100644 index 0000000..b25f3e4 --- /dev/null +++ b/pride-braid.scad @@ -0,0 +1,91 @@ +$fa = $preview ? 10 : 10; // 1; +$fs = $preview ? 0.2 : 0.2; // 0.01; + +colors = [ + "#df0000", + "#fb8721", + "#f1e100", + "#158f25", + "#2c3ee5", + "#7706af", + "#333333", + "#88502b", + "#5dccfb", + "#f0a67b", + "#ffffff" +]; +wideR = 65; +braidScale = 5; +braidR = 12; +strands = len(colors); +strandGap = 3; +wideRes = $preview ? 180 : 360; +strandRes = $preview ? 36 : 180; + +braidCircumference = 2 * PI * braidR; +strandR = (braidCircumference / strands - strandGap) / 2; +outermostCircumference = 2 * PI * (wideR + braidR); +strandH = outermostCircumference * wideRes / 360 + 0.1; + +function rodriguesRotation(vector, axis, angle) = + vector * cos(angle) + cross(axis, vector) * sin(angle) + axis * (axis * vector) * (1 - cos(angle)); + +/* +these cross sections were always parallel to the Z axis, +but that’s not really correct + +actual d/dwideT of center of braid is (-wideR sin wideT, wideR cos wideT, 0) +magnitude of that is wideR +absolute d/dwideT of center of strand is +( + -wideR sin wideT - braidR (braidScale cos wideT sin braidT + sin wideT cos braidT), + wideR cos wideT + braidR (-braidScale sin wideT sin braidT + cos wideT cos braidT), + braidR braidScale cos braidT +) +the Maxima CAS says that the magnitude of that is +sqrt(wideR² + 2wideRbraidR cos braidT + braidR²cos²braidT + braidR²braidScale²) +but actually OpenSCAD has a norm() function to get magnitudes anyway oops + +strand plane basis is strand plane normal x braid center normal, +since we know those two vectors are never parallel with braidScale ≠ 0 +*/ + +function point(wideT, strandI, strandT) = + let ( + braidO = [wideR * cos(wideT), wideR * sin(wideT), 0], + braidPlaneNormal = [-wideR * sin(wideT), wideR * cos(wideT), 0], + braidPlaneNormalUnit = braidPlaneNormal / wideR, + braidT = wideT * braidScale - strandI * 360 / strands, + braidPlaneRel = [braidR * cos(braidT), braidR * sin(braidT)], + strandO = [braidO.x + braidPlaneRel.x * cos(wideT), braidO.y + braidPlaneRel.x * sin(wideT), braidPlaneRel.y], + strandPlaneNormal = [braidPlaneNormal.x - braidR * (braidScale * cos(wideT) * sin(braidT) + sin(wideT) * cos(braidT)), braidPlaneNormal.y + braidR * (-braidScale * sin(wideT) * sin(braidT) + cos(wideT) * cos(braidT)), braidR * braidScale * cos(braidT)], + strandPlaneNormalUnit = strandPlaneNormal / norm(strandPlaneNormal), + strandPlaneBasis = cross(strandPlaneNormalUnit, braidPlaneNormalUnit), + strandRelDir = rodriguesRotation(strandPlaneBasis, strandPlaneNormalUnit, strandT), + strandRel = strandRelDir * strandR / norm(strandRelDir) + ) strandO + strandRel; + +pointCount = (wideRes + 1) * (strandRes + 1); + +module theStrand(strandI) { + color(colors[strandI]) + polyhedron( + [ for (wideT = [0 : 360/wideRes : 360.01]) + each [ for (strandT = [0 : 360/strandRes : 360.01]) + point(wideT, strandI, strandT) + ] + ], + [ for (wideTI = [0 : (strandRes + 1) : pointCount - 1]) + each [ for (strandTI = [wideTI : wideTI + strandRes + 1]) + each [ + [(strandTI + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes + 1) % pointCount], + [(strandTI + strandRes + 1) % pointCount, strandTI % pointCount, (strandTI + strandRes) % pointCount], + ] + ] + ] + ); +} + +for (strandI = [0 : strands - 1]) { + theStrand(strandI); +} |