Initial copy stand design
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
core
|
||||
core.*
|
||||
@@ -0,0 +1,159 @@
|
||||
# Copy Stand Base Plate
|
||||
|
||||
A parametric, 3D-printed copy-stand base sized for a Bambu Lab X2D and designed
|
||||
around a Lobster Holder V2.1 on its CS-Lite. The base uses a removable 2020
|
||||
extrusion socket, adjustable 1/4-20 feet, and an optical-breadboard-style
|
||||
1-inch grid of heat-set inserts.
|
||||
|
||||
The structural parts are intended for black ASA. Grid lines are flush blue ASA
|
||||
inlays printed against a textured build plate.
|
||||
|
||||
## Coordinate system and principal dimensions
|
||||
|
||||
- Base: 203.2 x 248 x 15 mm, with 12.7 mm corner radii.
|
||||
- The grid origin is Y=0 and positive Y runs toward the operator. The socket's
|
||||
straight front reference face is Y=12.7 mm (0.5 inch), halfway between the
|
||||
origin and the first insert row.
|
||||
- The 8 x 8 insert array has 25.4 mm spacing. Its rows are Y=25.4 through
|
||||
Y=203.2 mm relative to the socket datum.
|
||||
- A blue Y=0 datum line continues beneath the socket, leaving aligned visible
|
||||
segments on both sides.
|
||||
- The removable socket captures 50 mm of a nominal 20 x 20 mm extrusion.
|
||||
- Its 110 x 38.7 x 10 mm flange reaches from the rear plate edge to the
|
||||
half-inch reference face. The column is shifted forward to improve the
|
||||
overturning load path and reduce the camera arm's required overhang.
|
||||
- The two outside inserts in the front grid row also mount the front leveling
|
||||
feet. They cannot accept top-side accessories while the feet are installed.
|
||||
|
||||
## Hardware
|
||||
|
||||
| Qty | Item | Relevant dimensions |
|
||||
| ---: | --- | --- |
|
||||
| 70 | [McMaster 97171A240 brass 1/4-20 heat-set insert](https://www.mcmaster.com/97171A240/) | 9.525 mm body, 12.7 mm flange, 1.27 mm flange thickness, 8.026 mm maximum hole |
|
||||
| 4 | [1/4-20 adjustable foot](https://www.amazon.com/dp/B095PDSNPM) | 19 mm pad diameter, 8.5 mm pad height, 17.8 mm stud |
|
||||
| 4 | [Knurled 1/4-20 locking nut](https://www.amazon.com/dp/B0GQSX98V4) | 24.4 mm OD, 6 mm thick |
|
||||
| 1 | [European-style 2020 T-slot extrusion](https://www.amazon.com/dp/B08Y8N7FD1) | 20 x 20 x 400 mm, 6 mm slots |
|
||||
| 4 | 1/4-20 x 1-inch socket-head screws | Socket flange to base |
|
||||
| 4 | Black 1/4-inch flat washers, specified as 0.7 inch (17.78 mm) OD | Load distribution beneath socket screw heads; measure thickness before assembly |
|
||||
| 4 | M5 x 10 mm socket-head screws and European 2020 T-nuts | Extrusion clamp; verify screw length against the selected T-nut |
|
||||
|
||||
The four feet use four of the listed inserts: two dedicated rear inserts and
|
||||
the two shared front grid inserts. Four more dedicated inserts secure the
|
||||
socket. The remaining 64 inserts make up the complete grid.
|
||||
|
||||
The McMaster drawing requires at least 0.530 inch (13.46 mm) of material. Every
|
||||
insert boss in this design is 15 mm tall, including all four socket-mounting
|
||||
bosses. The 52.5 x 22.5 mm locating slot passes through the base between those
|
||||
bosses, so it does not reduce their thickness. Inserts install from the open
|
||||
underside with their flange flush against the boss. The 1.35 mm flange seat and
|
||||
9.525 mm body leave a 4.125 mm screw-clearance tunnel below the clean 6.8 mm
|
||||
opening in the working surface.
|
||||
|
||||
## Build
|
||||
|
||||
Requirements: OpenSCAD 2021.01 or later, Ruby, and Python 3.
|
||||
|
||||
```bash
|
||||
ruby build.rb
|
||||
```
|
||||
|
||||
The build is incremental. Run `REBUILD=1 ruby build.rb` to regenerate every
|
||||
component after changing geometry parameters.
|
||||
|
||||
Generated files are placed under `exports/3mf` and `exports/stl`. The primary
|
||||
files are:
|
||||
|
||||
- `base_multicolor.3mf`: aligned black base and blue inlay meshes, assigned to
|
||||
filaments 1 and 2.
|
||||
- `socket.3mf`: removable black socket and gusset assembly.
|
||||
- `coupon_multicolor.3mf`: two-piece insert, rail-clearance, through-key-fit,
|
||||
and line calibration coupon.
|
||||
- `coupon_key.3mf`: standalone full-size tongue gauge from the two-piece
|
||||
coupon.
|
||||
|
||||
The portable multipart files intentionally do not embed a printer or process
|
||||
profile. The individual aligned meshes and single-color fallbacks are retained
|
||||
alongside them.
|
||||
|
||||
## Calibrate before the full base
|
||||
|
||||
Print `coupon_multicolor.3mf` in the intended ASA, face down on the intended
|
||||
textured plate. It produces a main coupon and a separate full-size tongue
|
||||
gauge. From left to right, the insert pilots are 7.70, 7.85, 8.00, 8.15, and
|
||||
8.30 mm. The four rail gauges are 20.0, 20.1, 20.2, and 20.3 mm. Each is a
|
||||
four-wall collar with the production 6 mm side walls, 2.7 mm end walls, and
|
||||
full 50 mm engagement depth. External gussets and M5 holes are omitted because
|
||||
they do not define the rail-fit surfaces. The coupon also
|
||||
contains the production-width blue line and a 6 mm-deep production-size
|
||||
52.5 x 22.5 mm through-slot. The separate 52 x 22 x 6 mm tongue prints in the
|
||||
same orientation as the socket tongue and has a support-free stop shoulder and
|
||||
vertical grip for insertion and removal. Verify that the slot is visibly open,
|
||||
with no first-layer membrane, before testing the fit.
|
||||
|
||||
The production model defaults to the physically calibrated 8.10 mm insert
|
||||
pilot and 20.1 mm rail cavity. These values were selected from an ASA coupon:
|
||||
the 20.1 mm collar accepted the fully cooled rail through all 50 mm by hand
|
||||
without objectionable play, and the 8.10 mm pilot provided the preferred
|
||||
insert fit. The rectangular tongue and through-slot also passed the physical
|
||||
fit test, so their dimensions remain unchanged. For another printer, material,
|
||||
or process, select the smallest rail gauge that fits without forcing and use
|
||||
the insert pilot that gives the required installation grip. Change
|
||||
`insert_pilot_diameter` or `rail_clearance` at the top of `copy_stand.scad`,
|
||||
then rebuild. The 8.30 mm coupon remains a diagnostic oversize option;
|
||||
McMaster specifies 8.026 mm as the maximum nominal hole diameter, while this
|
||||
design's 8.10 mm production value reflects the tested print process.
|
||||
|
||||
## Bambu Studio setup
|
||||
|
||||
1. Open `base_multicolor.3mf` as one multipart object.
|
||||
2. Assign black ASA to Filament 1 and blue ASA to Filament 2.
|
||||
3. Keep the 248 mm dimension along the printer's Y axis and place the base to
|
||||
one side. Its 203.2 mm X width leaves about 52.8 mm for a nominal 35 x 35 mm
|
||||
prime tower and clearance.
|
||||
4. Confirm the tower and any brim fit in Preview before printing.
|
||||
5. Print the base face down. No support is designed into the base, socket, or
|
||||
coupon.
|
||||
|
||||
The coupon occupies approximately 228 x 110 x 53.2 mm including its separate
|
||||
tongue and full-depth rail collars. Orient its 228 mm dimension along Y to
|
||||
leave ample X space for the prime tower.
|
||||
|
||||
A 0.4 mm nozzle and 0.20 mm layers are the compatibility target. Use at least
|
||||
six walls, six top and bottom layers, and 40-50% gyroid or cubic infill for the
|
||||
socket. Use at least four walls for the base, suitable ASA bed adhesion, a
|
||||
fully heat-soaked enclosure, and conservative cooling. The modeled open cells
|
||||
provide the base structure; sparse slicer infill is not relied on there.
|
||||
|
||||
## Assembly
|
||||
|
||||
1. Heat-set the inserts from the underside. Stop when each brass flange is
|
||||
flush with its counterbore; do not pull it toward the textured top surface.
|
||||
2. Thread the leveling feet through their locking nuts and into the two rear
|
||||
foot inserts and two outer inserts of the front grid row.
|
||||
3. Lower the socket's 52 x 22 x 6 mm tongue into the base's through-slot, then
|
||||
fasten the flange with four 0.7-inch OD washers and four 1/4-20 x 1-inch
|
||||
screws. The screws provide clamping and vertical retention; the tongue
|
||||
provides repeatable X/Y location and carries lateral shear. With a washer
|
||||
thickness `t` in millimeters, nominal insert engagement is
|
||||
`11.275 - t` mm, limited by the insert's 9.525 mm threaded length.
|
||||
4. Preload two T-nuts in each side slot of the extrusion. Lower the extrusion
|
||||
to the socket floor and fasten it through the four M5 clamp holes.
|
||||
5. Level the base, then tighten each knurled locking nut against its insert
|
||||
flange.
|
||||
|
||||
The Lobster/CS-Lite locating fixture, Arca rail, camera carriage, and camera
|
||||
alignment accessories are deliberately deferred. They can attach to the grid
|
||||
without changing this base.
|
||||
|
||||
The socket's broad side buttresses stop before the four base screws. Vertical
|
||||
19 mm washer and tool envelopes preserve access above those screws, and 14 mm
|
||||
horizontal tunnels provide driver access to the M5 rail clamps without
|
||||
sacrificing the collar wall around the T-slot extrusion. The 19 mm fore/aft
|
||||
screw-row spacing and shallower side buttresses improve overturning and
|
||||
torsional resistance compared with the compact first revision.
|
||||
|
||||
Before mounting a camera, proof-test the assembled stand with a dummy load at
|
||||
the maximum working overhang. Under the normal payload, target less than 1 mm
|
||||
of rail-tip motion. After 24 hours at twice the intended payload, require no
|
||||
cracking, washer embedment, or loose fasteners and less than 0.25 mm permanent
|
||||
rail-tip displacement after unloading.
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 755.70758 647.99997" fill="#000000">
|
||||
<g transform="translate(-4.873836,-5.198043)">
|
||||
<g transform="translate(-337.27261,-73.468683)">
|
||||
<path d="m 693.64932,79.277393 c -1.95734,0.245333 -8.92667,1.101333 -15.28667,1.834666 -59.064,6.604 -121.43066,33.018671 -169.368,71.661331 -37.052,29.96 -65.54533,65.42267 -88.29066,110.18 -10.884,21.27866 -11.984,25.06933 -7.33733,24.70266 1.46666,-0.12266 9.66133,-0.24533 18.09866,-0.12266 l 15.408,0.12266 18.22,21.4 c 10.028,11.74 18.588,21.4 18.95467,21.4 0.368,0 1.10133,-3.91333 1.59066,-8.68266 0.36667,-4.76934 1.956,-13.94 3.30134,-20.42134 11.74,-55.02933 35.58533,-100.88666 72.516,-139.28533 23.724,-24.824 47.32533,-41.94533 79.48666,-57.84266 21.156,-10.393334 37.54267,-16.508001 58.82,-22.010667 l 13.452,-3.546667 -7.948,0.122667 c -4.40267,0.122666 -9.53867,0.366666 -11.61733,0.488"/>
|
||||
<path d="m 963.90398,116.08673 c -100.64267,8.43733 -217.18267,54.17333 -324.67333,127.30133 -22.13333,15.04133 -58.69733,42.06666 -58.69733,43.28933 0,0.61066 5.136,0.97866 11.372,0.97866 h 11.25066 l 14.064,-10.51733 c 18.58667,-13.94133 52.828,-36.808 73.98267,-49.404 21.768,-13.08533 70.56,-37.78666 93.54933,-47.448 82.78933,-34.72933 160.68666,-48.18 208.5,-36.196 19.07732,4.76934 35.34132,16.75334 41.94402,30.69334 3.0573,6.604 3.3026,8.19333 3.3026,24.21333 0,16.14133 -0.2453,17.732 -4.0346,28.73733 -10.028,29.104 -32.896,65.91333 -63.71336,102.72133 -4.76933,5.748 -8.68133,10.63867 -8.68133,11.00534 0,0.368 18.09867,0.48933 40.35469,0.24533 l 40.3546,-0.36667 v -18.34266 -18.344 l 7.8267,-10.63867 c 17.2427,-23.60133 32.4067,-50.74933 39.8667,-71.78266 20.9093,-59.06534 -2.936,-96.36267 -67.26,-105.16667 -11.6174,-1.59066 -45.49069,-2.20133 -59.30802,-0.97866"/>
|
||||
<path d="m 397.10266,402.60538 v 99.052 h 41.57733 V 365.91872 l 5.86933,6.48133 c 3.18,3.54667 17.36534,19.93333 31.55067,36.44267 14.18533,16.38666 35.34133,40.844 47.08,54.296 11.74,13.45066 23.968,27.636 27.27066,31.54933 l 5.86934,6.96933 h 21.27866 21.27734 v -99.052 -99.05199 h -21.52267 -21.644 l 0.61067,67.86933 c 0.24533,37.29733 0.24533,67.86933 -0.12267,67.86933 -0.36533,-0.12267 -2.68933,-2.568 -5.01333,-5.50267 -2.32267,-2.93466 -16.14134,-18.95466 -30.572,-35.58666 -41.33334,-47.692 -52.216,-60.28667 -67.25733,-77.89733 l -14.064,-16.508 -21.03334,-0.12267 -21.15466,-0.12133 v 99.05199"/>
|
||||
<path d="m 615.99598,402.60538 v 99.052 h 41.57867 V 369.22139 l 25.68,34.11733 c 14.06267,18.83333 30.32667,40.356 36.07467,47.93733 l 10.63866,13.81733 35.952,-47.936 35.952,-47.93599 0.24533,51.60533 0.36667,51.604 14.552,-12.34934 c 8.072,-6.848 17.24267,-14.79733 20.54533,-17.732 l 5.86934,-5.25866 v -66.76933 -66.768 h -21.03334 l -21.156,0.12133 -35.46266,47.08133 c -19.444,25.80267 -35.83067,46.836 -36.19733,46.71333 -0.73467,-0.36666 -16.63067,-21.03333 -54.17334,-71.04933 l -17.24266,-22.86666 h -21.03334 -21.156 v 99.05199"/>
|
||||
<path d="m 968.91731,383.89605 -47.56933,0.36667 -25.192,24.824 c -80.58667,79.12 -147.84533,129.13599 -225.74133,167.77733 -108.836,54.052 -203.85333,70.43866 -258.27066,44.51333 -11.25067,-5.38133 -23.11333,-15.53067 -28.12667,-23.968 -10.51733,-17.97733 -10.39466,-45.492 0.24534,-69.58133 3.79066,-8.68267 3.79066,-9.04933 -0.612,-9.04933 h -3.54667 l -0.36667,-10.76134 -0.36666,-10.64 -5.87067,9.784 c -14.42933,24.212 -26.168,52.95067 -29.592,72.516 -5.01467,28.37067 0.61067,49.40533 17.73067,66.28 10.15066,10.02667 18.95466,15.28666 33.996,20.29866 30.572,10.15067 71.29333,11.49467 115.68399,3.548 l 16.01867,-2.81333 14.67467,9.04933 c 55.51866,33.87334 114.33866,50.62667 177.43866,50.62667 54.78533,0 100.76533,-10.63867 149.43599,-34.484 55.15067,-27.14933 103.332,-70.68266 135.73866,-122.65466 7.4587,-11.98534 22.3787,-40.844 24.5787,-47.32667 l 1.224,-4.03466 -2.0787,3.05733 c -32.52932,46.95866 -68.60266,78.99733 -115.56132,102.35466 -68.60267,34.24 -147.968,41.21067 -219.13866,19.076 -12.352,-3.79066 -40.96667,-15.16266 -40.844,-16.14133 0,-0.36667 13.084,-7.21467 28.98133,-15.04133 15.89733,-7.82667 36.68667,-18.71067 46.10267,-24.33467 37.54133,-22.012 84.74533,-55.152 119.84133,-84.13333 l 13.452,-11.12933 0.36666,4.892 0.36667,4.89066 h 90.49333 90.36929 v -18.95466 -18.95334 h -69.70262 -69.70534 v -4.64666 c 0,-4.40267 0.98,-5.62667 16.99867,-20.78934 l 16.99733,-16.14133 h 39.86667 l 39.86529,-0.12267 v -19.56666 c 0,-10.63867 -0.1226,-19.32 -0.244,-19.19867 -0.2453,0.244 -21.76796,0.48934 -47.93729,0.612"/>
|
||||
<path d="m 455.18932,464.60538 v 53.56134 l -22.74533,0.612 -22.74667,0.61066 5.01467,11.00667 c 13.452,29.47066 32.89467,59.79866 52.58266,81.932 l 5.38134,5.992 17.732,-1.46667 c 19.19866,-1.59067 36.31866,-4.52533 58.57466,-10.15067 12.96267,-3.17866 43.53467,-12.84 45.00134,-14.184 0.24533,-0.24533 -2.20134,-2.81466 -5.50267,-5.62666 -36.07467,-30.08267 -64.56667,-68.60267 -82.42133,-111.28133 -5.50267,-12.84 -6.236,-13.94 -27.63734,-38.764 -12.10533,-14.064 -22.25599,-25.55867 -22.62266,-25.55867 -0.36667,-0.12267 -0.61067,23.968 -0.61067,53.31733"/>
|
||||
<g transform="matrix(0.53949817,0,0,0.53949817,789.88891,124.13937)">
|
||||
<path d="m 134.23594,367.14576 v 34.66667 h 69.06667 c 37.86666,0 71.06666,-0.4 73.86666,-0.8 4.4,-0.8 7.2,-3.73333 31.33333,-32.8 14.53334,-17.46667 26.53334,-32.4 26.8,-32.93333 0.26667,-0.66667 -0.93333,-1.46667 -2.66666,-2 -1.73334,-0.4 -47.06667,-0.8 -100.66667,-0.8 h -97.73333 z"/>
|
||||
<path d="m 134.90261,432.74576 c -0.4,0.93334 -0.53333,27.2 -0.4,58.4 l 0.4,56.53333 38.4,-37.06666 38.26666,-37.06667 -0.26666,-20.93333 -0.4,-20.8 -37.73334,-0.4 c -29.46666,-0.26667 -37.86666,0 -38.26666,1.33333 z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
require "open3"
|
||||
|
||||
ROOT = File.expand_path(__dir__)
|
||||
SCAD = File.join(ROOT, "copy_stand.scad")
|
||||
OPENSCAD = ENV.fetch("OPENSCAD", "openscad")
|
||||
PYTHON = ENV.fetch("PYTHON", "python3")
|
||||
EXPORT_3MF = File.join(ROOT, "exports", "3mf")
|
||||
EXPORT_STL = File.join(ROOT, "exports", "stl")
|
||||
COMPONENT_3MF = File.join(EXPORT_3MF, "components")
|
||||
COMPONENT_STL = File.join(EXPORT_STL, "components")
|
||||
|
||||
def run!(*command)
|
||||
puts command.join(" ")
|
||||
output, status = Open3.capture2e(*command)
|
||||
puts output unless output.empty?
|
||||
raise "Command failed: #{command.join(' ')}" unless status.success?
|
||||
end
|
||||
|
||||
def export(part, directory, extension)
|
||||
output = File.join(directory, "#{part}.#{extension}")
|
||||
return output if ENV["REBUILD"] != "1" && File.size?(output)
|
||||
|
||||
run!(OPENSCAD, "-o", output, "-D", %(part="#{part}"), SCAD)
|
||||
output
|
||||
end
|
||||
|
||||
def export_segment(part, index, directory, extension)
|
||||
output = File.join(directory, "#{part}_#{index}.#{extension}")
|
||||
return output if ENV["REBUILD"] != "1" && File.size?(output)
|
||||
|
||||
run!(OPENSCAD, "-o", output, "-D", %(part="#{part}"),
|
||||
"-D", "segment_index=#{index}", SCAD)
|
||||
output
|
||||
end
|
||||
|
||||
def package(output, title, bases, accents = [])
|
||||
command = [
|
||||
PYTHON, File.join(ROOT, "tools", "package_multimaterial_3mf.py"),
|
||||
output, "--title", title
|
||||
]
|
||||
bases.each { |path| command.concat(["--base", path]) }
|
||||
accents.each { |path| command.concat(["--accent", path]) }
|
||||
run!(*command)
|
||||
end
|
||||
|
||||
FileUtils.mkdir_p([EXPORT_3MF, EXPORT_STL, COMPONENT_3MF, COMPONENT_STL])
|
||||
|
||||
%w[base_accent socket coupon coupon_black coupon_key coupon_accent].each do |part|
|
||||
export(part, EXPORT_3MF, "3mf")
|
||||
end
|
||||
|
||||
%w[socket coupon coupon_key].each do |part|
|
||||
export(part, EXPORT_STL, "stl")
|
||||
end
|
||||
|
||||
base_segments = (0..5).map do |index|
|
||||
export_segment("base_segment", index, COMPONENT_3MF, "3mf")
|
||||
end
|
||||
black_segments = (0..5).map do |index|
|
||||
export_segment("base_black_segment", index, COMPONENT_3MF, "3mf")
|
||||
end
|
||||
|
||||
package(File.join(EXPORT_3MF, "base.3mf"), "Copy Stand Base - Single Color",
|
||||
base_segments)
|
||||
package(File.join(EXPORT_3MF, "base_black.3mf"), "Copy Stand Base - Black Mesh",
|
||||
black_segments)
|
||||
package(File.join(EXPORT_3MF, "base_multicolor.3mf"), "Copy Stand Base",
|
||||
black_segments, [File.join(EXPORT_3MF, "base_accent.3mf")])
|
||||
|
||||
package(File.join(EXPORT_3MF, "coupon_multicolor.3mf"),
|
||||
"Copy Stand Calibration Coupon",
|
||||
[File.join(EXPORT_3MF, "coupon_black.3mf"),
|
||||
File.join(EXPORT_3MF, "coupon_key.3mf")],
|
||||
[File.join(EXPORT_3MF, "coupon_accent.3mf")])
|
||||
|
||||
# A multi-solid ASCII STL fallback avoids forcing OpenSCAD/CGAL to union the
|
||||
# large stepped base in one expensive operation. Slicers merge the touching
|
||||
# solids at slice time.
|
||||
segment_stls = (0..5).map do |index|
|
||||
export_segment("base_segment", index, COMPONENT_STL, "stl")
|
||||
end
|
||||
File.open(File.join(EXPORT_STL, "base.stl"), "wb") do |combined|
|
||||
segment_stls.each { |path| combined.write(File.binread(path)) }
|
||||
end
|
||||
|
||||
puts "Exports written to #{File.join(ROOT, 'exports')}"
|
||||
@@ -0,0 +1,628 @@
|
||||
/*
|
||||
* Parametric copy-stand base for a Bambu Lab X2D.
|
||||
* Service coordinates: X left-to-right, Y rear-to-front, Z bottom-to-top.
|
||||
* The grid origin is at plate Y=datum_y. The socket's straight front edge is
|
||||
* 0.5 inch forward of that origin and remains a convenient reference face.
|
||||
*/
|
||||
|
||||
/* [Output] */
|
||||
part = "assembly"; // [assembly,base,base_black,base_segment,base_black_segment,base_accent,socket,coupon,coupon_black,coupon_key,coupon_accent]
|
||||
segment_index = 0; // [0:1:5]
|
||||
show_references = true;
|
||||
|
||||
/* [Base] */
|
||||
plate_width = 203.2;
|
||||
plate_depth = 248;
|
||||
plate_height = 15;
|
||||
corner_radius = 12.7;
|
||||
datum_y = 26;
|
||||
top_skin = 3.2;
|
||||
perimeter_wall = 3.2;
|
||||
rib_width = 2.4;
|
||||
|
||||
/* [Grid inlay] */
|
||||
grid_pitch = 25.4;
|
||||
grid_columns = 8;
|
||||
grid_rows = 8;
|
||||
grid_line_width = 0.8;
|
||||
inlay_depth = 0.6;
|
||||
inlay_clearance = 0.08;
|
||||
|
||||
/* [McMaster 97171A240 inserts] */
|
||||
insert_pilot_diameter = 8.1;
|
||||
insert_body_length = 9.525;
|
||||
insert_flange_diameter = 12.9;
|
||||
insert_flange_depth = 1.35;
|
||||
screw_clearance_diameter = 6.8;
|
||||
insert_boss_diameter = 20;
|
||||
|
||||
/* [Feet] */
|
||||
foot_pad_diameter = 19;
|
||||
foot_pad_height = 8.5;
|
||||
foot_stud_length = 17.8;
|
||||
foot_nut_diameter = 24.4;
|
||||
foot_nut_height = 6;
|
||||
|
||||
/* [Removable 2020 socket] */
|
||||
rail_size = 20;
|
||||
rail_clearance = 0.05;
|
||||
socket_capture = 50;
|
||||
socket_front_y = grid_pitch / 2;
|
||||
socket_rear_y = -datum_y;
|
||||
socket_flange_size = [110, socket_front_y - socket_rear_y, 10];
|
||||
socket_key_size = [52, 22, 6];
|
||||
socket_key_clearance = 0.25;
|
||||
socket_side_wall = 6;
|
||||
socket_end_wall = 2.7;
|
||||
socket_mount_x = 45;
|
||||
socket_mount_ys = [-16, 3];
|
||||
socket_mount_tool_diameter = 19;
|
||||
socket_gusset_outer_x = 35;
|
||||
socket_gusset_y_span = [-23, 10];
|
||||
socket_gusset_height = 32;
|
||||
rail_screw_diameter = 5.5;
|
||||
rail_screw_head_diameter = 9.5;
|
||||
rail_screw_head_depth = 3;
|
||||
rail_tool_diameter = 14;
|
||||
rail_screw_heights = [15, 40];
|
||||
|
||||
/* [Calibration] */
|
||||
coupon_insert_diameters = [7.70, 7.85, 8.00, 8.15, 8.30];
|
||||
coupon_rail_gaps = [20.0, 20.1, 20.2, 20.3];
|
||||
|
||||
$fn = $preview ? 36 : 72;
|
||||
eps = 0.02;
|
||||
|
||||
assert(plate_width <= 256 && plate_depth <= 256,
|
||||
"Base exceeds the X2D main-nozzle build area");
|
||||
assert(plate_height >= 13.462,
|
||||
"Insert bosses violate McMaster's 0.530-inch minimum material thickness");
|
||||
assert(top_skin > inlay_depth,
|
||||
"Top skin must be thicker than the inlay");
|
||||
assert(grid_columns == 8 && grid_rows == 8,
|
||||
"This release's shared front-foot geometry expects an 8 x 8 grid");
|
||||
assert(socket_key_size[0] / 2 + socket_key_clearance
|
||||
< socket_mount_x - insert_boss_diameter / 2,
|
||||
"Socket key slot overlaps a full-depth mounting-insert boss");
|
||||
assert(socket_gusset_outer_x
|
||||
< socket_mount_x - socket_mount_tool_diameter / 2,
|
||||
"Socket gusset enters a flange-screw tool-access envelope");
|
||||
assert(socket_front_y == grid_pitch / 2,
|
||||
"Socket reference face must remain at the half-inch grid coordinate");
|
||||
assert(socket_rear_y == -datum_y,
|
||||
"Socket flange must reach the rear edge of the base");
|
||||
assert(socket_mount_x + socket_mount_tool_diameter / 2
|
||||
<= socket_flange_size[0] / 2,
|
||||
"Socket washer envelope exceeds the flange width");
|
||||
assert(socket_mount_ys[0] - socket_mount_tool_diameter / 2
|
||||
>= socket_rear_y
|
||||
&& socket_mount_ys[len(socket_mount_ys) - 1]
|
||||
+ socket_mount_tool_diameter / 2 <= socket_front_y,
|
||||
"Socket washer envelope exceeds the flange depth");
|
||||
assert(socket_rail_axis_y() + socket_key_size[1] / 2
|
||||
+ socket_key_clearance
|
||||
< grid_pitch - insert_boss_diameter / 2,
|
||||
"Socket key slot overlaps the first grid-row insert bosses");
|
||||
assert(socket_mount_ys[len(socket_mount_ys) - 1]
|
||||
+ insert_boss_diameter / 2
|
||||
< grid_pitch - insert_boss_diameter / 2,
|
||||
"Socket mounting bosses overlap the first grid-row bosses");
|
||||
assert(socket_key_size[2] < plate_height,
|
||||
"Socket tongue must stop above the base underside");
|
||||
|
||||
grid_xs = [for (i = [0 : grid_columns - 1])
|
||||
plate_width / 2 + (i - (grid_columns - 1) / 2) * grid_pitch];
|
||||
grid_ys = [for (i = [1 : grid_rows]) datum_y + i * grid_pitch];
|
||||
rear_foot_ys = corner_radius;
|
||||
socket_hole_xs = [plate_width / 2 - socket_mount_x,
|
||||
plate_width / 2 + socket_mount_x];
|
||||
socket_hole_ys = [for (y = socket_mount_ys) datum_y + y];
|
||||
all_insert_points = concat(
|
||||
[for (x = grid_xs, y = grid_ys) [x, y]],
|
||||
[for (x = [grid_xs[0], grid_xs[len(grid_xs) - 1]]) [x, rear_foot_ys]],
|
||||
[for (x = socket_hole_xs, y = socket_hole_ys) [x, y]]
|
||||
);
|
||||
|
||||
module rounded_rect_2d(size, radius) {
|
||||
offset(r = radius)
|
||||
square([size[0] - 2 * radius, size[1] - 2 * radius], center = true);
|
||||
}
|
||||
|
||||
module rounded_prism(size, radius) {
|
||||
linear_extrude(height = size[2])
|
||||
rounded_rect_2d([size[0], size[1]], radius);
|
||||
}
|
||||
|
||||
module plate_outline_2d(inset = 0) {
|
||||
translate([plate_width / 2, plate_depth / 2])
|
||||
offset(delta = -inset)
|
||||
rounded_rect_2d([plate_width, plate_depth], corner_radius);
|
||||
}
|
||||
|
||||
module insert_boss_at(x, y) {
|
||||
translate([x, y, 0]) cylinder(h = plate_height, d = insert_boss_diameter);
|
||||
}
|
||||
|
||||
module insert_positive_bosses() {
|
||||
for (p = all_insert_points) insert_boss_at(p[0], p[1]);
|
||||
}
|
||||
|
||||
module underside_structure_2d() {
|
||||
intersection() {
|
||||
plate_outline_2d();
|
||||
union() {
|
||||
difference() {
|
||||
plate_outline_2d();
|
||||
plate_outline_2d(perimeter_wall);
|
||||
}
|
||||
for (x = grid_xs)
|
||||
translate([x - rib_width / 2, corner_radius, 0])
|
||||
square([rib_width, plate_depth - 2 * corner_radius]);
|
||||
for (y = concat([datum_y], grid_ys))
|
||||
translate([corner_radius, y - rib_width / 2, 0])
|
||||
square([plate_width - 2 * corner_radius, rib_width]);
|
||||
|
||||
translate([corner_radius, rear_foot_ys - rib_width / 2, 0])
|
||||
square([plate_width - 2 * corner_radius, rib_width]);
|
||||
for (x = socket_hole_xs)
|
||||
translate([x - rib_width / 2, corner_radius, 0])
|
||||
square([rib_width, datum_y - corner_radius]);
|
||||
|
||||
for (p = all_insert_points)
|
||||
translate(p) circle(d = insert_boss_diameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module base_structure_positive() {
|
||||
union() {
|
||||
linear_extrude(height = plate_height - top_skin)
|
||||
underside_structure_2d();
|
||||
translate([0, 0, plate_height - top_skin])
|
||||
linear_extrude(height = top_skin) plate_outline_2d();
|
||||
}
|
||||
}
|
||||
|
||||
module one_insert_cutout(x, y, pilot = insert_pilot_diameter) {
|
||||
translate([x, y, -eps])
|
||||
cylinder(h = insert_flange_depth + eps, d = insert_flange_diameter);
|
||||
translate([x, y, insert_flange_depth - eps])
|
||||
cylinder(h = insert_body_length + 2 * eps, d = pilot);
|
||||
translate([x, y, insert_flange_depth + insert_body_length - eps])
|
||||
cylinder(
|
||||
h = plate_height - insert_flange_depth - insert_body_length + 2 * eps,
|
||||
d = screw_clearance_diameter
|
||||
);
|
||||
}
|
||||
|
||||
module all_insert_cutouts() {
|
||||
union() {
|
||||
for (p = all_insert_points)
|
||||
translate([p[0], p[1], -eps])
|
||||
cylinder(h = insert_flange_depth + eps,
|
||||
d = insert_flange_diameter);
|
||||
}
|
||||
union() {
|
||||
for (p = all_insert_points)
|
||||
translate([p[0], p[1], insert_flange_depth - eps])
|
||||
cylinder(h = insert_body_length + 2 * eps,
|
||||
d = insert_pilot_diameter);
|
||||
}
|
||||
union() {
|
||||
for (p = all_insert_points)
|
||||
translate([p[0], p[1],
|
||||
insert_flange_depth + insert_body_length - eps])
|
||||
cylinder(
|
||||
h = plate_height - insert_flange_depth
|
||||
- insert_body_length + 2 * eps,
|
||||
d = screw_clearance_diameter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module insert_pattern_2d(diameter) {
|
||||
for (p = all_insert_points) translate(p) circle(d = diameter);
|
||||
}
|
||||
|
||||
module socket_key_2d(clearance = 0) {
|
||||
translate([plate_width / 2, datum_y + socket_rail_axis_y()])
|
||||
rounded_rect_2d(
|
||||
[socket_key_size[0] + 2 * clearance,
|
||||
socket_key_size[1] + 2 * clearance],
|
||||
2 + clearance
|
||||
);
|
||||
}
|
||||
|
||||
module grid_artwork_2d(clearance = 0) {
|
||||
intersection() {
|
||||
plate_outline_2d();
|
||||
offset(delta = clearance)
|
||||
union() {
|
||||
for (x = grid_xs)
|
||||
translate([x - grid_line_width / 2, datum_y])
|
||||
square([grid_line_width, plate_depth - datum_y]);
|
||||
for (y = concat([datum_y], grid_ys))
|
||||
translate([0, y - grid_line_width / 2])
|
||||
square([plate_width, grid_line_width]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module accent_service() {
|
||||
translate([0, 0, plate_height - inlay_depth])
|
||||
linear_extrude(height = inlay_depth)
|
||||
difference() {
|
||||
grid_artwork_2d();
|
||||
insert_pattern_2d(screw_clearance_diameter);
|
||||
socket_key_2d(socket_key_clearance);
|
||||
}
|
||||
}
|
||||
|
||||
module base_layer_2d(outline = false, hole_diameter, artwork = false) {
|
||||
difference() {
|
||||
if (outline) plate_outline_2d();
|
||||
else underside_structure_2d();
|
||||
insert_pattern_2d(hole_diameter);
|
||||
// The locator is a through-slot so no unsupported bridge is printed
|
||||
// against the textured build plate.
|
||||
socket_key_2d(socket_key_clearance);
|
||||
if (artwork) grid_artwork_2d(inlay_clearance);
|
||||
}
|
||||
}
|
||||
|
||||
module base_segment_service(index, with_artwork = false) {
|
||||
underside_top = plate_height - top_skin;
|
||||
body_top = insert_flange_depth + insert_body_length;
|
||||
artwork_bottom = plate_height - inlay_depth;
|
||||
top_split = (underside_top + artwork_bottom) / 2;
|
||||
|
||||
if (index == 0)
|
||||
linear_extrude(height = insert_flange_depth)
|
||||
base_layer_2d(false, insert_flange_diameter);
|
||||
else if (index == 1)
|
||||
translate([0, 0, insert_flange_depth])
|
||||
linear_extrude(height = insert_body_length)
|
||||
base_layer_2d(false, insert_pilot_diameter);
|
||||
else if (index == 2)
|
||||
translate([0, 0, body_top])
|
||||
linear_extrude(height = underside_top - body_top)
|
||||
base_layer_2d(false, screw_clearance_diameter);
|
||||
else if (index == 3)
|
||||
translate([0, 0, underside_top])
|
||||
linear_extrude(height = top_split - underside_top)
|
||||
base_layer_2d(true, screw_clearance_diameter);
|
||||
else if (index == 4)
|
||||
translate([0, 0, top_split])
|
||||
linear_extrude(height = artwork_bottom - top_split)
|
||||
base_layer_2d(true, screw_clearance_diameter);
|
||||
else if (index == 5)
|
||||
translate([0, 0, artwork_bottom])
|
||||
linear_extrude(height = inlay_depth)
|
||||
base_layer_2d(true, screw_clearance_diameter, with_artwork);
|
||||
else assert(false, str("Unknown base segment: ", index));
|
||||
}
|
||||
|
||||
module base_layered_service(with_artwork = false) {
|
||||
for (i = [0 : 5]) base_segment_service(i, with_artwork);
|
||||
}
|
||||
|
||||
module base_black_service() {
|
||||
base_layered_service(true);
|
||||
}
|
||||
|
||||
module base_complete_service() {
|
||||
base_layered_service(false);
|
||||
}
|
||||
|
||||
module print_orient_base() {
|
||||
translate([0, plate_depth, plate_height])
|
||||
rotate([180, 0, 0]) children();
|
||||
}
|
||||
|
||||
module socket_flange_2d() {
|
||||
translate([0, (socket_front_y + socket_rear_y) / 2])
|
||||
rounded_rect_2d([socket_flange_size[0], socket_flange_size[1]], 3);
|
||||
}
|
||||
|
||||
function socket_outer_y() = rail_size + 2 * rail_clearance
|
||||
+ 2 * socket_end_wall;
|
||||
function socket_rail_axis_y() = socket_front_y - socket_outer_y() / 2;
|
||||
|
||||
module socket_key_local_2d() {
|
||||
translate([0, socket_rail_axis_y()])
|
||||
rounded_rect_2d([socket_key_size[0], socket_key_size[1]], 2);
|
||||
}
|
||||
|
||||
module socket_gusset(side = 1) {
|
||||
cavity = rail_size + 2 * rail_clearance;
|
||||
collar_half_x = (cavity + 2 * socket_side_wall) / 2;
|
||||
near_x = side > 0 ? collar_half_x : -collar_half_x - 2;
|
||||
outer_x = side > 0 ? socket_gusset_outer_x - 2
|
||||
: -socket_gusset_outer_x;
|
||||
gusset_depth = socket_gusset_y_span[1] - socket_gusset_y_span[0];
|
||||
|
||||
// Broad buttress with a short outer reach. Tool-access bores below remove
|
||||
// any material that could obstruct either family of fasteners.
|
||||
hull() {
|
||||
translate([near_x, socket_gusset_y_span[0], socket_flange_size[2]])
|
||||
cube([2, gusset_depth, socket_gusset_height]);
|
||||
translate([outer_x, socket_gusset_y_span[0], socket_flange_size[2]])
|
||||
cube([2, gusset_depth, 2]);
|
||||
}
|
||||
}
|
||||
|
||||
module socket_positive() {
|
||||
cavity = rail_size + 2 * rail_clearance;
|
||||
outer_x = cavity + 2 * socket_side_wall;
|
||||
outer_y = socket_outer_y();
|
||||
|
||||
translate([0, 0, -socket_key_size[2]])
|
||||
linear_extrude(height = socket_key_size[2] + eps)
|
||||
socket_key_local_2d();
|
||||
linear_extrude(height = socket_flange_size[2]) socket_flange_2d();
|
||||
translate([-outer_x / 2, socket_front_y - outer_y,
|
||||
socket_flange_size[2]])
|
||||
cube([outer_x, outer_y, socket_capture]);
|
||||
socket_gusset(-1);
|
||||
socket_gusset(1);
|
||||
}
|
||||
|
||||
module socket_cutouts() {
|
||||
cavity = rail_size + 2 * rail_clearance;
|
||||
collar_half_x = (cavity + 2 * socket_side_wall) / 2;
|
||||
translate([-cavity / 2, socket_rail_axis_y() - cavity / 2,
|
||||
socket_flange_size[2]])
|
||||
cube([cavity, cavity, socket_capture + eps]);
|
||||
|
||||
// Four base mounting holes with clear vertical driver access.
|
||||
for (x = [-socket_mount_x, socket_mount_x], y = socket_mount_ys) {
|
||||
translate([x, y, -socket_key_size[2] - eps])
|
||||
cylinder(h = socket_flange_size[2] + socket_key_size[2] + 2 * eps,
|
||||
d = screw_clearance_diameter);
|
||||
translate([x, y, socket_flange_size[2]])
|
||||
cylinder(h = socket_gusset_height + eps,
|
||||
d = socket_mount_tool_diameter);
|
||||
}
|
||||
|
||||
// Two screw levels through both side walls create four M5 clamp points.
|
||||
for (h = rail_screw_heights) {
|
||||
translate([-socket_flange_size[0] / 2, socket_rail_axis_y(),
|
||||
socket_flange_size[2] + h])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = socket_flange_size[0], d = rail_screw_diameter);
|
||||
translate([-collar_half_x - eps, socket_rail_axis_y(),
|
||||
socket_flange_size[2] + h])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = rail_screw_head_depth + eps,
|
||||
d = rail_screw_head_diameter);
|
||||
translate([collar_half_x - rail_screw_head_depth,
|
||||
socket_rail_axis_y(),
|
||||
socket_flange_size[2] + h])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = rail_screw_head_depth + eps,
|
||||
d = rail_screw_head_diameter);
|
||||
|
||||
// Driver tunnels exist only outside the collar, preserving the side
|
||||
// wall while allowing a socket driver through each buttress.
|
||||
translate([-socket_gusset_outer_x - eps, socket_rail_axis_y(),
|
||||
socket_flange_size[2] + h])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = socket_gusset_outer_x - collar_half_x + 2 * eps,
|
||||
d = rail_tool_diameter);
|
||||
translate([collar_half_x - eps, socket_rail_axis_y(),
|
||||
socket_flange_size[2] + h])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = socket_gusset_outer_x - collar_half_x + 2 * eps,
|
||||
d = rail_tool_diameter);
|
||||
}
|
||||
}
|
||||
|
||||
module socket_service() {
|
||||
difference() {
|
||||
socket_positive();
|
||||
socket_cutouts();
|
||||
}
|
||||
}
|
||||
|
||||
module socket_print() {
|
||||
translate([socket_flange_size[0] / 2, -socket_rear_y,
|
||||
socket_key_size[2]]) socket_service();
|
||||
}
|
||||
|
||||
module reference_rail() {
|
||||
color([0.08, 0.08, 0.09, 0.8])
|
||||
translate([plate_width / 2 - rail_size / 2,
|
||||
datum_y + socket_rail_axis_y() - rail_size / 2,
|
||||
plate_height + socket_flange_size[2]])
|
||||
cube([rail_size, rail_size, 400]);
|
||||
}
|
||||
|
||||
module reference_feet() {
|
||||
foot_points = concat(
|
||||
[[grid_xs[0], rear_foot_ys],
|
||||
[grid_xs[len(grid_xs)-1], rear_foot_ys]],
|
||||
[[grid_xs[0], grid_ys[len(grid_ys)-1]],
|
||||
[grid_xs[len(grid_xs)-1], grid_ys[len(grid_ys)-1]]]
|
||||
);
|
||||
for (p = foot_points) {
|
||||
color([0.05, 0.05, 0.05, 0.8]) {
|
||||
translate([p[0], p[1], -foot_nut_height])
|
||||
cylinder(h = foot_nut_height, d = foot_nut_diameter);
|
||||
translate([p[0], p[1], -foot_nut_height - foot_pad_height])
|
||||
cylinder(h = foot_pad_height, d = foot_pad_diameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two-piece calibration coupon, modeled directly in print orientation.
|
||||
coupon_size = [165, 110, 3.2];
|
||||
coupon_boss_height = 15;
|
||||
coupon_rail_xs = [20, 60, 100, 140];
|
||||
coupon_rail_y = 23;
|
||||
coupon_fit_center = [82.5, 90];
|
||||
coupon_fit_outer = [64, 34];
|
||||
coupon_fit_height = 6;
|
||||
coupon_key_center = [200, 90];
|
||||
coupon_key_cap = [56, 26, 2];
|
||||
coupon_key_handle = [40, 6, 8];
|
||||
|
||||
assert(coupon_fit_height == socket_key_size[2],
|
||||
"Coupon socket depth must match the production tongue");
|
||||
assert(len(coupon_rail_gaps) == len(coupon_rail_xs),
|
||||
"Each rail gauge must have one layout position");
|
||||
assert(len([for (gap = coupon_rail_gaps)
|
||||
if (gap == rail_size + 2 * rail_clearance) gap]) == 1,
|
||||
"Production rail cavity must appear once in the coupon gauges");
|
||||
assert(coupon_rail_xs[0]
|
||||
- (coupon_rail_gaps[len(coupon_rail_gaps) - 1]
|
||||
+ 2 * socket_side_wall) / 2 >= 0
|
||||
&& coupon_rail_xs[len(coupon_rail_xs) - 1]
|
||||
+ (coupon_rail_gaps[len(coupon_rail_gaps) - 1]
|
||||
+ 2 * socket_side_wall) / 2 <= coupon_size[0],
|
||||
"Rail gauges exceed the main coupon width");
|
||||
assert(coupon_fit_center[0] - coupon_fit_outer[0] / 2 >= 0
|
||||
&& coupon_fit_center[0] + coupon_fit_outer[0] / 2 <= coupon_size[0]
|
||||
&& coupon_fit_center[1] - coupon_fit_outer[1] / 2 >= 0
|
||||
&& coupon_fit_center[1] + coupon_fit_outer[1] / 2 <= coupon_size[1],
|
||||
"Coupon fit socket exceeds the main coupon outline");
|
||||
assert(coupon_key_center[0] - coupon_key_cap[0] / 2 > coupon_size[0],
|
||||
"Coupon tongue must remain a physically separate print");
|
||||
assert(coupon_key_center[0] + coupon_key_cap[0] / 2 <= 256
|
||||
&& coupon_key_center[1] + coupon_key_cap[1] / 2 <= 256,
|
||||
"Coupon and separate tongue exceed the X2D build area");
|
||||
|
||||
module coupon_outline_2d() {
|
||||
translate([coupon_size[0] / 2, coupon_size[1] / 2])
|
||||
rounded_rect_2d([coupon_size[0], coupon_size[1]], 4);
|
||||
}
|
||||
|
||||
module coupon_accent_2d(clearance = 0) {
|
||||
offset(delta = clearance)
|
||||
translate([8, 6]) square([75, grid_line_width]);
|
||||
}
|
||||
|
||||
module coupon_positive() {
|
||||
linear_extrude(height = coupon_size[2]) coupon_outline_2d();
|
||||
for (i = [0 : len(coupon_insert_diameters) - 1])
|
||||
translate([14 + i * 24, 56, coupon_size[2]])
|
||||
cylinder(h = coupon_boss_height - coupon_size[2], d = insert_boss_diameter);
|
||||
|
||||
// Four production-style closed collars, left-to-right 20.0-20.3 mm.
|
||||
for (i = [0 : len(coupon_rail_gaps) - 1]) {
|
||||
gap = coupon_rail_gaps[i];
|
||||
outer_x = gap + 2 * socket_side_wall;
|
||||
outer_y = gap + 2 * socket_end_wall;
|
||||
translate([coupon_rail_xs[i], coupon_rail_y,
|
||||
coupon_size[2] - eps])
|
||||
difference() {
|
||||
translate([-outer_x / 2, -outer_y / 2, 0])
|
||||
cube([outer_x, outer_y, socket_capture + eps]);
|
||||
translate([-gap / 2, -gap / 2, -eps])
|
||||
cube([gap, gap, socket_capture + 3 * eps]);
|
||||
}
|
||||
}
|
||||
|
||||
// Full-depth test socket surrounding the production-size through-slot.
|
||||
translate([coupon_fit_center[0], coupon_fit_center[1],
|
||||
coupon_size[2] - eps])
|
||||
rounded_prism(
|
||||
[coupon_fit_outer[0], coupon_fit_outer[1],
|
||||
coupon_fit_height - coupon_size[2] + eps],
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
module coupon_common_cutouts() {
|
||||
for (i = [0 : len(coupon_insert_diameters) - 1]) {
|
||||
x = 14 + i * 24;
|
||||
pilot = coupon_insert_diameters[i];
|
||||
translate([x, 56, coupon_boss_height - insert_flange_depth])
|
||||
cylinder(h = insert_flange_depth + eps, d = insert_flange_diameter);
|
||||
translate([x, 56, coupon_boss_height - insert_flange_depth - insert_body_length])
|
||||
cylinder(h = insert_body_length + eps, d = pilot);
|
||||
translate([x, 56, -eps])
|
||||
cylinder(h = coupon_boss_height - insert_flange_depth
|
||||
- insert_body_length + 2 * eps,
|
||||
d = screw_clearance_diameter);
|
||||
}
|
||||
translate([coupon_fit_center[0], coupon_fit_center[1], -eps])
|
||||
rounded_prism(
|
||||
[socket_key_size[0] + 2 * socket_key_clearance,
|
||||
socket_key_size[1] + 2 * socket_key_clearance,
|
||||
coupon_fit_height + 2 * eps],
|
||||
2 + socket_key_clearance
|
||||
);
|
||||
}
|
||||
|
||||
module coupon_key() {
|
||||
// Separate full-size tongue. A 45-degree stop shoulder remains
|
||||
// support-free, and the vertical rib provides a grip for removal.
|
||||
translate([coupon_key_center[0], coupon_key_center[1], 0]) {
|
||||
rounded_prism(socket_key_size, 2);
|
||||
hull() {
|
||||
translate([0, 0, socket_key_size[2] - eps])
|
||||
rounded_prism(
|
||||
[socket_key_size[0], socket_key_size[1], eps], 2);
|
||||
translate([0, 0,
|
||||
socket_key_size[2] + coupon_key_cap[2] - eps])
|
||||
rounded_prism(
|
||||
[coupon_key_cap[0], coupon_key_cap[1], eps], 3);
|
||||
}
|
||||
translate([0, 0, socket_key_size[2] + coupon_key_cap[2] - eps])
|
||||
rounded_prism(
|
||||
[coupon_key_handle[0], coupon_key_handle[1],
|
||||
coupon_key_handle[2] + eps],
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module coupon_black() {
|
||||
difference() {
|
||||
coupon_positive();
|
||||
coupon_common_cutouts();
|
||||
translate([0, 0, -eps])
|
||||
linear_extrude(height = inlay_depth + 2 * eps)
|
||||
coupon_accent_2d(inlay_clearance);
|
||||
}
|
||||
}
|
||||
|
||||
module coupon_accent() {
|
||||
linear_extrude(height = inlay_depth) coupon_accent_2d();
|
||||
}
|
||||
|
||||
module coupon_complete() {
|
||||
union() {
|
||||
difference() {
|
||||
coupon_positive();
|
||||
coupon_common_cutouts();
|
||||
}
|
||||
coupon_key();
|
||||
}
|
||||
}
|
||||
|
||||
module assembly() {
|
||||
color([0.035, 0.035, 0.04]) base_black_service();
|
||||
color([0.05, 0.25, 0.8]) accent_service();
|
||||
color([0.035, 0.035, 0.04])
|
||||
translate([plate_width / 2, datum_y, plate_height]) socket_service();
|
||||
if (show_references) {
|
||||
reference_rail();
|
||||
reference_feet();
|
||||
}
|
||||
}
|
||||
|
||||
if (part == "assembly") assembly();
|
||||
else if (part == "base") print_orient_base() base_complete_service();
|
||||
else if (part == "base_black") print_orient_base() base_black_service();
|
||||
else if (part == "base_segment")
|
||||
print_orient_base() base_segment_service(segment_index, false);
|
||||
else if (part == "base_black_segment")
|
||||
print_orient_base() base_segment_service(segment_index, true);
|
||||
else if (part == "base_accent") print_orient_base() accent_service();
|
||||
else if (part == "socket") socket_print();
|
||||
else if (part == "coupon") coupon_complete();
|
||||
else if (part == "coupon_black") coupon_black();
|
||||
else if (part == "coupon_key") coupon_key();
|
||||
else if (part == "coupon_accent") coupon_accent();
|
||||
else assert(false, str("Unknown part: ", part));
|
||||
|
After Width: | Height: | Size: 6.3 MiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Combine two aligned OpenSCAD 3MF meshes into a Bambu-friendly multipart 3MF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||
ET.register_namespace("", CORE)
|
||||
|
||||
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||
</Types>
|
||||
'''
|
||||
|
||||
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||
</Relationships>
|
||||
'''
|
||||
|
||||
|
||||
def load_mesh(path: Path) -> ET.Element:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||
if mesh is None:
|
||||
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||
return copy.deepcopy(mesh)
|
||||
|
||||
|
||||
def model_xml(bases: list[Path], accents: list[Path], title: str) -> bytes:
|
||||
root = ET.Element(f"{{{CORE}}}model", {
|
||||
"unit": "millimeter",
|
||||
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||
})
|
||||
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = "copy-stand"
|
||||
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||
ET.SubElement(materials, f"{{{CORE}}}base", {"name": "Black ASA", "displaycolor": "#101014"})
|
||||
ET.SubElement(materials, f"{{{CORE}}}base", {"name": "Blue ASA inlay", "displaycolor": "#0D47CC"})
|
||||
parts: list[tuple[int, str, Path, int]] = []
|
||||
next_id = 2
|
||||
for index, source in enumerate(bases):
|
||||
parts.append((next_id, f"Black ASA segment {index + 1} - Filament 1", source, 0))
|
||||
next_id += 1
|
||||
for index, source in enumerate(accents):
|
||||
parts.append((next_id, f"Blue ASA inlay {index + 1} - Filament 2", source, 1))
|
||||
next_id += 1
|
||||
|
||||
for object_id, name, source, material_index in parts:
|
||||
obj = ET.SubElement(resources, f"{{{CORE}}}object", {
|
||||
"id": str(object_id), "name": name, "type": "model",
|
||||
"pid": "1", "pindex": str(material_index),
|
||||
})
|
||||
obj.append(load_mesh(source))
|
||||
assembly_id = next_id
|
||||
assembly = ET.SubElement(resources, f"{{{CORE}}}object", {
|
||||
"id": str(assembly_id), "name": title, "type": "model",
|
||||
})
|
||||
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||
for object_id, _name, _source, _material_index in parts:
|
||||
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": str(object_id)})
|
||||
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": str(assembly_id)})
|
||||
return ET.tostring(root, encoding="utf-8", xml_declaration=True), assembly_id, parts
|
||||
|
||||
|
||||
def model_settings_xml(title: str, assembly_id: int,
|
||||
parts: list[tuple[int, str, Path, int]]) -> bytes:
|
||||
root = ET.Element("config")
|
||||
obj = ET.SubElement(root, "object", {"id": str(assembly_id)})
|
||||
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||
for part_id, name, _source, material_index in parts:
|
||||
extruder = material_index + 1
|
||||
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||
ET.SubElement(root, "assemble")
|
||||
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||
info = zipfile.ZipInfo(name)
|
||||
info.create_system = 3
|
||||
info.external_attr = 0o100644 << 16
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
archive.writestr(info, data)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--base", action="append", type=Path, required=True)
|
||||
parser.add_argument("--accent", action="append", type=Path, default=[])
|
||||
parser.add_argument("--title", required=True)
|
||||
args = parser.parse_args()
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
xml, assembly_id, parts = model_xml(args.base, args.accent, args.title)
|
||||
with zipfile.ZipFile(args.output, "w") as archive:
|
||||
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||
write_entry(archive, "3D/3dmodel.model", xml)
|
||||
write_entry(archive, "Metadata/model_settings.config",
|
||||
model_settings_xml(args.title, assembly_id, parts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||