mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-08-12 15:44:37 +02:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
21
.github/release.yml
vendored
Normal file
21
.github/release.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# .github/release.yml
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- Ignore-For-Release
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- Semver-Major
|
||||
- Breaking-Change
|
||||
- title: New Features
|
||||
labels:
|
||||
- Semver-Minor
|
||||
- Enhancement
|
||||
- title: Bugfixes
|
||||
labels:
|
||||
- Bug
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
@@ -467,7 +467,6 @@ module generic_bottle_neck(
|
||||
roundover = 0.58 * diamMagMult;
|
||||
lip_roundover_r = (roundover > (neck_d - inner_d) / 2) ? 0 : roundover;
|
||||
h = height + support_width;
|
||||
echo(h=h);
|
||||
threadbase_d = neck_d - 0.8 * diamMagMult;
|
||||
|
||||
$fn = segs(33 / 2);
|
||||
@@ -693,14 +692,12 @@ module bottle_adapter_neck_to_cap(
|
||||
: neck_support_od;
|
||||
cap_neck_id = default(cap_neck_id,neck_id);
|
||||
wall = default(wall, neck_support_od + neck_d + cap_od + neck_id - 2*tolerance);
|
||||
echo(wall=wall);
|
||||
|
||||
$fn = segs(33 / 2);
|
||||
wallt1 = min(wall, (max(neck_support_od, neck_d) - neck_id) / 2);
|
||||
wallt2 = min(wall, (cap_od + 2 * cap_wall - cap_neck_id) / 2);
|
||||
|
||||
top_h = neck_h + max(1,neck_h/17)*sign(neck_support_od);
|
||||
echo(top_h=top_h);
|
||||
bot_h = cap_h + cap_wall;
|
||||
attachable(anchor=anchor,orient=orient,spin=spin, r=max([neck_id/2+wallt1, cap_neck_id/2+wallt2, neck_support_od/2]), h=top_h+bot_h+d) {
|
||||
zmove((bot_h-top_h)/2)
|
||||
@@ -1298,7 +1295,6 @@ module sp_cap(diam,type,wall,style="L",top_adj=0, bot_adj=0, texture="none", anc
|
||||
|
||||
twist = struct_val(_sp_twist, type);
|
||||
|
||||
echo(top_adj=top_adj,bot_adj=bot_adj);
|
||||
dum3=assert(top_adj<S+0.75*a, str("The top_adj value is too large so the thread won't fit. It must be smaller than ",S+0.75*a));
|
||||
oprofile = _sp_thread_profile(tpi,a,S+0.75*a-top_adj,style,flip=true);
|
||||
bounds=pointlist_bounds(oprofile);
|
||||
|
235
gears.scad
235
gears.scad
@@ -103,7 +103,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// back(82)rect([45, 20],anchor=BACK);
|
||||
// }
|
||||
// color("black"){
|
||||
// stroke(arc(r=_root_radius(mod=5,teeth=30),angle=[70,110]),width=.25);
|
||||
// stroke(arc(r=_root_radius_basic(mod=5,teeth=30),angle=[70,110]),width=.25);
|
||||
// stroke(arc(r=pitch_radius(mod=5,teeth=30),angle=[70,110]),width=.25);
|
||||
// stroke(arc(r=outer_radius(mod=5,teeth=30),angle=[70,110]),width=.25);
|
||||
// back(63.5)right(24.2)text("root circle",size=2.5);
|
||||
@@ -134,7 +134,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// are specified basic OpenSCAD units, so if you work in millimeters and want to give circular pitch in inches, be
|
||||
// sure to multiply by `INCH`. The diametral pitch is given based on inches under the assumption that OpenSCAD units are millimeters.
|
||||
// .
|
||||
// Note that there is no direct way to specify the size of a gear. The diameter of a gear depends on its tooth count
|
||||
// You cannot directly specify the size of a gear. The diameter of a gear depends on its tooth count
|
||||
// and tooth size. If you want a gear with a particular diameter you can get close by seeting the module to `d/teeth`,
|
||||
// but that specifies the pitch circle, so the gear teeth will have a somewhat larger radius. You should **not**
|
||||
// apply scale() to gears. Always change their size by adjusting the tooth size parameters.
|
||||
@@ -148,7 +148,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// gears with a small number of teeth, but it also increases gear wear and meshing noise. Higher pressure angles also
|
||||
// increase the force that tries to push the gears apart, and hence the load on the gear axles. The current standard pressure
|
||||
// angle is 20 degrees. It replaces an old 14.5 degree standard.
|
||||
// Figure(2D,Med,NoAxes): Teeth of the same size with different pressure angles. Note that 20 deg is the industry standard.
|
||||
// Figure(2D,Med,NoAxes): Teeth of the same size with different pressure angles. The industry standard is 20°.
|
||||
// pang = [30,20,14.5];
|
||||
// ycopies(n=3, spacing=25){
|
||||
// intersection(){
|
||||
@@ -177,6 +177,15 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// fwd(6.4) right(22) text("clearance", size=2.5);
|
||||
// }
|
||||
// Continues:
|
||||
// If the clearance is too large it can lead to a self-intersecting gear profile. When this occurs, you
|
||||
// will see a message indicating that the profile was clipped, and what the required clearance is to
|
||||
// avoid the clipping. This can be a starting point for adjusting the clipping. Typical gear pressure angles,
|
||||
// as noted above, are 14.5, 20, or sometimes 25 degrees, but in some cases, larger pressure angles
|
||||
// may be useful. These large pressure angles can give rise to self-intersecting gear geometry even
|
||||
// with a zero clearance. To get a valid model, such gears need a **negative** clearance value.
|
||||
// Figure(2D,NoAxes): This gear has a 55 degree pressure angle. If you don't specify clearance, the message tells you it clipped at -2.2. Here we have used -2.3 to avoid a sharp corner in the valleys between teeth.
|
||||
// spur_gear2d(mod=5, teeth=7, profile_shift=0, pressure_angle=55,clearance=-2.3);
|
||||
// Continues:
|
||||
// Another clearance requirement can present a serious problem when the number of teeth is low. As the gear rotates, the
|
||||
// teeth may interfere with each other. This may require undercutting the gear teeth to create space, which weakens the teeth.
|
||||
// Is is best to avoid gears with very small numbers of teeth when possible.
|
||||
@@ -200,7 +209,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// can also be used to fine tune the spacing between gears. When the gear has many teeth a negative profile shift may
|
||||
// be able to bring the gears slightly closer together, while still avoiding undercutting.
|
||||
// Profile shifting also changes the effective pressure angle of the gear engagement.
|
||||
// Figure(2D,Med,NoAxes): The green gear is a 7 tooth gear without profile shifting. In yellow is the same gear, profile shifted. Note that the teeth too longer narrow at their base. Also note that the effective root circle has a larger radius, and the teeth are also longer.
|
||||
// Figure(2D,Med,NoAxes): The green gear is a 7 tooth gear without profile shifting. Its teeth are narrow and weak at their base. In yellow is the same gear, profile shifted. It has much stronger teeth. The profile shifted gear also has a larger root circle radius and longer teeth.
|
||||
// spur_gear2d(mod=5, teeth=7);
|
||||
// color("green")spur_gear2d(mod=5, teeth=7, profile_shift=0);
|
||||
// Continues:
|
||||
@@ -231,7 +240,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// gear teeth need to be shortened. The shortening factor depends on characteristics of both gears, so it cannot
|
||||
// be automatically incorporated. (Consider the situation where one gear mates with multiple other gears.) With modest
|
||||
// profile shifts, you can probably ignore this adjustment, but with more extreme profile shifts, it may be important.
|
||||
// You can compute the shortening parameter using {{gear_shorten()}}. Note that the actual shortening distance is obtained
|
||||
// You can compute the shortening parameter using {{gear_shorten()}}. The actual shortening distance is obtained
|
||||
// by scaling the shortening factor by the gear's module.
|
||||
// Figure(2D,Big,NoAxes,VPT=[55.8861,-4.31463,8.09832],VPR=[0,0,0],VPD=325.228): With large profile shifts the teeth need to be shortened or they don't have clearance in the valleys of the teeth in the meshing gear.
|
||||
// teeth1=25;
|
||||
@@ -465,7 +474,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// It is possible to design the worm to follow the curved shape of its mated gear, resulting
|
||||
// in an enveloping (also called "globoid") worm. This type of worm makes better contact with
|
||||
// the worm gear, but is less often used due to manufacturing complexity and consequent expense.
|
||||
// Figure(3D,Big,NoAxes,VPT=[0,0,0],VPR=[192,0,180],VPD=172.84): A cylindrical worm appears on the left in green. Note its straight sides. The enveloping (globoid) worm gears appears on the right in green. Note that its sides curve so several teeth can mate with the worm gear, and it requires a complex tooth form
|
||||
// Figure(3D,Big,NoAxes,VPT=[0,0,0],VPR=[192,0,180],VPD=172.84): A cylindrical worm appears on the left in green. Note its straight sides. The enveloping (globoid) worm gears appears on the right in green. Its sides curve so several teeth can mate with the worm gear, and it requires a complex tooth form.
|
||||
// tilt=20;
|
||||
// starts=1;
|
||||
// ps=0;
|
||||
@@ -531,7 +540,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// the surface of an imaginary cone, which is the "pitch cone" of the bevel gear. Two bevel gears can mesh when their pitch cone
|
||||
// apexes coincide and the cones touch along their length. The teeth of bevel gears shrink as they get closer to the center of the gear.
|
||||
// Tooth dimensions and pitch diameter (the base of the pitch cone) are referenced to the outer end of the teeth.
|
||||
// Note that the pitch radius, computed the same was as for other gears, gives the radius of the pitch cone's base.
|
||||
// The pitch radius, computed the same was as for other gears, gives the radius of the pitch cone's base.
|
||||
// Bevel gears can be made with straight teeth, analogous to spur gears, and with the
|
||||
// same disadvantage of sudden full contact that is noisy. Spiral teeth are analogous to helical
|
||||
// teeth on cylindrical gears: the teeth engage gradually and smoothly, transmitting motion more smoothly
|
||||
@@ -608,7 +617,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// crown gear is the pitch radius determined by the gear's tooth size and number of teeth. The face width
|
||||
// of a crown gear is limited by geometry, so if you make it too large you will get an error.
|
||||
// .
|
||||
// Note that the geometry of these crown gears is tricky and not well documented by sources we have found.
|
||||
// The geometry of these crown gears is tricky and not well documented by sources we have found.
|
||||
// If you know something about crown gears that could improve the implementation, please open an issue
|
||||
// on github.
|
||||
// Section: Backlash (Fitting Real Gears Together)
|
||||
@@ -638,7 +647,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// color("black")stroke(arc(n=32,r=r1,angle=[90+bang/2,90]),width=.1,endcaps="arrow2");
|
||||
// }
|
||||
// color("black")back(r1+.25)right(5.5)text("backlash/2",size=1);
|
||||
// Figure(2D,Med,VPT=[0.532987,50.0891,0.0383045],VPR=[0,0,0],VPD=53.9078): Here two gears appear together with a more reasonable backlash applied to both gears. Again the lighter color shows the ideal gears and the darker shade shows the gear with backlash. Note that in this example, backlash is present on both of the meshing gears, so the total backlash of the system is the combined backlash from both gears.
|
||||
// Figure(2D,Med,VPT=[0.532987,50.0891,0.0383045],VPR=[0,0,0],VPD=53.9078): Here two gears appear together with a more reasonable backlash applied to both gears. Again the lighter color shows the ideal gears and the darker shade shows the gear with backlash. In this example, backlash is present on both of the meshing gears, so the total backlash of the system is the combined backlash from both gears.
|
||||
// teeth1=20;teeth2=33;
|
||||
// mod=5;
|
||||
// ha=0;
|
||||
@@ -738,7 +747,7 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// thickness = Thickness of gear. Default: 10
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// helical = Teeth spiral around the gear at this angle, positive for left handed, negative for right handed. Default: 0
|
||||
// herringbone = If true, and helical is set, creates a herringbone gear. Default: False
|
||||
// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 20
|
||||
@@ -751,10 +760,11 @@ function _inherit_gear_thickness(thickness,dflt=10) =
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: mod/4
|
||||
// slices = Number of vertical layers to divide gear into. Useful for refining gears with `helical`.
|
||||
// internal = If true, create a mask for difference()ing from something else.
|
||||
// $gear_steps = Number of points to sample gear profile. Default: 16
|
||||
// atype = Set to "root", "tip" or "pitch" to determine anchoring circle. Default: "pitch"
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Side Effects:
|
||||
// If internal is true then the default tag is "remove"
|
||||
// Anchor Types:
|
||||
@@ -925,7 +935,7 @@ function spur_gear(
|
||||
profile_shift = auto_profile_shift(teeth,PA,helical,profile_shift=profile_shift),
|
||||
pr = pitch_radius(circ_pitch, teeth, helical),
|
||||
or = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=internal,shorten=shorten),
|
||||
rr = _root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal),
|
||||
rr = _root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal),
|
||||
anchor_rad = atype=="pitch" ? pr
|
||||
: atype=="tip" ? or
|
||||
: atype=="root" ? rr
|
||||
@@ -1006,7 +1016,7 @@ module spur_gear(
|
||||
profile_shift = auto_profile_shift(teeth,PA,helical,profile_shift=profile_shift);
|
||||
pr = pitch_radius(circ_pitch, teeth, helical);
|
||||
or = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=internal,shorten=shorten);
|
||||
rr = _root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal);
|
||||
rr = _root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal);
|
||||
anchor_rad = atype=="pitch" ? pr
|
||||
: atype=="tip" ? or
|
||||
: atype=="root" ? rr
|
||||
@@ -1116,7 +1126,7 @@ module spur_gear(
|
||||
// teeth = Total number of teeth around the spur gear.
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees.
|
||||
// profile_shift = Profile shift factor x. Default: "auto"
|
||||
// shorten = Shorten gear tips by the module times this value. Needed for large profile shifted gears. Default: 0
|
||||
@@ -1126,6 +1136,7 @@ module spur_gear(
|
||||
// gear_spin = Rotate gear and children around the gear center, regardless of how gear is anchored. Default: 0
|
||||
// clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear. Default: mod/4
|
||||
// internal = If true, create a mask for difference()ing from something else.
|
||||
// $gear_steps = Number of points to sample gear profile. Default: 16
|
||||
// shaft_diam = If given, the diameter of the central shaft hole.
|
||||
// atype = Set to "root", "tip" or "pitch" to determine anchoring circle. Default: "pitch"
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
@@ -1224,7 +1235,7 @@ function spur_gear2d(
|
||||
assert(is_finite(shaft_diam) && shaft_diam>=0)
|
||||
assert(is_integer(hide) && hide>=0 && hide<teeth)
|
||||
assert(is_finite(PA) && PA>=0 && PA<90, "Bad pressure_angle value.")
|
||||
assert(clearance==undef || (is_finite(clearance) && clearance>=0))
|
||||
assert(clearance==undef || is_finite(clearance))
|
||||
assert(is_finite(backlash) && backlash>=0)
|
||||
assert(is_finite(helical) && abs(helical)<90)
|
||||
assert(is_finite(gear_spin))
|
||||
@@ -1232,7 +1243,7 @@ function spur_gear2d(
|
||||
profile_shift = auto_profile_shift(teeth,PA,helical,profile_shift=profile_shift),
|
||||
pr = pitch_radius(circ_pitch, teeth, helical=helical),
|
||||
or = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=internal,shorten=shorten),
|
||||
rr = _root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal),
|
||||
rr = _root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal),
|
||||
anchor_rad = atype=="pitch" ? pr
|
||||
: atype=="tip" ? or
|
||||
: atype=="root" ? rr
|
||||
@@ -1292,7 +1303,7 @@ module spur_gear2d(
|
||||
assert(is_finite(shaft_diam) && shaft_diam>=0)
|
||||
assert(is_integer(hide) && hide>=0 && hide<teeth)
|
||||
assert(is_finite(PA) && PA>=0 && PA<90, "Bad pressure_angle value.")
|
||||
assert(clearance==undef || (is_finite(clearance) && clearance>=0))
|
||||
assert(clearance==undef || is_finite(clearance))
|
||||
assert(is_finite(backlash) && backlash>=0)
|
||||
assert(is_finite(helical) && abs(helical)<90)
|
||||
assert(is_finite(gear_spin));
|
||||
@@ -1312,7 +1323,7 @@ module spur_gear2d(
|
||||
);
|
||||
pr = pitch_radius(circ_pitch, teeth, helical=helical);
|
||||
or = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=internal,shorten=shorten);
|
||||
rr = _root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal);
|
||||
rr = _root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=internal);
|
||||
anchor_rad = atype=="pitch" ? pr
|
||||
: atype=="tip" ? or
|
||||
: atype=="root" ? rr
|
||||
@@ -1363,11 +1374,12 @@ module spur_gear2d(
|
||||
// profile_shift = Profile shift factor x for tooth profile. Default: 0
|
||||
// clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear (in millimeters)
|
||||
// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// $gear_steps = Number of points to sample gear profile. Default: 16
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example:
|
||||
// ring_gear(circ_pitch=5, teeth=48, thickness=10);
|
||||
// Example: Adjusting Backing
|
||||
@@ -1422,7 +1434,7 @@ module ring_gear(
|
||||
assert(is_finite(gear_spin));
|
||||
pr = pitch_radius(circ_pitch, teeth, helical=helical);
|
||||
ar = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=true);
|
||||
rr=_root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=true);
|
||||
rr=_root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=true);
|
||||
or = is_def(or) ?
|
||||
assert(is_finite(or) && or>ar, "or is invalid or too small for teeth")
|
||||
or
|
||||
@@ -1510,8 +1522,9 @@ module ring_gear(
|
||||
// profile_shift = Profile shift factor x for tooth profile. Default: 0
|
||||
// clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear (in millimeters)
|
||||
// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// $gear_steps = Number of points to sample gear profile. Default: 16
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// Anchor Types:
|
||||
@@ -1567,7 +1580,7 @@ module ring_gear2d(
|
||||
assert(is_finite(gear_spin));
|
||||
pr = pitch_radius(circ_pitch, teeth, helical=helical);
|
||||
ar = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=true);
|
||||
rr=_root_radius(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=true);
|
||||
rr=_root_radius_basic(circ_pitch, teeth, clearance, profile_shift=profile_shift, internal=true);
|
||||
or = is_def(or) ?
|
||||
assert(is_finite(or) && or>ar, "or is invalid or too small for teeth")
|
||||
or
|
||||
@@ -1628,7 +1641,7 @@ module ring_gear2d(
|
||||
// The rack appears oriented with
|
||||
// its teeth pointed UP, so to mesh with gears in the XY plane, use `orient=BACK` or `orient=FWD` and apply any desired rotation.
|
||||
// The pitch line of the rack is aligned with the x axis, the TOP anchors are at the tips of the teeth and the BOTTOM anchors at
|
||||
// the bottom of the backing. Note that for helical racks the corner anchors still point at 45 degr angles.
|
||||
// the bottom of the backing. Note that for helical racks the corner anchors still point at 45° angles.
|
||||
// Arguments:
|
||||
// pitch = The pitch, or distance between teeth centers along the rack. Matches up with circular pitch on a spur gear. Default: 5
|
||||
// teeth = Total number of teeth along the rack. Default: 20
|
||||
@@ -1638,7 +1651,7 @@ module ring_gear2d(
|
||||
// bottom = Distance from rack's pitch line (the x-axis) to the bottom of the rack. (Alternative to backing or width)
|
||||
// width = Distance from base of rack to tips of teeth (alternative to bottom and backing).
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// helical = The angle of the rack teeth away from perpendicular to the rack length. Used to match helical spur gear pinions. Default: 0
|
||||
// herringbone = If true, and helical is set, creates a herringbone rack.
|
||||
// profile_shift = Profile shift factor x. Default: 0
|
||||
@@ -1647,7 +1660,7 @@ module ring_gear2d(
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: module/4
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Named Anchors:
|
||||
// "root" = At the base of the teeth, at the center of rack.
|
||||
// "root-left" = At the base of the teeth, at the left end of the rack.
|
||||
@@ -1907,7 +1920,7 @@ function rack(
|
||||
// bottom = Distance from rack's pitch line (the x-axis) to the bottom of the rack. (Alternative to backing or width)
|
||||
// width = Distance from base of rack to tips of teeth (alternative to bottom and backing).
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// helical = The angle of the rack teeth away from perpendicular to the rack length. Stretches out the tooth shapes. Used to match helical spur gear pinions. Default: 0
|
||||
// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees.
|
||||
// profile_shift = Profile shift factor x for tooth shape. Default: 0
|
||||
@@ -2136,11 +2149,11 @@ module rack2d(
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: module/4
|
||||
// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0
|
||||
// slices = Number of vertical layers to divide gear into. Useful for refining gears with `spiral`. Default: 1
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example:
|
||||
// crown_gear(mod=1, teeth=40, backing=3, face_width=5, pressure_angle=20);
|
||||
// Example:
|
||||
@@ -2348,7 +2361,7 @@ module crown_gear(
|
||||
// shaft_angle = Angle between the shafts of the two gears. Default: 90
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// backing = Distance from bottom of bevel gear to bottom corner of teeth (Alternative to bottom or thickness). Default: 0 if the gear is thick enough (see above)
|
||||
// bottom = Distance from bevel gear's pitch base to the bottom of the bevel gear. (Alternative to backing or thickness)
|
||||
@@ -2367,7 +2380,7 @@ module crown_gear(
|
||||
// gear_spin = Rotate gear and children around the gear center, regardless of how gear is anchored. Default: 0
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: "pitchbase"
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Named Anchors:
|
||||
// "pitchbase" = With the base of the pitch cone in the XY plane, centered at the origin. This is the natural height for the gear, and the default anchor.
|
||||
// "apex" = At the pitch cone apex for the bevel gear.
|
||||
@@ -2377,7 +2390,7 @@ module crown_gear(
|
||||
// circ_pitch=5, teeth=36, mate_teeth=36,
|
||||
// shaft_diam=5, spiral=0
|
||||
// );
|
||||
// Example(NoAxes): Spiral Beveled Gear and Pinion. Note conical backing added to the yellow gear to prevent it from being thin.
|
||||
// Example(NoAxes): Spiral Beveled Gear and Pinion. Conical backing added to the yellow gear to prevents it from being thin.
|
||||
// t1 = 16; t2 = 28;
|
||||
// color("lightblue")bevel_gear(
|
||||
// circ_pitch=5, teeth=t1, mate_teeth=t2,
|
||||
@@ -2427,7 +2440,7 @@ module crown_gear(
|
||||
// xrot(ang)
|
||||
// bevel_gear(mod=3,15,35,ang,spiral=0,right_handed=true,anchor="apex")
|
||||
// cyl(h=65,d=3,$fn=16,anchor=BOT);
|
||||
// Example(NoAxes,VPT=[-6.28233,3.60349,15.6594],VPR=[71.1,0,52.1],VPD=213.382): Non-right angled bevel gear pair positioned in a frame, with holes cut in the frame for the shafts. Note that when rotating a gear to its appropriate angle, you must rotate around an axis tangent to the gear's pitch base, **not** the gear center. This is accomplished by shifting the gear by its pitch radius before applying the rotation.
|
||||
// Example(NoAxes,VPT=[-6.28233,3.60349,15.6594],VPR=[71.1,0,52.1],VPD=213.382): Non-right angled bevel gear pair positioned in a frame, with holes cut in the frame for the shafts. When rotating a gear to its appropriate angle, you must rotate around an axis tangent to the gear's pitch base, **not** the gear center. This is accomplished by shifting the gear by its pitch radius before applying the rotation.
|
||||
// include <BOSL2/rounding.scad>
|
||||
// angle = 60;
|
||||
// t1=17; t2=29; mod=2; bot=4; wall=2; shaft=5;
|
||||
@@ -2455,7 +2468,6 @@ module crown_gear(
|
||||
// xrot(ang,cp=[0,-pitch_radius(mod=3,teeth=15),0])
|
||||
// bevel_gear(mod=3,15,35,ang,right_handed=true);
|
||||
|
||||
echo(VPT=$vpt,VPR=$vpr,VPD=$vpd);
|
||||
|
||||
|
||||
|
||||
@@ -2493,7 +2505,7 @@ function bevel_gear(
|
||||
slices = cutter_radius==0? 1 : slices,
|
||||
pitch_angle = posmod(atan(sin(shaft_angle)/((mate_teeth/teeth)+cos(shaft_angle))),180),
|
||||
pr = pitch_radius(circ_pitch, teeth),
|
||||
rr = _root_radius(circ_pitch, teeth, clearance),
|
||||
rr = _root_radius_basic(circ_pitch, teeth, clearance),
|
||||
pitchoff = (pr-rr) * sin(pitch_angle),
|
||||
ocone_rad = pitch_angle<90 ? opp_ang_to_hyp(pr, pitch_angle)
|
||||
: opp_ang_to_hyp(pitch_radius(circ_pitch,mate_teeth), shaft_angle-pitch_angle),
|
||||
@@ -2703,11 +2715,11 @@ module bevel_gear(
|
||||
// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 20
|
||||
// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: module/4
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example:
|
||||
// worm(circ_pitch=8, d=30, l=50, $fn=72);
|
||||
// Example: Multiple Starts.
|
||||
@@ -2859,11 +2871,11 @@ module worm(
|
||||
// starts = The number of lead starts. Default: 1
|
||||
// arc = Arc angle of the mated worm gear to envelop. Default: `2 * pressure_angle`
|
||||
// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 20
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example:
|
||||
// enveloping_worm(circ_pitch=8, mate_teeth=45, d=30, $fn=72);
|
||||
// Example: Multiple Starts.
|
||||
@@ -3042,11 +3054,11 @@ module enveloping_worm(
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: module/4
|
||||
// profile_shift = Profile shift factor x. Default: "auto"
|
||||
// slices = The number of vertical slices to refine the curve of the worm throat. Default: 10
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example: Right-Handed
|
||||
// worm_gear(circ_pitch=5, teeth=36, worm_diam=30, worm_starts=1);
|
||||
// Example: Left-Handed
|
||||
@@ -3298,7 +3310,7 @@ module worm_gear(
|
||||
/// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle
|
||||
/// internal = If true, create a mask for difference()ing from something else.
|
||||
/// center = If true, centers the pitch circle of the tooth profile at the origin. Default: false.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// mod = The module of the gear (pitch diameter / teeth)
|
||||
/// Example(2D):
|
||||
/// _gear_tooth_profile(circ_pitch=5, teeth=20, pressure_angle=20);
|
||||
@@ -3333,7 +3345,7 @@ function _gear_tooth_profile(
|
||||
_involute = function(base_r,a)
|
||||
let(b=a*PI/180) base_r * [cos(a)+b*sin(a), sin(a)-b*cos(a)],
|
||||
|
||||
steps = 16,
|
||||
steps = !is_undef($gear_steps) ? $gear_steps : 16,
|
||||
circ_pitch = circular_pitch(pitch=pitch, circ_pitch=circ_pitch, diam_pitch=diam_pitch, mod=mod),
|
||||
mod = module_value(circ_pitch=circ_pitch),
|
||||
clear = default(clearance, 0.25 * mod),
|
||||
@@ -3342,7 +3354,7 @@ function _gear_tooth_profile(
|
||||
arad = outer_radius(circ_pitch, teeth, helical=helical, profile_shift=profile_shift, internal=internal, shorten=shorten),
|
||||
prad = pitch_radius(circ_pitch, teeth, helical=helical),
|
||||
brad = _base_radius(circ_pitch, teeth, pressure_angle, helical=helical),
|
||||
rrad = _root_radius(circ_pitch, teeth, clearance, helical=helical, profile_shift=profile_shift, internal=internal),
|
||||
rrad = _root_radius_basic(circ_pitch, teeth, clear, helical=helical, profile_shift=profile_shift, internal=internal),
|
||||
srad = max(rrad,brad),
|
||||
tthick = circ_pitch/PI / cos(helical) * (PI/2 + 2*profile_shift * tan(pressure_angle)) + (internal?backlash:-backlash),
|
||||
tang = tthick / prad / 2 * 180 / PI,
|
||||
@@ -3426,7 +3438,6 @@ function _gear_tooth_profile(
|
||||
// Round out the clearance valley
|
||||
rcircum = 2 * PI * (internal? ma_rad : rrad),
|
||||
rpart = (180/teeth-tang)/360,
|
||||
round_r = min(clear, rcircum*rpart),
|
||||
line1 = internal
|
||||
? select(tooth_half_raw,-2,-1)
|
||||
: select(tooth_half_raw,0,1),
|
||||
@@ -3437,6 +3448,8 @@ function _gear_tooth_profile(
|
||||
rcorner = internal
|
||||
? [last(line1), isect_pt, line2[0]]
|
||||
: [line2[0], isect_pt, line1[0]],
|
||||
maxr = norm(rcorner[0]-rcorner[1])*tan(vector_angle(rcorner)/2), // Max radius that will actually fit on the corner
|
||||
round_r = min(maxr, clear, rcircum*rpart),
|
||||
rounded_tooth_half = deduplicate([
|
||||
if (!internal && round_r>0) each arc(n=8, r=round_r, corner=rcorner),
|
||||
if (!internal && round_r<=0) isect_pt,
|
||||
@@ -3464,17 +3477,29 @@ function _gear_tooth_profile(
|
||||
tooth_half = !undercut_max? rounded_tooth_half :
|
||||
strip_left(rounded_tooth_half, 0),
|
||||
|
||||
// look for self-intersections in the gear profile. If found, clip them off
|
||||
invalid = [for(i=idx(tooth_half)) if (atan2(tooth_half[i].y,tooth_half[i].x)>90+180/teeth) i],
|
||||
clipped = invalid==[] ? tooth_half
|
||||
: let(
|
||||
ind = last(invalid),
|
||||
ipt = line_intersection([[0,0],polar_to_xy(1,90+180/teeth)], select(tooth_half,ind,ind+1)),
|
||||
c = prad - mod*(1-profile_shift) - norm(ipt)
|
||||
)
|
||||
echo(str(teeth, " tooth gear profile clipped at clearance = ",c))
|
||||
[
|
||||
ipt,
|
||||
each slice(tooth_half, ind+1,-1)
|
||||
],
|
||||
|
||||
// Mirror the tooth to complete it.
|
||||
full_tooth = deduplicate([
|
||||
each tooth_half,
|
||||
each reverse(xflip(tooth_half)),
|
||||
each clipped,
|
||||
each reverse(xflip(clipped)),
|
||||
]),
|
||||
|
||||
// Reduce number of vertices.
|
||||
tooth = path_merge_collinear(
|
||||
resample_path(full_tooth, n=ceil(2*steps), keep_corners=30, closed=false)
|
||||
),
|
||||
|
||||
out = center? fwd(prad, p=tooth) : tooth
|
||||
) out;
|
||||
|
||||
@@ -3491,7 +3516,7 @@ function _gear_tooth_profile(
|
||||
// The transmission ratio of a planetary gear assembly depends on which element is fixed and which ones are considered the input and output shafts.
|
||||
// The fixed element can be the ring gear, the sun gear, or the carrier, and then you specify the desired ratio between the other two.
|
||||
// You must also specify a maximum number of teeth on the ring gear. The function calculates the best approximation to your desired
|
||||
// transmission ratio under that constraint: a large enough increase in the allowed number of teeth will yield a more accurate approximation. Note that the planet gears
|
||||
// transmission ratio under that constraint: a large enough increase in the allowed number of teeth will yield a more accurate approximation. The planet gears
|
||||
// appear uniformly spaced around the sun gear, but this uniformity is often only approximate. Exact uniformity occurs when teeth_sun+teeth_ring
|
||||
// is a multiple of the number of planet gears.
|
||||
// .
|
||||
@@ -3525,7 +3550,7 @@ function _gear_tooth_profile(
|
||||
// max_teeth = maximum number of teeth allowed on the ring gear
|
||||
// ---
|
||||
// mod = The module of the gear, pitch diameter divided by tooth count.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = distance between teeth centers around the pitch circle.
|
||||
// ring_carrier = set ring/carrier transmission ratio to this value in a ring driven system, must be between 1 and 2
|
||||
// carrier_ring = set carrier/ring transmission ratio to this value in a carrier driven system, must be between 1/2 and 1
|
||||
@@ -3658,7 +3683,7 @@ function planetary_gears(n, max_teeth, helical=0, circ_pitch, mod, diam_pitch,
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example(2D,Med,VPT=[0,31,0],VPR=[0,0,0],VPD=40):
|
||||
// $fn=144;
|
||||
// teeth=20;
|
||||
@@ -3703,7 +3728,7 @@ function circular_pitch(circ_pitch, mod, pitch, diam_pitch) =
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example:
|
||||
// diam_pitch1 = diametral_pitch(mod=2);
|
||||
// diam_pitch2 = diametral_pitch(circ_pitch=8);
|
||||
@@ -3732,7 +3757,7 @@ function diametral_pitch(circ_pitch, mod, pitch, diam_pitch) =
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example:
|
||||
// mod1 = module_value(circ_pitch=8);
|
||||
// mod2 = module_value(mod=2);
|
||||
@@ -3755,7 +3780,7 @@ function module_value(circ_pitch, mod, pitch, diam_pitch) =
|
||||
/// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
/// profile_shift = Profile shift factor x. Default: 0
|
||||
/// ---
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// mod = The module of the gear (pitch diameter / teeth)
|
||||
/// Example:
|
||||
/// ad = _adendum(circ_pitch=5);
|
||||
@@ -3796,7 +3821,7 @@ function _adendum(
|
||||
/// clearance = If given, sets the clearance between meshing teeth. Default: module/4
|
||||
/// profile_shift = Profile shift factor x. Default: 0
|
||||
/// ---
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// mod = The module of the gear (pitch diameter / teeth)
|
||||
/// shorten = amount to shorten tip
|
||||
/// Example:
|
||||
@@ -3842,7 +3867,7 @@ function _dedendum(
|
||||
// helical = The helical angle (from vertical) of the teeth on the gear. Default: 0
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example:
|
||||
// pr = pitch_radius(circ_pitch=5, teeth=11);
|
||||
// pr = pitch_radius(circ_pitch=5, teeth=11, helical=30);
|
||||
@@ -3885,8 +3910,10 @@ function pitch_radius(
|
||||
// or = outer_radius(mod=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=], [shorten=]);
|
||||
// or = outer_radius(diam_pitch=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=], [shorten=]);
|
||||
// Description:
|
||||
// Calculates the outer radius for the gear. The gear fits entirely within a cylinder of this radius, unless
|
||||
// it has been strongly profile shifted, in which case it will be undersized due to tip clipping.
|
||||
// Calculates the standard outer radius for the gear. The gear fits entirely within a cylinder of this radius. The gear
|
||||
// will fit exactly in the cylinder except in two cases:
|
||||
// 1. It has been strongly profile shifted, in which case it will be undersized due to tip clipping.
|
||||
// 2. The pressure angle is very high, in which case the tips meet in points before the standard radius, also resulting in undersized teeth
|
||||
// Arguments:
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// teeth = The number of teeth on the gear.
|
||||
@@ -3898,7 +3925,7 @@ function pitch_radius(
|
||||
// shorten = Shortening factor, needed to maintain clearance with profile shifting. Default: 0
|
||||
// internal = If true, calculate for an internal gear.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example:
|
||||
// or = outer_radius(circ_pitch=5, teeth=20);
|
||||
// or = outer_radius(circ_pitch=5, teeth=20, helical=30);
|
||||
@@ -3930,11 +3957,55 @@ function outer_radius(circ_pitch, teeth, clearance, internal=false, helical=0, p
|
||||
);
|
||||
|
||||
|
||||
/// Function: _root_radius()
|
||||
|
||||
// Function: root_radius()
|
||||
// Synopsis: Returns the radius of the roots of the teeth
|
||||
// Topics: Gears, Parts
|
||||
// See Also: spur_gear(), diametral_pitch(), circular_pitch(), module_value(), pitch_radius(), outer_radius()
|
||||
// Usage:
|
||||
// rr = outer_radius(mod=|circ_pitch=|diam_pitch=, teeth, [helical], [pressure_angle=], [clearance=], [internal=], [profile_shift=], [backlash=]);
|
||||
// Description:
|
||||
// Calculates the actual radius of the roots of the teeth. The root radius is usually given as a straight forward calcluation, but
|
||||
// when large pressure-angle teeth are clipped, it is more difficult to determine this radius. This function calculates the actual
|
||||
// root radius so that you can, for example, place a partial tooth gear onto a matching circle. The `backlash` parameter may seem
|
||||
// unnecessary, but when large pressure angle teeth are clipped, the value of backlash changes the clipping radius. For regular
|
||||
// gear teeth, `backlash` has no effect on the radius.
|
||||
|
||||
// Arguments:
|
||||
// teeth = The number of teeth on the gear.
|
||||
// helical = The helical angle (from vertical) of the teeth on the gear. Default: 0
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// profile_shift = Profile shift factor x. Default: "auto"
|
||||
// pressure_angle = Pressure angle. Default: 20
|
||||
// clearance = If given, sets the clearance between meshing teeth. Default: module/4
|
||||
// backlash = Add extra space to produce a total of 2*backlash between the two gears.
|
||||
// internal = If true, calculate for an internal gear.
|
||||
// Example(2D,NoAxes): A partial gear with its circle added to complete it.
|
||||
// teeth=5;
|
||||
// mod=5;
|
||||
// rr = root_radius(mod=mod, teeth);
|
||||
// spur_gear2d(mod=mod, teeth=teeth, hide=floor(teeth/2));
|
||||
// circle(r=rr, $fn=64);
|
||||
|
||||
function root_radius(teeth, helical=0, clearance, internal=false, profile_shift="auto", pressure_angle=20, mod, pitch, diam_pitch, backlash=0) =
|
||||
let(
|
||||
profile_shift = auto_profile_shift(teeth, pressure_angle, helical, profile_shift=profile_shift),
|
||||
tooth = _gear_tooth_profile(teeth=teeth, pressure_angle=pressure_angle, clearance=clearance, backlash=backlash, helical=helical,
|
||||
internal=internal, profile_shift=profile_shift, mod=mod, diam_pitch=diam_pitch, pitch=pitch),
|
||||
miny = norm(tooth[0])
|
||||
)
|
||||
miny;
|
||||
|
||||
|
||||
|
||||
/// Function: _root_radius_basic()
|
||||
/// Usage:
|
||||
/// rr = _root_radius(circ_pitch, teeth, [helical], [clearance=], [internal=], [profile_shift=]);
|
||||
/// rr = _root_radius(diam_pitch=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=]);
|
||||
/// rr = _root_radius(mod=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=]);
|
||||
/// rr = _root_radius_basic(circ_pitch, teeth, [helical], [clearance=], [internal=], [profile_shift=]);
|
||||
/// rr = _root_radius_basic(diam_pitch=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=]);
|
||||
/// rr = _root_radius_basic(mod=, teeth=, [helical=], [clearance=], [internal=], [profile_shift=]);
|
||||
/// Topics: Gears
|
||||
/// Description:
|
||||
/// Calculates the root radius for the gear, at the base of the dedendum. Does not apply auto profile shifting.
|
||||
@@ -3947,19 +4018,19 @@ function outer_radius(circ_pitch, teeth, clearance, internal=false, helical=0, p
|
||||
/// helical = The helical angle (from vertical) of the teeth on the gear. Default: 0
|
||||
/// profile_shift = Profile shift factor x. Default:0
|
||||
/// mod = The module of the gear (pitch diameter / teeth)
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// Example:
|
||||
/// rr = _root_radius(circ_pitch=5, teeth=11);
|
||||
/// rr = _root_radius(circ_pitch=5, teeth=16, helical=30);
|
||||
/// rr = _root_radius(diam_pitch=10, teeth=11);
|
||||
/// rr = _root_radius(mod=2, teeth=16);
|
||||
/// rr = _root_radius_basic(circ_pitch=5, teeth=11);
|
||||
/// rr = _root_radius_basic(circ_pitch=5, teeth=16, helical=30);
|
||||
/// rr = _root_radius_basic(diam_pitch=10, teeth=11);
|
||||
/// rr = _root_radius_basic(mod=2, teeth=16);
|
||||
/// Example(2D):
|
||||
/// pr = _root_radius(circ_pitch=5, teeth=11);
|
||||
/// pr = _root_radius_basic(circ_pitch=5, teeth=11);
|
||||
/// #spur_gear2d(pitch=5, teeth=11);
|
||||
/// color("black")
|
||||
/// stroke(circle(r=pr),width=0.1,closed=true);
|
||||
|
||||
function _root_radius(circ_pitch, teeth, clearance, internal=false, helical=0, profile_shift=0, diam_pitch, mod, pitch) =
|
||||
function _root_radius_basic(circ_pitch, teeth, clearance, internal=false, helical=0, profile_shift=0, diam_pitch, mod, pitch) =
|
||||
let( circ_pitch = circular_pitch(pitch, mod, circ_pitch, diam_pitch) )
|
||||
pitch_radius(circ_pitch, teeth, helical) - (
|
||||
internal
|
||||
@@ -3983,7 +4054,7 @@ function _root_radius(circ_pitch, teeth, clearance, internal=false, helical=0, p
|
||||
/// helical = The helical angle (from vertical) of the teeth on the gear. Default: 0
|
||||
/// ---
|
||||
/// mod = The module of the gear (pitch diameter / teeth)
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
/// Example:
|
||||
/// br = _base_radius(circ_pitch=5, teeth=20, pressure_angle=20);
|
||||
/// br = _base_radius(circ_pitch=5, teeth=20, pressure_angle=20, helical=30);
|
||||
@@ -4055,7 +4126,7 @@ function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) =
|
||||
// crowning = The amount to oversize the virtual hobbing cutter used to make the teeth, to add a slight crowning to the teeth to make them fit the work easier. Default: 1
|
||||
// clearance = Clearance gap at the bottom of the inter-tooth valleys. Default: module/4
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// Example:
|
||||
// thick = worm_gear_thickness(circ_pitch=5, teeth=36, worm_diam=30);
|
||||
// thick = worm_gear_thickness(mod=2, teeth=28, worm_diam=25);
|
||||
@@ -4126,7 +4197,7 @@ function worm_gear_thickness(
|
||||
// profile_shift = profile shift of worm gear
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// pressure_angle = The pressure angle of the gear.
|
||||
// backlash = Add extra space to produce a total of 2*backlash between the two gears.
|
||||
@@ -4155,8 +4226,8 @@ function worm_dist(d,starts,teeth,mod,profile_shift=0,diam_pitch,circ_pitch,pres
|
||||
// taking into account profile shifting and helical angle. You can give the helical angle as either positive or negative.
|
||||
// If you set one of the tooth counts to zero than that gear will be treated as a rack and the distance returned is the
|
||||
// distance between the rack's pitch line and the gear's center. If you set internal1 or internal2 to true then the
|
||||
// specified gear is a ring gear; the returned distance is still the distance between the centers of the gears. Note that
|
||||
// for a regular gear and ring gear to be compatible the ring gear must have more teeth and at least as much profile shift
|
||||
// specified gear is a ring gear; the returned distance is still the distance between the centers of the gears.
|
||||
// For a regular gear and ring gear to be compatible the ring gear must have more teeth and at least as much profile shift
|
||||
// as the regular gear.
|
||||
// .
|
||||
// The backlash parameter computes the distance offset that produces a total backlash of `2*backlash` in the
|
||||
@@ -4169,7 +4240,7 @@ function worm_dist(d,starts,teeth,mod,profile_shift=0,diam_pitch,circ_pitch,pres
|
||||
// profile_shift2 = Profile shift factor x for the second gear. Default: 0
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// internal1 = first gear is an internal (ring) gear. Default: false
|
||||
// internal2 = second gear is an internal (ring) gear. Default: false
|
||||
@@ -4286,7 +4357,7 @@ function _working_pressure_angle(teeth1,profile_shift1, teeth2, profile_shift2,
|
||||
// profile_shift1 = Profile shift factor x for the first gear. Default: "auto"
|
||||
// profile_shift2 = Profile shift factor x for the second gear. Default: "auto"
|
||||
// ---
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// pressure_angle = The pressure angle of the gear.
|
||||
@@ -4381,7 +4452,7 @@ function gear_skew_angle(teeth1,teeth2,helical1,helical2,profile_shift1,profile_
|
||||
// Synopsis: Returns total profile shift needed to achieve a desired spacing between two gears
|
||||
// Description:
|
||||
// Compute the total profile shift, split between two gears, needed to place those gears with a specified separation.
|
||||
// If the requested separation is too small, returns NaN. Note that the profile shift returned may also be impractically
|
||||
// If the requested separation is too small, returns NaN. The profile shift returned may also be impractically
|
||||
// large or small and does not necessarily lead to a valid gear configuration. You will need to split the profile shift
|
||||
// between the two gears. Note that for helical gears, much more adjustment is available by modifying the helical angle.
|
||||
// Arguments:
|
||||
@@ -4391,7 +4462,7 @@ function gear_skew_angle(teeth1,teeth2,helical1,helical2,profile_shift1,profile_
|
||||
// helical = The helical angle (from vertical) of the teeth on the gear. Default: 0
|
||||
// ---
|
||||
// mod = The module of the gear (pitch diameter / teeth)
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. Note that the diametral pitch is a completely different thing than the pitch diameter.
|
||||
// diam_pitch = The diametral pitch, or number of teeth per inch of pitch diameter. The diametral pitch is a completely different thing than the pitch diameter.
|
||||
// circ_pitch = The circular pitch, the distance between teeth centers around the pitch circle.
|
||||
// pressure_angle = normal pressure angle of gear teeth. Default: 20
|
||||
// Example(2D,Med,NoAxes,VPT=[37.0558,0.626722,9.78411],VPR=[0,0,0],VPD=496): For a pair of module 4 gears with 19, and 37 teeth, the separation without profile shifting is 112. Suppose we want it instead to be 115. A positive profile shift, split evenly between the gears, achieves the goal, as shown by the red rectangle, with width 115.
|
||||
@@ -4408,7 +4479,7 @@ function gear_skew_angle(teeth1,teeth2,helical1,helical2,profile_shift1,profile_
|
||||
// right(d)
|
||||
// spur_gear2d(mod=mod,teeth=teeth2,profile_shift=ps2,shorten=shorten,gear_spin=-90,shaft_diam=5);
|
||||
// stroke([rect([desired,40], anchor=LEFT)],color="red");
|
||||
// Example(2D,Med,NoAxes,VPT=[37.0558,0.626722,9.78411],VPR=[0,0,0],VPD=496): For the same pair of module 4 gears with 19, and 37 teeth, suppose we want a closer spacing of 110 instead of 112. A positive profile shift does the job, as shown by the red rectangle with width 110. More of the negative shift is assigned to the large gear, to avoid undercutting the smaller gear.
|
||||
// Example(2D,Med,NoAxes,VPT=[37.0558,0.626722,9.78411],VPR=[0,0,0],VPD=496): For the same pair of module 4 gears with 19, and 37 teeth, suppose we want a closer spacing of 110 instead of 112. A negative profile shift does the job, as shown by the red rectangle with width 110. More of the negative shift is assigned to the large gear, to avoid undercutting the smaller gear.
|
||||
// teeth1=37;
|
||||
// teeth2=19;
|
||||
// mod=4;
|
||||
@@ -4568,7 +4639,7 @@ module _show_gear_tooth_profile(
|
||||
profile_shift = default(profile_shift, auto_profile_shift(teeth, pressure_angle, helical));
|
||||
or = outer_radius(mod=mod, teeth=teeth, clearance=clearance, helical=helical, profile_shift=profile_shift, internal=internal);
|
||||
pr = pitch_radius(mod=mod, teeth=teeth, helical=helical);
|
||||
rr = _root_radius(mod=mod, teeth=teeth, helical=helical, profile_shift=profile_shift, clearance=clearance, internal=internal);
|
||||
rr = _root_radius_basic(mod=mod, teeth=teeth, helical=helical, profile_shift=profile_shift, clearance=clearance, internal=internal);
|
||||
br = _base_radius(mod=mod, teeth=teeth, helical=helical, pressure_angle=pressure_angle);
|
||||
tang = 360/teeth;
|
||||
rang = tang * 1.075;
|
||||
|
@@ -3621,23 +3621,17 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
|
||||
// color("blue") down(1)
|
||||
// square((len(field)-1)*pixsize, true);
|
||||
// Example(3D,NoAxes): You can pass a function literal taking x,y arguments, in which case the center value of each pixel is computed in addition to the corners for somewhat greater resolution than the specified pixel size. By default, two smoothing passes are performed on the output paths when making contours from a function.
|
||||
// function wave2d(x,y,wavelen) =
|
||||
// wavelen=42;
|
||||
// wave2d = function(x,y)
|
||||
// 40*cos(180/wavelen*norm([x,y]));
|
||||
//
|
||||
// isoval=-30;
|
||||
// pixsize = 10;
|
||||
// wavelen=42;
|
||||
// translate([0,0,isoval]) color("green") zrot(-90)
|
||||
// contour(function(x,y) wave2d(x,y,wavelen),
|
||||
// contour(wave2d,
|
||||
// bounding_box=[[-50,-50],[50,50]],
|
||||
// isovalue=[isoval,INF], pixel_size=pixsize);
|
||||
//
|
||||
// %heightfield(size=[100,100], bottom=-45, data=[
|
||||
// for (y=[-50:pixsize:50]) [
|
||||
// for(x=[-50:pixsize:50])
|
||||
// wave2d(x,y,wavelen)
|
||||
// ]
|
||||
// ], style="quincunx");
|
||||
// %plot3d(wave2d, [-50:pixsize:50],[-50:pixsize:50],
|
||||
// style="quincunx",base=5);
|
||||
// Example(2D,NoAxes): Here's a simple function that produces a contour in the shape of a flower with some petals. Note that the function has smaller values inside the shape so we choose a `-INF` bound for the isovalue.
|
||||
// f = function (x, y, petals=5)
|
||||
// sin(petals*atan2(y,x)) + norm([x,y]);
|
||||
@@ -3665,13 +3659,8 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
|
||||
// bbox = 720;
|
||||
// up(isovalue) color("red") linear_extrude(1)
|
||||
// contour(f, [isovalue,INF], bbox, pixel_size);
|
||||
// %heightfield(size=[720,720], data = [
|
||||
// for (y=[-360:pixel_size/2:360]) [
|
||||
// for(x=[-360:pixel_size/2:360])
|
||||
// f(x,y)
|
||||
// ]
|
||||
// ],
|
||||
// bottom=-70, maxz=70, style="quincunx");
|
||||
// %plot3d(f, [-360:pixel_size/2:360],
|
||||
// [-360:pixel_size/2:360], style="quincunx");
|
||||
// Example(2D,NoAxes): A [Cassini oval](https://en.wikipedia.org/wiki/Cassini_oval) is a curve drawn such that for any point on the perimeter, the product of the distances from two fixed points is constant. The curve resembles two circular [metaballs](#functionmodule-metaballs2d) interacting. When the ratio `b/a=1`, there is a cusp where two contours meet at the origin, although the contour algorithm doesn't allow the two contours to touch.
|
||||
// a=4; b=4.1;
|
||||
// f = function(x,y) (x^2+y^2)^2 - 2*a^2*(x^2-y^2) + a^4;
|
||||
|
@@ -501,7 +501,7 @@ function reverse(list) =
|
||||
// Description:
|
||||
// Rotates the contents of a list by `n` positions left, so that list[n] becomes the first entry of the list.
|
||||
// If `n` is negative, then the rotation is `abs(n)` positions to the right.
|
||||
// If `list` is a string, then a string is returned with the characters rotates within the string.
|
||||
// If `list` is a string, then a string is returned with the characters rotated within the string.
|
||||
// Arguments:
|
||||
// list = The list to rotate.
|
||||
// n = The number of positions to rotate by. If negative, rotated to the right. Positive rotates to the left. Default: 1
|
||||
|
93
masks2d.scad
93
masks2d.scad
@@ -35,9 +35,9 @@ function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
|
||||
// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
|
||||
// See Also: corner_profile(), edge_profile(), face_profile(), fillet()
|
||||
// Usage: As module
|
||||
// mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=]) [ATTACHMENTS];
|
||||
// mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=], [clip_angle=]) [ATTACHMENTS];
|
||||
// Usage: As function
|
||||
// path = mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=]);
|
||||
// path = mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=], [clip_angle=]);
|
||||
// Description:
|
||||
// Creates a 2D roundover/bead mask shape that is useful for extruding into a 3D mask for an edge.
|
||||
// Conversely, you can use that same extruded shape to make an interior fillet between two walls.
|
||||
@@ -52,7 +52,12 @@ function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
|
||||
// in different directions.) You can get the same height by setting the `height` parameter, which is an alternate way to control the size of the rounding.
|
||||
// You can also set `quarter_round=true`, which creates a rounding that uses a quarter circle of the specified radius for all mask angles. If you have set inset
|
||||
// you will need `flat_top=true` as well. Note that this is the default if you use `quarter_round=true` but not otherwise. Generally if you want a roundover
|
||||
// results are best using the `height` option but if you want a bead as you get using `inset` the results are often best using the `quarter_round=true` option.
|
||||
// results are best using the `height` option but if you want a bead as you get using `inset` the results are often best using the `quarter_round=true` option.
|
||||
// .
|
||||
// If you set the `clip_angle` option then the bottom of the arc is clipped at the specified angle from vertical. This
|
||||
// can be useful for creating bottom roundings for 3d printing. If you specify the radius either directly or indirectly
|
||||
// using `cut` or `joint` and combine that with a height specification using `h` or `height`, then `clip_angle` is automatically
|
||||
// calculated and a clipped circle of the specified height and radius is produced.
|
||||
// Arguments:
|
||||
// r = Radius of the roundover.
|
||||
// inset = Optional bead inset size, perpendicular to the two edges. Scalar or 2-vector. Default: 0
|
||||
@@ -65,6 +70,7 @@ function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
|
||||
// joint = Joint distance. IE: How far from the edge the roundover should start. See [Types of Roundovers](rounding.scad#section-types-of-roundovers).
|
||||
// flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle. Default: true if quarter_round is set, false otherwise.
|
||||
// quarter_round = If true, make a roundover independent of the mask_angle, defined based on a quarter circle of the specified size. Creates mask with angle-independent height. Default: false.
|
||||
// clip_angle = Clip the bottom of the rounding where the circle is this angle from the vertical. Must be between mask_angle-90 and 90 degrees. Default: 90 (no clipping)
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
//
|
||||
@@ -88,6 +94,12 @@ function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
|
||||
// mask2d_roundover(r=10, inset=2, mask_angle=50, quarter_round=true);
|
||||
// Example(2D): quarter_round bead on an obtuse angle
|
||||
// mask2d_roundover(r=10, inset=2, mask_angle=135, quarter_round=true);
|
||||
// Example(2D): clipping a circle to a 50 deg angle
|
||||
// mask2d_roundover(r=10, inset=1/2, clip_angle=50);
|
||||
// Example(2D): clipping a circle to a 50 deg angle. The bottom of the arc is not tangent to the x axis.
|
||||
// mask2d_roundover(r=10, inset=1/2, clip_angle=50);
|
||||
// Example(2D): clipping the arc by specifying `r` and `h`
|
||||
// mask2d_roundover(mask_angle=66, r=10, h=12, inset=1);
|
||||
// Example: Masking by Edge Attachment
|
||||
// diff()
|
||||
// cube([50,60,70],center=true)
|
||||
@@ -126,8 +138,8 @@ function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
|
||||
// prismoid([30,20], [50,60], h=20, shift=[40,50])
|
||||
// edge_profile(TOP, excess=27)
|
||||
// mask2d_roundover(r=5, mask_angle=$edge_angle, quarter_round=true, inset=1.5, $fn=128);
|
||||
module mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, d, h, height, cut, quarter_round=false, joint, anchor=CENTER,spin=0) {
|
||||
path = mask2d_roundover(r=r, d=d, h=h, height=height, cut=cut, joint=joint, inset=inset,
|
||||
module mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, d, h, height, cut, quarter_round=false, joint, anchor=CENTER,spin=0, clip_angle) {
|
||||
path = mask2d_roundover(r=r, d=d, h=h, height=height, cut=cut, joint=joint, inset=inset, clip_angle=clip_angle,
|
||||
flat_top=flat_top, mask_angle=mask_angle, excess=excess, quarter_round=quarter_round);
|
||||
attachable(anchor,spin, two_d=true, path=path) {
|
||||
polygon(path);
|
||||
@@ -136,36 +148,51 @@ module mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, d, h,
|
||||
}
|
||||
|
||||
|
||||
function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, quarter_round=false, d, h, height, cut, joint, anchor=CENTER, spin=0) =
|
||||
assert(one_defined([r,height,d,h,cut,joint],"r,height,d,h,cut,joint"))
|
||||
|
||||
function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, clip_angle, flat_top, quarter_round=false, d, h, height, cut, joint, anchor=CENTER, spin=0) =
|
||||
assert(num_defined([r,d,cut,joint])<=1, "Must define at most one of r, d, cut and joint")
|
||||
assert(num_defined([h,height])<=1, "Must define at most one of h and height")
|
||||
assert(all_nonnegative([excess]), "excess must be a nonnegative value")
|
||||
assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
|
||||
assert(is_finite(inset)||is_vector(inset,2))
|
||||
assert(is_bool(quarter_round))
|
||||
let(flat_top=default(flat_top, quarter_round))
|
||||
assert(is_bool(flat_top))
|
||||
assert(is_undef(clip_angle) || (is_finite(clip_angle) && clip_angle<=90 && clip_angle>(quarter_round?90:mask_angle)-90),
|
||||
str("\nclip_angle must be between ",(quarter_round?90:mask_angle)-90," and 90"))
|
||||
let(
|
||||
inset = is_list(inset)? inset : [inset,inset],
|
||||
r = get_radius(r=r,d=d,dflt=undef),
|
||||
dummy2=assert(is_def(r) || !quarter_round,"Must give r / d when quarter_round is true"),
|
||||
h = u_add(one_defined([h,height],"h,hight",dflt=undef),flat_top || mask_angle>=90?0:-inset.x*cos(mask_angle)),
|
||||
h = u_add(one_defined([h,height],"h,hight",dflt=undef),flat_top || mask_angle>=90?0:-inset.x*cos(mask_angle)),
|
||||
// compute [joint length, radius] for different types of input
|
||||
jr = is_def(h) ? assert(all_positive([h]), "height / h must be larger than y inset")
|
||||
h/sin(mask_angle)*[1,tan(mask_angle/2)]
|
||||
: is_def(r) ? assert(all_positive([r]), "r / d must be a positive value")
|
||||
[r/tan(mask_angle/2), r]
|
||||
: is_def(joint) ? assert(all_positive([joint]), "joint must be a positive value")
|
||||
joint*[1, tan(mask_angle/2)]
|
||||
: assert(all_positive([cut]),"cut must be a positive value")
|
||||
let(circ_radius=cut/(1/sin(mask_angle/2)-1))
|
||||
[circ_radius/tan(mask_angle/2), circ_radius],
|
||||
dist=jr[0],
|
||||
radius=jr[1],
|
||||
rcalc = is_def(r) ? assert(all_positive([r]), "r / d must be a positive value") r
|
||||
: is_def(joint) ? assert(all_positive([joint]), "joint must be a positive value") joint*tan(mask_angle/2)
|
||||
: is_def(cut) ? assert(all_positive([cut]),"cut must be a positive value") cut/(1/sin(mask_angle/2)-1)
|
||||
: undef,
|
||||
jra = is_def(clip_angle)?
|
||||
assert(num_defined([rcalc,h])==1, "When clip_angle is given must give exactly one of r, joint, h/height, or cut")
|
||||
let( r = is_def(rcalc) ? rcalc
|
||||
: h/(sin(mask_angle)/tan(mask_angle/2)-1+sin(clip_angle))
|
||||
)
|
||||
[r/tan(mask_angle/2), r, clip_angle]
|
||||
: num_defined([rcalc,h])==2 ? let( a=-sin(mask_angle)/tan(mask_angle/2)+1)
|
||||
assert(h/rcalc + a <= 1,str("\nheight cannot be larger than ", rcalc*(1-a)))
|
||||
[rcalc/tan(mask_angle/2) ,rcalc, asin(h/rcalc + a)]
|
||||
: is_def(rcalc) ? [rcalc/tan(mask_angle/2), rcalc, 90]
|
||||
: [ each h/sin(mask_angle)*[1,tan(mask_angle/2)], 90],
|
||||
dist=jra[0],
|
||||
radius=jra[1],
|
||||
clip_angle = jra[2],
|
||||
|
||||
clipshift = clip_angle==90 ? [0,0]
|
||||
: let( v=1-cos(90-clip_angle))
|
||||
radius*[v/tan(mask_angle),v],
|
||||
quarter_round_top = approx(mask_angle,90) ? 0
|
||||
: radius/tan(mask_angle),
|
||||
: radius/tan(mask_angle),
|
||||
extra = radius/20, // Exact solution is tangent, which will make bad geometry, so insert an offset factor
|
||||
quarter_round_shift = !quarter_round || mask_angle<=90 ? 0
|
||||
: radius/sin(180-mask_angle)-radius+extra,
|
||||
: radius/sin(180-mask_angle)-radius+extra,
|
||||
outside_corner = _inset_corner(
|
||||
quarter_round ?
|
||||
[
|
||||
@@ -188,18 +215,26 @@ function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, quar
|
||||
outside_corner[1][2]
|
||||
],
|
||||
dummy=assert(last(cornerpath).x>=0,str("inset.y is too large to fit roundover at angle ",mask_angle)),
|
||||
path = deduplicate([
|
||||
each outside_corner[0],
|
||||
outside_corner[1][0],
|
||||
each arc(corner=cornerpath, r=radius),
|
||||
outside_corner[1][2]
|
||||
arcpath = let (basic = arc(corner=cornerpath, r=radius))
|
||||
clip_angle==90 ? basic
|
||||
:
|
||||
let(
|
||||
cutind = [for(i=idx(basic)) if (basic[i].y-inset.y < clipshift.y) i],
|
||||
ipt = line_intersection([basic[cutind[0]-1],basic[cutind[0]]], [[0,clipshift.y+inset.y],[1,clipshift.y+inset.y]])
|
||||
)
|
||||
move(-clipshift, [ each select(basic, 0,cutind[0]), ipt]),
|
||||
path = deduplicate([
|
||||
[last(arcpath).x,-excess],
|
||||
outside_corner[0][1],
|
||||
move(-clipshift, outside_corner[0][2]),
|
||||
each arcpath,
|
||||
[last(arcpath).x,inset.y]
|
||||
]
|
||||
,closed=true)
|
||||
) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
|
||||
|
||||
|
||||
|
||||
|
||||
// Function&Module: mask2d_teardrop()
|
||||
// Synopsis: Creates a 2D teardrop shape with specified max angle from vertical.
|
||||
// SynTags: Geom, Path
|
||||
@@ -615,7 +650,7 @@ function mask2d_chamfer(edge, angle, inset=0, excess=0.01, mask_angle=90, flat_t
|
||||
: is_def(x) ? assert(num_defined([y,edge,angle])<=1, "Conflicting values of x, y, height, edge and angle given")
|
||||
(
|
||||
is_def(y) ? [x,y]
|
||||
: is_def(edge) ? let(yopt=quadratic_roots(1,-2*x*cos(mask_angle), x^2-edge^2,real=true),fff=echo(yopt))
|
||||
: is_def(edge) ? let(yopt=quadratic_roots(1,-2*x*cos(mask_angle), x^2-edge^2,real=true))
|
||||
assert(yopt!=[] && max(yopt)>0, "edge too short for x value")
|
||||
[x,max(yopt)]
|
||||
: let(angle=default(angle,mask_angle/2))
|
||||
@@ -832,7 +867,7 @@ function mask2d_dovetail(edge, angle, slope, shelf=0, inset=0, mask_angle=90, ex
|
||||
// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
|
||||
// See Also: corner_profile(), edge_profile(), face_profile()
|
||||
// Usage: As Module
|
||||
// mask2d_ogee(pattern, [excess], ...) [ATTAHCMENTS];
|
||||
// mask2d_ogee(pattern, [excess], ...) [ATTACHMENTS];
|
||||
// Usage: As Function
|
||||
// path = mask2d_ogee(pattern, [excess], ...);
|
||||
// Description:
|
||||
|
185
math.scad
185
math.scad
@@ -56,7 +56,7 @@ NAN = acos(2);
|
||||
// Creates a list of `n` numbers, starting at `s`, incrementing by `step` each time.
|
||||
// You can also pass a list for n and then the length of the input list is used.
|
||||
// Arguments:
|
||||
// n = The length of the list of numbers to create, or a list to match the length of
|
||||
// n = The length of the list of numbers to create, or a list to match the length of.
|
||||
// s = The starting value of the list of numbers.
|
||||
// step = The amount to increment successive numbers in the list.
|
||||
// reverse = Reverse the list. Default: false.
|
||||
@@ -80,11 +80,12 @@ function count(n,s=0,step=1,reverse=false) = let(n=is_list(n) ? len(n) : n)
|
||||
// l = lerp(a, b, LIST);
|
||||
// Description:
|
||||
// Interpolate between two values or vectors.
|
||||
// If `u` is given as a number, returns the single interpolated value.
|
||||
// If `u` is 0.0, then the value of `a` is returned.
|
||||
// If `u` is 1.0, then the value of `b` is returned.
|
||||
// If `u` is a range, or list of numbers, returns a list of interpolated values.
|
||||
// It is valid to use a `u` value outside the range 0 to 1. The result will be an extrapolation
|
||||
// * If `u` is given as a number, returns the single interpolated value.
|
||||
// * If `u` is 0.0, then the value of `a` is returned.
|
||||
// * If `u` is 1.0, then the value of `b` is returned.
|
||||
// * If `u` is a range, or list of numbers, returns a list of interpolated values.
|
||||
// .
|
||||
// It is valid to use a `u` value outside the range 0 to 1 to extrapolate
|
||||
// along the slope formed by `a` and `b`.
|
||||
// Arguments:
|
||||
// a = First value or vector.
|
||||
@@ -105,9 +106,9 @@ function count(n,s=0,step=1,reverse=false) = let(n=is_list(n) ? len(n) : n)
|
||||
// // Points colored in ROYGBIV order.
|
||||
// rainbow(pts) translate($item) circle(d=3,$fn=8);
|
||||
function lerp(a,b,u) =
|
||||
assert(same_shape(a,b), "Bad or inconsistent inputs to lerp")
|
||||
assert(same_shape(a,b), "\nBad or inconsistent inputs to lerp.")
|
||||
is_finite(u)? (1-u)*a + u*b :
|
||||
assert(is_finite(u) || is_vector(u) || valid_range(u), "Input u to lerp must be a number, vector, or valid range.")
|
||||
assert(is_finite(u) || is_vector(u) || valid_range(u), "\nInput u to lerp must be a number, vector, or valid range.")
|
||||
[for (v = u) (1-v)*a + v*b ];
|
||||
|
||||
|
||||
@@ -133,7 +134,7 @@ function lerp(a,b,u) =
|
||||
// l = lerpn(0,1,6); // Returns: [0, 0.2, 0.4, 0.6, 0.8, 1]
|
||||
// l = lerpn(0,1,5,false); // Returns: [0, 0.2, 0.4, 0.6, 0.8]
|
||||
function lerpn(a,b,n,endpoint=true) =
|
||||
assert(same_shape(a,b), "Bad or inconsistent inputs to lerpn")
|
||||
assert(same_shape(a,b), "\nBad or inconsistent inputs to lerpn.")
|
||||
assert(is_int(n))
|
||||
assert(is_bool(endpoint))
|
||||
let( d = n - (endpoint? 1 : 0) )
|
||||
@@ -148,7 +149,7 @@ function lerpn(a,b,n,endpoint=true) =
|
||||
// Description:
|
||||
// Compute bilinear interpolation between four values using two
|
||||
// coordinates that are meant to lie in [0,1]. (If they are outside
|
||||
// this range, the function will extrapolate values.) The `pts`
|
||||
// this range, the function extrapolates values.) The `pts`
|
||||
// argument is a list of the four values at the for corners, `[A,B,C,D]`.
|
||||
// These values are arranged on the corners as shown below. The `x` and
|
||||
// `y` parameters give the fraction of the distance from the left and bottom
|
||||
@@ -196,7 +197,7 @@ function bilerp(points,x,y) =
|
||||
// sqr([2,3,4]); // Returns: 29
|
||||
// sqr([[1,2],[3,4]]); // Returns [[7,10],[15,22]]
|
||||
function sqr(x) =
|
||||
assert(is_finite(x) || is_vector(x) || is_matrix(x), "Input is not a number nor a list of numbers.")
|
||||
assert(is_finite(x) || is_vector(x) || is_matrix(x), "\nInput is not a number nor a list of numbers.")
|
||||
x*x;
|
||||
|
||||
|
||||
@@ -213,7 +214,7 @@ function sqr(x) =
|
||||
// log2(16); // Returns: 4
|
||||
// log2(256); // Returns: 8
|
||||
function log2(x) =
|
||||
assert( is_finite(x), "Input is not a number.")
|
||||
assert( is_finite(x), "\nInput is not a number.")
|
||||
ln(x)/ln(2);
|
||||
|
||||
// this may return NAN or INF; should it check x>0 ?
|
||||
@@ -234,7 +235,7 @@ function log2(x) =
|
||||
// l = hypot(3,4); // Returns: 5
|
||||
// l = hypot(3,4,5); // Returns: ~7.0710678119
|
||||
function hypot(x,y,z=0) =
|
||||
assert( is_vector([x,y,z]), "Improper number(s).")
|
||||
assert( is_vector([x,y,z]), "\nImproper number(s).")
|
||||
norm([x,y,z]);
|
||||
|
||||
|
||||
@@ -248,14 +249,14 @@ function hypot(x,y,z=0) =
|
||||
// Returns the factorial of the given integer value, or n!/d! if d is given.
|
||||
// Arguments:
|
||||
// n = The integer number to get the factorial of. (n!)
|
||||
// d = If given, the returned value will be (n! / d!)
|
||||
// d = If given, the returned value is (n! / d!)
|
||||
// Example:
|
||||
// x = factorial(4); // Returns: 24
|
||||
// y = factorial(6); // Returns: 720
|
||||
// z = factorial(9); // Returns: 362880
|
||||
function factorial(n,d=0) =
|
||||
assert(is_int(n) && is_int(d) && n>=0 && d>=0, "Factorial is defined only for non negative integers")
|
||||
assert(d<=n, "d cannot be larger than n")
|
||||
assert(is_int(n) && is_int(d) && n>=0 && d>=0, "\nFactorial is defined only for non negative integers.")
|
||||
assert(d<=n, "\nd cannot be larger than n.")
|
||||
product([1,for (i=[n:-1:d+1]) i]);
|
||||
|
||||
|
||||
@@ -274,7 +275,7 @@ function factorial(n,d=0) =
|
||||
// y = binomial(4); // Returns: [1,4,6,4,1]
|
||||
// z = binomial(6); // Returns: [1,6,15,20,15,6,1]
|
||||
function binomial(n) =
|
||||
assert( is_int(n) && n>0, "Input is not an integer greater than 0.")
|
||||
assert( is_int(n) && n>0, "\nInput must be an integer greater than 0.")
|
||||
[for( c = 1, i = 0;
|
||||
i<=n;
|
||||
c = c*(n-i)/(i+1), i = i+1
|
||||
@@ -296,7 +297,7 @@ function binomial(n) =
|
||||
// x = binomial_coefficient(3,2); // Returns: 3
|
||||
// y = binomial_coefficient(10,6); // Returns: 210
|
||||
function binomial_coefficient(n,k) =
|
||||
assert( is_int(n) && is_int(k), "Some input is not a number.")
|
||||
assert( is_int(n) && is_int(k), "\nSome input is not a number.")
|
||||
k < 0 || k > n ? 0 :
|
||||
k ==0 || k ==n ? 1 :
|
||||
let( k = min(k, n-k),
|
||||
@@ -316,14 +317,14 @@ function binomial_coefficient(n,k) =
|
||||
// Description:
|
||||
// Computes the Greatest Common Divisor/Factor of `a` and `b`.
|
||||
function gcd(a,b) =
|
||||
assert(is_int(a) && is_int(b),"Arguments to gcd must be integers")
|
||||
assert(is_int(a) && is_int(b),"\nArguments to gcd must be integers.")
|
||||
b==0 ? abs(a) : gcd(b,a % b);
|
||||
|
||||
|
||||
// Computes lcm for two integers
|
||||
function _lcm(a,b) =
|
||||
assert(is_int(a) && is_int(b), "Invalid non-integer parameters to lcm")
|
||||
assert(a!=0 && b!=0, "Arguments to lcm should not be zero")
|
||||
assert(is_int(a) && is_int(b), "\nInvalid non-integer parameters to lcm.")
|
||||
assert(a!=0 && b!=0, "\nArguments to lcm must be non-zero.")
|
||||
abs(a*b) / gcd(a,b);
|
||||
|
||||
|
||||
@@ -348,7 +349,7 @@ function lcm(a,b=[]) =
|
||||
!is_list(a) && !is_list(b)
|
||||
? _lcm(a,b)
|
||||
: let( arglist = concat(force_list(a),force_list(b)) )
|
||||
assert(len(arglist)>0, "Invalid call to lcm with empty list(s)")
|
||||
assert(len(arglist)>0, "\nInvalid call to lcm with empty list(s).")
|
||||
_lcmlist(arglist);
|
||||
|
||||
// Function rational_approx()
|
||||
@@ -392,7 +393,7 @@ function _cfrac_to_pq(cfrac,p=0,q=1,ind) =
|
||||
// a = sinh(x);
|
||||
// Description: Takes a value `x`, and returns the hyperbolic sine of it.
|
||||
function sinh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
(exp(x)-exp(-x))/2;
|
||||
|
||||
// Function: cosh()
|
||||
@@ -403,7 +404,7 @@ function sinh(x) =
|
||||
// a = cosh(x);
|
||||
// Description: Takes a value `x`, and returns the hyperbolic cosine of it.
|
||||
function cosh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
(exp(x)+exp(-x))/2;
|
||||
|
||||
|
||||
@@ -416,7 +417,7 @@ function cosh(x) =
|
||||
// Description: Takes a value `x`, and returns the hyperbolic tangent of it.
|
||||
|
||||
function tanh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
let (e = exp(2*x) + 1)
|
||||
e == INF ? 1 : (e-2)/e;
|
||||
|
||||
@@ -428,7 +429,7 @@ function tanh(x) =
|
||||
// a = asinh(x);
|
||||
// Description: Takes a value `x`, and returns the inverse hyperbolic sine of it.
|
||||
function asinh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
ln(x+sqrt(x*x+1));
|
||||
|
||||
|
||||
@@ -440,7 +441,7 @@ function asinh(x) =
|
||||
// a = acosh(x);
|
||||
// Description: Takes a value `x`, and returns the inverse hyperbolic cosine of it.
|
||||
function acosh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
ln(x+sqrt(x*x-1));
|
||||
|
||||
|
||||
@@ -452,7 +453,7 @@ function acosh(x) =
|
||||
// a = atanh(x);
|
||||
// Description: Takes a value `x`, and returns the inverse hyperbolic tangent of it.
|
||||
function atanh(x) =
|
||||
assert(is_finite(x), "The input must be a finite number.")
|
||||
assert(is_finite(x), "\nThe input must be a finite number.")
|
||||
ln((1+x)/(1-x))/2;
|
||||
|
||||
|
||||
@@ -467,7 +468,7 @@ function atanh(x) =
|
||||
// Description:
|
||||
// Quantize a value `x` to an integer multiple of `y`, rounding to the nearest multiple.
|
||||
// The value of `y` does NOT have to be an integer. If `x` is a list, then every item
|
||||
// in that list will be recursively quantized.
|
||||
// in that list is recursively quantized.
|
||||
// Arguments:
|
||||
// x = The value or list to quantize.
|
||||
// y = Positive quantum to quantize to
|
||||
@@ -491,7 +492,7 @@ function atanh(x) =
|
||||
// q = quant([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,12,12,12]
|
||||
// r = quant([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[12,12,12]]
|
||||
function quant(x,y) =
|
||||
assert( is_finite(y) && y>0, "The quantum `y` must be a positive value.")
|
||||
assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.")
|
||||
is_num(x) ? round(x/y)*y
|
||||
: _roundall(x/y)*y;
|
||||
|
||||
@@ -508,7 +509,7 @@ function _roundall(data) =
|
||||
// Description:
|
||||
// Quantize a value `x` to an integer multiple of `y`, rounding down to the previous multiple.
|
||||
// The value of `y` does NOT have to be an integer. If `x` is a list, then every item in that
|
||||
// list will be recursively quantized down.
|
||||
// list is recursively quantized down.
|
||||
// Arguments:
|
||||
// x = The value or list to quantize.
|
||||
// y = Postive quantum to quantize to.
|
||||
@@ -532,7 +533,7 @@ function _roundall(data) =
|
||||
// q = quantdn([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,9,9,12]
|
||||
// r = quantdn([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[9,9,12]]
|
||||
function quantdn(x,y) =
|
||||
assert( is_finite(y) && y>0, "The quantum `y` must be a positive value.")
|
||||
assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.")
|
||||
is_num(x) ? floor(x/y)*y
|
||||
: _floorall(x/y)*y;
|
||||
|
||||
@@ -549,7 +550,7 @@ function _floorall(data) =
|
||||
// Description:
|
||||
// Quantize a value `x` to an integer multiple of `y`, rounding up to the next multiple.
|
||||
// The value of `y` does NOT have to be an integer. If `x` is a list, then every item in
|
||||
// that list will be recursively quantized up.
|
||||
// that list is recursively quantized up.
|
||||
// Arguments:
|
||||
// x = The value or list to quantize.
|
||||
// y = Positive quantum to quantize to.
|
||||
@@ -573,7 +574,7 @@ function _floorall(data) =
|
||||
// q = quantup([9,10,10.4,10.5,11,12],3); // Returns: [9,12,12,12,12,12]
|
||||
// r = quantup([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,12,12],[12,12,12]]
|
||||
function quantup(x,y) =
|
||||
assert( is_finite(y) && y>0, "The quantum `y` must be a positive value.")
|
||||
assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.")
|
||||
is_num(x) ? ceil(x/y)*y
|
||||
: _ceilall(x/y)*y;
|
||||
|
||||
@@ -602,7 +603,7 @@ function _ceilall(data) =
|
||||
// d = constrain(9.1, 0, 9); // Returns: 9
|
||||
// e = constrain(-0.1, 0, 9); // Returns: 0
|
||||
function constrain(v, minval, maxval) =
|
||||
assert( is_finite(v+minval+maxval), "Input must be finite number(s).")
|
||||
assert( is_finite(v+minval+maxval), "\nInput must be finite number(s).")
|
||||
min(maxval, max(minval, v));
|
||||
|
||||
|
||||
@@ -613,7 +614,7 @@ function constrain(v, minval, maxval) =
|
||||
// Usage:
|
||||
// mod = posmod(x, m)
|
||||
// Description:
|
||||
// Returns the positive modulo `m` of `x`. Value returned will be satisfy `0 <= mod < m`.
|
||||
// Returns the positive modulo `m` of `x`. The value returned satisfies `0 <= mod < m`.
|
||||
// Arguments:
|
||||
// x = The value to constrain.
|
||||
// m = Modulo value.
|
||||
@@ -626,7 +627,7 @@ function constrain(v, minval, maxval) =
|
||||
// f = posmod(700,360); // Returns: 340
|
||||
// g = posmod(3,2.5); // Returns: 0.5
|
||||
function posmod(x,m) =
|
||||
assert( is_finite(x) && is_finite(m) && !approx(m,0) , "Input must be finite numbers. The divisor cannot be zero.")
|
||||
assert( is_finite(x) && is_finite(m) && !approx(m,0) , "\nInput must be finite numbers. The divisor cannot be zero.")
|
||||
(x%m+m)%m;
|
||||
|
||||
|
||||
@@ -646,7 +647,7 @@ function posmod(x,m) =
|
||||
// a5 = modang(270); // Returns: -90
|
||||
// a6 = modang(700); // Returns: -20
|
||||
function modang(x) =
|
||||
assert( is_finite(x), "Input must be a finite number.")
|
||||
assert( is_finite(x), "\nInput must be a finite number.")
|
||||
let(xx = posmod(x,360)) xx<180? xx : xx-360;
|
||||
|
||||
|
||||
@@ -660,12 +661,12 @@ function modang(x) =
|
||||
// Takes two angles (degrees) in any range and finds the angle halfway between
|
||||
// the given angles, where halfway is interpreted using the shorter direction.
|
||||
// In the case where the angles are exactly 180 degrees apart,
|
||||
// it will return `angle1+90`. The returned angle is always in the interval [0,360).
|
||||
// it returns `angle1+90`. The returned angle is always in the interval [0,360).
|
||||
// Arguments:
|
||||
// angle1 = first angle
|
||||
// angle2 = second angle
|
||||
function mean_angle(angle1,angle2) =
|
||||
assert(is_vector([angle1,angle2]), "Inputs must be finite numbers.")
|
||||
assert(is_vector([angle1,angle2]), "\nInputs must be finite numbers.")
|
||||
let(
|
||||
ang1 = posmod(angle1,360),
|
||||
ang2 = posmod(angle2,360)
|
||||
@@ -675,6 +676,62 @@ function mean_angle(angle1,angle2) =
|
||||
: posmod((ang1+ang2-360)/2,360);
|
||||
|
||||
|
||||
// Function: fit_to_range()
|
||||
// Synopsis: Scale the values in an array to span a range.
|
||||
// Topics: Math, Bounds, Scaling
|
||||
// See Also: fit_to_box()
|
||||
// Usage:
|
||||
// a = fit_to_range(M, minval, maxval);
|
||||
// Description:
|
||||
// Given a vector or list of vectors, scale the values so that they span the full range from `minval` to
|
||||
// `maxval`. If `minval>maxval`, then the output is a rescaled mirror image of the input.
|
||||
// Arguments:
|
||||
// M = vector or list of vectors to scale. A list of vectors needn't be a rectangular matrix; the vectors can have different lengths.
|
||||
// minval = Minimum value of the rescaled data range.
|
||||
// maxval = Maximum value of the rescaled data range.
|
||||
// Example:
|
||||
// a = [0.0066, 0.194, 0.598, 0.194, 0.0066];
|
||||
// v = fit_to_range(a,5,10);
|
||||
// // Returns: [5, 6.584, 10, 6.584, 5]
|
||||
//
|
||||
// b = [ [20,20,0], [40,80,20], [60,40,20] ];
|
||||
// m = fit_to_range(b,-10,10);
|
||||
// // Returns: [[-5,-5,-10], [0,10,-5], [5,0,-5]]
|
||||
//
|
||||
// c = [2,3,4,5,6];
|
||||
// inv = fit_to_range(c, 20, 8); // inverted range!
|
||||
// // Returns: [20, 17, 14, 11, 8]
|
||||
// Example(3D): A texture tile that spans the range [-1,1] is rescaled to span [0,1], resulting in the edges of the texture (which were at z=0) to be raised due to raising the minimu from -1 to 0.
|
||||
// tex = [
|
||||
// [0,0,0, 0, 0, 0,0,0,0],
|
||||
// [0,1,1, 1, 1, 1,1,1,0],
|
||||
// [0,1,0, 0, 0, 0,0,1,0],
|
||||
// [0,1,0,-1,-1,-1,0,1,0],
|
||||
// [0,1,0,-1, 0,-1,0,1,0],
|
||||
// [0,1,0,-1,-1,-1,0,1,0],
|
||||
// [0,1,0, 0, 0, 0,0,1,0],
|
||||
// [0,1,1, 1, 1, 1,1,1,0],
|
||||
// [0,0,0, 0, 0, 0,0,0,0]
|
||||
// ];
|
||||
// left(5) textured_tile(tex,
|
||||
// [9,9,2],tex_reps=1, anchor=BOTTOM);
|
||||
// right(5) textured_tile(fit_to_range(tex,0,1),
|
||||
// [9,9,2],tex_reps=1, anchor=BOTTOM);
|
||||
|
||||
function fit_to_range(M, minval, maxval) =
|
||||
let(
|
||||
is_vec = is_vector(M),
|
||||
dum = assert(is_vec || (is_list(M) && is_vector(M[0])), "\nParameter M must be a vector or list of vectors."),
|
||||
rowlen = len(is_vec ? M : M[0]),
|
||||
v = is_vec ? M : flatten(M),
|
||||
a = min(v),
|
||||
b = max(v)
|
||||
) a==b ? M
|
||||
: is_vec ? add_scalar(add_scalar(M,-a) * ((maxval-minval)/(b-a)), minval)
|
||||
: [ for(row=M)
|
||||
add_scalar(add_scalar(row, -a) * ((maxval-minval)/(b-a)), + minval)
|
||||
];
|
||||
|
||||
|
||||
// Section: Operations on Lists (Sums, Mean, Products)
|
||||
|
||||
@@ -688,7 +745,7 @@ function mean_angle(angle1,angle2) =
|
||||
// Returns the sum of all entries in the given consistent list.
|
||||
// If passed an array of vectors, returns the sum the vectors.
|
||||
// If passed an array of matrices, returns the sum of the matrices.
|
||||
// If passed an empty list, the value of `dflt` will be returned.
|
||||
// If passed an empty list, the value of `dflt` is returned.
|
||||
// Arguments:
|
||||
// v = The list to get the sum of.
|
||||
// dflt = The default value to return if `v` is an empty list. Default: 0
|
||||
@@ -697,7 +754,7 @@ function mean_angle(angle1,angle2) =
|
||||
// sum([[1,2,3], [3,4,5], [5,6,7]]); // returns [9, 12, 15]
|
||||
function sum(v, dflt=0) =
|
||||
v==[]? dflt :
|
||||
assert(is_consistent(v), "Input to sum is non-numeric or inconsistent")
|
||||
assert(is_consistent(v), "\nInput to sum is non-numeric or inconsistent.")
|
||||
is_finite(v[0]) || is_vector(v[0]) ? [for(i=v) 1]*v :
|
||||
_sum(v,v[0]*0);
|
||||
|
||||
@@ -721,7 +778,7 @@ function _sum(v,_total,_i=0) = _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1);
|
||||
// mean([2,3,4]); // returns 3.
|
||||
// mean([[1,2,3], [3,4,5], [5,6,7]]); // returns [3, 4, 5]
|
||||
function mean(v) =
|
||||
assert(is_list(v) && len(v)>0, "Invalid list.")
|
||||
assert(is_list(v) && len(v)>0, "\nInvalid list.")
|
||||
sum(v)/len(v);
|
||||
|
||||
|
||||
@@ -735,7 +792,7 @@ function mean(v) =
|
||||
// Description:
|
||||
// Returns the median of the given vector.
|
||||
function median(v) =
|
||||
assert(is_vector(v), "Input to median must be a vector")
|
||||
assert(is_vector(v), "\nInput to median must be a vector.")
|
||||
len(v)%2 ? max( list_smallest(v, ceil(len(v)/2)) ) :
|
||||
let( lowest = list_smallest(v, len(v)/2 + 1),
|
||||
max = max(lowest),
|
||||
@@ -762,7 +819,7 @@ function median(v) =
|
||||
// deltas([2,5,9,17]); // returns [3,4,8].
|
||||
// deltas([[1,2,3], [3,6,8], [4,8,11]]); // returns [[2,4,5], [1,2,3]]
|
||||
function deltas(v, wrap=false) =
|
||||
assert( is_consistent(v) && len(v)>1 , "Inconsistent list or with length<=1.")
|
||||
assert( is_consistent(v) && len(v)>1 , "\nInconsistent list or with length<=1.")
|
||||
[for (p=pair(v,wrap)) p[1]-p[0]] ;
|
||||
|
||||
|
||||
@@ -784,7 +841,7 @@ function deltas(v, wrap=false) =
|
||||
// cumsum([[1,2,3], [3,4,5], [5,6,7]]); // returns [[1,2,3], [4,6,8], [9,12,15]]
|
||||
function cumsum(v) =
|
||||
v==[] ? [] :
|
||||
assert(is_consistent(v), "The input is not consistent." )
|
||||
assert(is_consistent(v), "\nThe input is not consistent." )
|
||||
[for (a = v[0],
|
||||
i = 1
|
||||
;
|
||||
@@ -822,7 +879,7 @@ function product(list,right=true) =
|
||||
if (i==len(list)) a][0]
|
||||
:
|
||||
assert(is_vector(list) || (is_matrix(list[0],square=true) && is_consistent(list)),
|
||||
"Input must be a vector, a list of vectors, or a list of matrices.")
|
||||
"\nInput must be a vector, a list of vectors, or a list of matrices.")
|
||||
[for (a = list[0],
|
||||
i = 1
|
||||
;
|
||||
@@ -842,8 +899,8 @@ function product(list,right=true) =
|
||||
// Description:
|
||||
// Returns a list where each item is the cumulative product of all items up to and including the corresponding entry in the input list.
|
||||
// If passed an array of vectors, returns a list of elementwise vector products. If passed a list of square matrices by default returns matrix
|
||||
// products multiplying on the left, so a list `[A,B,C]` will produce the output `[A,BA,CBA]`. If you set `right=true` then it returns
|
||||
// the product of multiplying on the right, so a list `[A,B,C]` will produce the output `[A,AB,ABC]` in that case.
|
||||
// products multiplying on the left, so a list `[A,B,C]` produces the output `[A,BA,CBA]`. If you set `right=true` then it returns
|
||||
// the product of multiplying on the right, so a list `[A,B,C]` produces the output `[A,AB,ABC]` in that case.
|
||||
// Arguments:
|
||||
// list = The list to get the cumulative product of.
|
||||
// right = if true multiply matrices on the right
|
||||
@@ -865,7 +922,7 @@ function cumprod(list,right=false) =
|
||||
a]
|
||||
:
|
||||
assert(is_vector(list) || (is_matrix(list[0],square=true) && is_consistent(list)),
|
||||
"Input must be a listector, a list of listectors, or a list of matrices.")
|
||||
"\nInput must be a listector, a list of listectors, or a list of matrices.")
|
||||
[for (a = list[0],
|
||||
i = 1
|
||||
;
|
||||
@@ -903,7 +960,7 @@ function convolve(p,q) =
|
||||
p==[] || q==[] ? [] :
|
||||
assert( (is_vector(p) || is_matrix(p))
|
||||
&& ( is_vector(q) || (is_matrix(q) && ( !is_vector(p[0]) || (len(p[0])==len(q[0])) ) ) ) ,
|
||||
"The inputs should be vectors or paths all of the same dimension.")
|
||||
"\nThe inputs should be vectors or paths all of the same dimension.")
|
||||
let( n = len(p),
|
||||
m = len(q))
|
||||
[for(i=[0:n+m-2], k1 = max(0,i-n+1), k2 = min(i,m-1) )
|
||||
@@ -959,8 +1016,8 @@ function sum_of_sines(a, sines) =
|
||||
// ints = rand_int(0,100,3);
|
||||
// int = rand_int(-10,10,1)[0];
|
||||
function rand_int(minval, maxval, n, seed=undef) =
|
||||
assert( is_finite(minval+maxval+n) && (is_undef(seed) || is_finite(seed) ), "Input must be finite numbers.")
|
||||
assert(maxval >= minval, "Max value cannot be smaller than minval")
|
||||
assert( is_finite(minval+maxval+n) && (is_undef(seed) || is_finite(seed) ), "\nInput must be finite numbers.")
|
||||
assert(maxval >= minval, "\nMax value cannot be smaller than minval.")
|
||||
let (rvect = is_def(seed) ? rands(minval,maxval+1,n,seed) : rands(minval,maxval+1,n))
|
||||
[for(entry = rvect) floor(entry)];
|
||||
|
||||
@@ -981,9 +1038,9 @@ function rand_int(minval, maxval, n, seed=undef) =
|
||||
// scale = the scale of the point coordinates. Default: 1
|
||||
// seed = an optional seed for the random generation.
|
||||
function random_points(n, dim, scale=1, seed) =
|
||||
assert( is_int(n) && n>=0, "The number of points should be a non-negative integer.")
|
||||
assert( is_int(dim) && dim>=1, "The point dimensions should be an integer greater than 1.")
|
||||
assert( is_finite(scale) || is_vector(scale,dim), "The scale should be a number or a vector with length equal to d.")
|
||||
assert( is_int(n) && n>=0, "\nThe number of points should be a non-negative integer.")
|
||||
assert( is_int(dim) && dim>=1, "\nThe point dimensions should be an integer greater than 1.")
|
||||
assert( is_finite(scale) || is_vector(scale,dim), "\nThe scale should be a number or a vector with length equal to d.")
|
||||
let(
|
||||
rnds = is_undef(seed)
|
||||
? rands(-1,1,n*dim)
|
||||
@@ -1011,18 +1068,18 @@ function gaussian_rands(n=1, mean=0, cov=1, seed=undef) =
|
||||
let(
|
||||
dim = is_num(mean) ? 1 : len(mean)
|
||||
)
|
||||
assert((dim==1 && is_num(cov)) || is_matrix(cov,dim,dim),"mean and covariance matrix not compatible")
|
||||
assert((dim==1 && is_num(cov)) || is_matrix(cov,dim,dim),"\nmean and covariance matrix not compatible.")
|
||||
assert(is_undef(seed) || is_finite(seed))
|
||||
let(
|
||||
nums = is_undef(seed)? rands(0,1,dim*n*2) : rands(0,1,dim*n*2,seed),
|
||||
rdata = [for (i = count(dim*n,0,2)) sqrt(-2*ln(nums[i]))*cos(360*nums[i+1])]
|
||||
)
|
||||
dim==1 ? add_scalar(sqrt(cov)*rdata,mean) :
|
||||
assert(is_matrix_symmetric(cov),"Supplied covariance matrix is not symmetric")
|
||||
assert(is_matrix_symmetric(cov),"\nSupplied covariance matrix is not symmetric.")
|
||||
let(
|
||||
L = cholesky(cov)
|
||||
)
|
||||
assert(is_def(L), "Supplied covariance matrix is not positive definite")
|
||||
assert(is_def(L), "\nSupplied covariance matrix is not positive definite.")
|
||||
move(mean,list_to_matrix(rdata,dim)*transpose(L));
|
||||
|
||||
|
||||
@@ -1117,7 +1174,7 @@ function random_polygon(n=3,size=1, seed) =
|
||||
// uses a two point method if sufficient points are available: f'(t) = (3*(f(t+h)-f(t)) - (f(t+2*h)-f(t+h)))/2h.
|
||||
// .
|
||||
// If `h` is a vector then it is assumed to be nonuniform, with h[i] giving the sampling distance
|
||||
// between data[i+1] and data[i], and the data values will be linearly resampled at each corner
|
||||
// between data[i+1] and data[i], and the data values are linearly resampled at each corner
|
||||
// to produce a uniform spacing for the derivative estimate. At the endpoints a single point method
|
||||
// is used: f'(t) = (f(t+h)-f(t))/h.
|
||||
// Arguments:
|
||||
@@ -1287,7 +1344,7 @@ function complex(list) =
|
||||
// c = c_mul(z1,z2)
|
||||
// Description:
|
||||
// Multiplies two complex numbers, vectors or matrices, where complex numbers
|
||||
// or entries are represented as vectors: [REAL, IMAGINARY]. Note that all
|
||||
// or entries are represented as vectors: [REAL, IMAGINARY]. All
|
||||
// entries in both arguments must be complex.
|
||||
// Arguments:
|
||||
// z1 = First complex number, vector or matrix
|
||||
@@ -1411,7 +1468,7 @@ function c_norm(z) = norm_fro(z);
|
||||
// roots = quadratic_roots(a, b, c, [real])
|
||||
// Description:
|
||||
// Computes roots of the quadratic equation a*x^2+b*x+c==0, where the
|
||||
// coefficients are real numbers. If real is true then returns only the
|
||||
// coefficients are real numbers. If real is true, then returns only the
|
||||
// real roots. Otherwise returns a pair of complex values. This method
|
||||
// may be more reliable than the general root finder at distinguishing
|
||||
// real roots from complex roots.
|
||||
@@ -1491,7 +1548,7 @@ function poly_mult(p,q) =
|
||||
// Computes division of the numerator polynomial by the denominator polynomial and returns
|
||||
// a list of two polynomials, [quotient, remainder]. If the division has no remainder then
|
||||
// the zero polynomial [0] is returned for the remainder. Similarly if the quotient is zero
|
||||
// the returned quotient will be [0].
|
||||
// the returned quotient is [0].
|
||||
function poly_div(n,d) =
|
||||
assert( is_vector(n) && is_vector(d) , "Invalid polynomials." )
|
||||
let( d = _poly_trim(d),
|
||||
@@ -1664,7 +1721,7 @@ function real_roots(p,eps=undef,tol=1e-14) =
|
||||
// argument. You must have a version of OpenSCAD that supports function literals
|
||||
// (2021.01 or newer). The tolerance (tol) specifies the accuracy of the solution:
|
||||
// abs(f(x)) < tol * yrange, where yrange is the range of observed function values.
|
||||
// This function can only find roots that cross the x axis: it cannot find the
|
||||
// This function can find only those roots that *cross* the x axis: it cannot find the
|
||||
// the root of x^2.
|
||||
// Arguments:
|
||||
// f = function literal for a scalar-valued single variable function
|
||||
|
201
rounding.scad
201
rounding.scad
@@ -547,6 +547,7 @@ function _rounding_offsets(edgespec,z_dir=1) =
|
||||
r = struct_val(edgespec,"r"),
|
||||
cut = struct_val(edgespec,"cut"),
|
||||
k = struct_val(edgespec,"k"),
|
||||
angle = struct_val(edgespec, "angle"),
|
||||
radius = in_list(edgetype,["circle","teardrop"])
|
||||
? (is_def(cut) ? cut/(sqrt(2)-1) : r)
|
||||
:edgetype=="chamfer"
|
||||
@@ -584,7 +585,7 @@ function _rounding_offsets(edgespec,z_dir=1) =
|
||||
[[-2*radius*(1-sqrt(2)/2), z_dir*abs(radius)]]
|
||||
)
|
||||
) :
|
||||
edgetype == "circle"? radius==0? [] : [for(i=[1:N]) [radius*(cos(i*90/N)-1), z_dir*abs(radius)*sin(i*90/N)]] :
|
||||
edgetype == "circle"? radius==0? [] : [for(i=[1:N]) [radius*(cos(i*angle/N)-1), z_dir*abs(radius)*sin(i*angle/N)]] :
|
||||
/* smooth */ joint==0 ? [] :
|
||||
list_tail(
|
||||
_bezcorner([[0,0],[0,z_dir*abs(joint)],[-joint,z_dir*abs(joint)]], k, $fn=N+2)
|
||||
@@ -1370,11 +1371,11 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// .
|
||||
// - profile: os_profile(points)
|
||||
// Define the offset profile with a list of points. The first point must be [0,0] and the roundover should rise in the positive y direction, with positive x values for inward motion (standard roundover) and negative x values for flaring outward. If the y value ever decreases then you might create a self-intersecting polyhedron, which is invalid. Such invalid polyhedra create cryptic assertion errors when you render your model and it is your responsibility to avoid creating them. Note that the starting point of the profile is the center of the extrusion. If you use a profile as the top, it rises upward. If you use it as the bottom, it is inverted and goes downward.
|
||||
// - circle: os_circle(r|cut). Define circular rounding either by specifying the radius or cut distance.
|
||||
// - circle: os_circle(r|cut=,height=|h=,[clip_angle=],). Define circular rounding or clipped circle rounding. You specify a full circle rounding by giving the radius, cut distance or height (which is equivalent to radius in this case). For a clipped circle rounding you can use two methods. You can specify the clip angle and then give a radius, cut, or height. (The cut distance in this case is the usual cut for a full circular arc.) Alternatively you can give a height and radius (or cut) and the appropriate clip angle is chosen for you.
|
||||
// - smooth: os_smooth(cut|joint, [k]). Define continuous curvature rounding, with `cut` and `joint` as for round_corners. The k parameter controls how fast the curvature changes and should be between 0 and 1.
|
||||
// - teardrop: os_teardrop(r|cut). Rounding using a 1/8 circle that then changes to a 45 degree chamfer. The chamfer is at the end, and enables the object to be 3d printed without support. The radius gives the radius of the circular part.
|
||||
// - chamfer: os_chamfer([height], [width], [cut], [angle]). Chamfer the edge at desired angle or with desired height and width. You can specify height and width together and the angle is ignored, or specify just one of height and width and the angle is used to determine the shape. Alternatively, specify "cut" along with angle to specify the cut back distance of the chamfer.
|
||||
// - mask: os_mask(mask, [out]). Create a profile from one of the [2d masking shapes](shapes2d.scad#section-2d-masking-shapes). The `out` parameter specifies that the mask should flare outward (like crown molding or baseboard). This is set false by default.
|
||||
// - mask: os_mask(mask, [out]). Create a profile from one of the [2d masking shapes](masks2d.scad#section-2d-masking-shapes). The `out` parameter specifies that the mask should flare outward (like crown molding or baseboard). This is set false by default.
|
||||
// .
|
||||
// The general settings that you can use with all of the helper functions are mostly used to control how offset_sweep() calls the offset() function.
|
||||
// - extra: Add an extra vertical step of the specified height, to be used for intersections or differences. This extra step extends the resulting object beyond the height you specify. It is ignored by anchoring. Default: 0
|
||||
@@ -1412,11 +1413,14 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// "chamfer". Use the "chamfer" style offset only in cases where the number of steps is small or just one (such as when using
|
||||
// the `os_chamfer` profile type).
|
||||
// .
|
||||
// The module form only can support a region as input. You can provide different profiles for the cutouts in a region using the `bottom_hole`, `top_hole`
|
||||
// or `ends_hole` parameters.
|
||||
// .
|
||||
// This module offers four anchor types. The default is "hull" in which VNF anchors are placed on the VNF of the **unrounded** object. You
|
||||
// can also use "intersect" to get the intersection anchors to the unrounded object. If you prefer anchors that respect the rounding
|
||||
// then use "surf_hull" or "intersect_hull".
|
||||
// Arguments:
|
||||
// path = 2d path (list of points) to extrude
|
||||
// path = 2d path (list of points) to extrude or a region for the module form
|
||||
// height / length / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments.
|
||||
// bottom / bot = rounding spec for the bottom end
|
||||
// top = rounding spec for the top end.
|
||||
@@ -1434,6 +1438,9 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// angle = default angle for chamfers. Default: 45
|
||||
// joint = default joint value for smooth roundover.
|
||||
// k = default curvature parameter value for "smooth" roundover
|
||||
// ends_hole = (module only) rounding spec that applies to top and bottom of holes in a region
|
||||
// bot_hole / bottom_hole = (module only) rounding spec for bottom end of holes in a region
|
||||
// top_hole = (module only) rounding spec for top end of holes in a region
|
||||
// convexity = convexity setting for use with polyhedron. (module only) Default: 10
|
||||
// anchor = Translate so anchor point is at the origin. Default: "base"
|
||||
// spin = Rotate this many degrees around Z axis after anchor. Default: 0
|
||||
@@ -1472,6 +1479,12 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// star = star(5, r=22, ir=13);
|
||||
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
|
||||
// offset_sweep(rounded_star, height=20, bottom=os_teardrop(r=4), top=os_chamfer(width=4),$fn=64);
|
||||
// Example(3D,NoAxes,VPR=[99.80,0.00,62.10],VPD=155.56,VPT=[-2.78,0.61,14.66]): Clipped circle rounding on the bottom (for 3d printability and regular circular rounding on the top, both with the same radius. The clipped circle rounding takes up less vertical space.
|
||||
// $fn=64;
|
||||
// offset_sweep(rect(50,rounding=6), h=30,bot=os_circle(r=6, clip_angle=50), top=os_circle(r=6));
|
||||
// Example(3D,NoAxes,VPR=[99.80,0.00,62.10],VPD=155.56,VPT=[-2.78,0.61,14.66]): The same as the previous example but with roundings specified by height. This means that they have different radii, but the height maches.
|
||||
// $fn=64;
|
||||
// offset_sweep(rect(50,rounding=6), h=30,bot=os_circle(h=6, clip_angle=50), top=os_circle(h=6));
|
||||
// Example: We round a cube using the continous curvature rounding profile. But note that the corners are not smooth because the curved square collapses into a square with corners. When a collapse like this occurs, we cannot turn `check_valid` off. For a better result use {{rounded_prism()}} instead.
|
||||
// square = square(1);
|
||||
// rsquare = round_corners(square, method="smooth", cut=0.1, k=0.7, $fn=36);
|
||||
@@ -1556,7 +1569,7 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// sq = [[0,0],[20,0],[20,20],[0,20]];
|
||||
// sinwave = os_profile(points=[for(theta=[0:5:720]) [4*sin(theta), theta/700*15]]);
|
||||
// offset_sweep(sq, height=20, top=sinwave, offset="delta");
|
||||
// Example: a box with a flared top. A nice roundover on the top requires a profile edge, but we can use "extra" to create a small chamfer.
|
||||
// Example(3D,NoAxes,VPR=[59.20,0.00,24.80],VPD=54.24,VPT=[-4.12,10.66,0.96]): a box with a flared top. A nice roundover on the top requires a profile edge, but we can use "extra" to create a small chamfer.
|
||||
// rhex = round_corners(hexagon(side=10), method="smooth", joint=2, $fs=0.2);
|
||||
// back_half()
|
||||
// difference(){
|
||||
@@ -1564,15 +1577,93 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
|
||||
// up(1)
|
||||
// offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4));
|
||||
// }
|
||||
// Example: Using os_mask to create ogee profiles:
|
||||
// Example(3D,NoAxes,VPR=[53.60,0.00,190.20],VPD=1036.38,VPT=[6.09,5.67,59.25]): Using os_mask to create ogee profiles:
|
||||
// ogee = mask2d_ogee([
|
||||
// "xstep",1, "ystep",1, // Starting shoulder.
|
||||
// "fillet",5, "round",5, // S-curve.
|
||||
// "ystep",1, // Ending shoulder.
|
||||
// "xstep",3, "ystep",3, // Starting shoulder.
|
||||
// "fillet",15, "round",15, // S-curve.
|
||||
// "ystep",3, // Ending shoulder.
|
||||
// ]);
|
||||
// star = star(5, r=220, ir=130);
|
||||
// rounded_star = round_corners(star, cut=flatten(repeat([5,0],5)), $fn=24);
|
||||
// offset_sweep(rounded_star, height=100, top=os_mask(ogee), bottom=os_mask(ogee,out=true));
|
||||
// offset_sweep(rounded_star, height=150, top=os_mask(ogee), bottom=os_mask(ogee,out=true));
|
||||
// Example(3D,NoAxes): Applying to a region, with different profiles for the outside in inside curves.
|
||||
// $fn = 32;
|
||||
// rgn = difference(
|
||||
// [
|
||||
// rect(50, rounding=5),
|
||||
// move([15,15], circle(d=10)),
|
||||
// move([-15,-15], circle(d=10)),
|
||||
// move([0,25], rect([10,7],anchor=BACK,
|
||||
// rounding=[-2,-2,2,2])),
|
||||
// zrot(55, square([4, 100], center=true)),
|
||||
// ellipse([12,4])
|
||||
// ]
|
||||
// );
|
||||
// offset_sweep(rgn, height=12, steps=6, ends_hole=os_chamfer(width=2),
|
||||
// ends=os_circle(r=1.7));
|
||||
|
||||
|
||||
module _offset_sweep_region(region, height,
|
||||
bottom, top,
|
||||
h, l, length, ends, bot, top_hole, bot_hole, bottom_hole, ends_hole,
|
||||
offset="round", r=0, steps=16,
|
||||
quality=1, check_valid=true,
|
||||
extra=0,
|
||||
cut=undef, chamfer_width=undef, chamfer_height=undef,
|
||||
joint=undef, k=0.75, angle=45,
|
||||
convexity=10,anchor="base",cp="centroid",
|
||||
spin=0, orient=UP, atype="hull")
|
||||
{
|
||||
connected_reg = region_parts(region);
|
||||
vnf_h_list = [for(reg=connected_reg)
|
||||
offset_sweep(path=reg[0], height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends,
|
||||
offset=offset, r=r, steps=steps,
|
||||
quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width,
|
||||
chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true)];
|
||||
vnf_list = column(vnf_h_list,0);
|
||||
height = vnf_h_list[0][1];
|
||||
|
||||
holes = [for(reg=connected_reg, i=[1:1:len(reg)-1]) reg[i]];
|
||||
|
||||
anchors = [
|
||||
named_anchor("zcenter", [0,0,0], UP),
|
||||
named_anchor("base", [0,0,-height/2], UP),
|
||||
named_anchor("top", [0,0,height/2], UP)
|
||||
];
|
||||
bottom_hole=first_defined([bottom_hole, bot_hole, ends_hole, bottom, ends]);
|
||||
top_hole = first_defined([top_hole,ends_hole,top,ends]);
|
||||
if (in_list(atype,["hull","intersect"]))
|
||||
attachable(anchor,spin,orient,region=region,h=height,cp=cp,anchors=anchors,extent=atype=="hull"){
|
||||
down(height/2)
|
||||
difference(){
|
||||
for(vnf=vnf_list)
|
||||
polyhedron(vnf[0],vnf[1],convexity=convexity);
|
||||
for(path=holes)
|
||||
offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top_hole, bottom=bottom_hole,
|
||||
offset=offset, r=r, steps=steps,
|
||||
quality=quality, check_valid=check_valid, extra=extra+0.1, cut=cut, chamfer_width=chamfer_width,
|
||||
chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _flipdir=true,convexity=convexity);
|
||||
}
|
||||
children();
|
||||
}
|
||||
else {
|
||||
allvnf=vnf_join(vnf_list);
|
||||
attachable(anchor,spin.orient,vnf=allvnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){
|
||||
difference(){
|
||||
for(vnf=vnf_list)
|
||||
vnf_polyhedron(vnf,convexity=convexity);
|
||||
for(path=holes)
|
||||
offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top_hole, bottom=bottom_hole,
|
||||
offset=offset, r=r, steps=steps,
|
||||
quality=quality, check_valid=check_valid, extra=extra+0.1, cut=cut, chamfer_width=chamfer_width,
|
||||
chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _flipdir=true,convexity=convexity);
|
||||
}
|
||||
children();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce
|
||||
@@ -1630,7 +1721,7 @@ function offset_sweep(
|
||||
extra=0, caps=true,
|
||||
cut=undef, chamfer_width=undef, chamfer_height=undef,
|
||||
joint=undef, k=0.75, angle=45, anchor="base", orient=UP, spin=0,atype="hull", cp="centroid",
|
||||
_return_height=false
|
||||
_return_height=false, _flipdir=false
|
||||
) =
|
||||
let(
|
||||
argspec = [
|
||||
@@ -1668,8 +1759,9 @@ function offset_sweep(
|
||||
)
|
||||
assert(offsetsok,"Offsets must be one of \"round\", \"delta\", or \"chamfer\"")
|
||||
let(
|
||||
offsets_bot = _rounding_offsets(bottom, -1),
|
||||
offsets_top = _rounding_offsets(top, 1),
|
||||
do_flip = _flipdir ? function(x) xflip(x) : function(x) x ,
|
||||
offsets_bot = do_flip(_rounding_offsets(bottom, -1)),
|
||||
offsets_top = do_flip(_rounding_offsets(top, 1)),
|
||||
dummy = (struct_val(top,"offset")=="chamfer" && len(offsets_top)>5)
|
||||
|| (struct_val(bottom,"offset")=="chamfer" && len(offsets_bot)>5)
|
||||
? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.")
|
||||
@@ -1722,49 +1814,75 @@ function offset_sweep(
|
||||
: reorient(anchor,spin,orient, vnf=vnf, p=vnf, extent=atype=="surf_hull", cp=cp, anchors=anchors)
|
||||
) _return_height ? [final_vnf,height] : final_vnf;
|
||||
|
||||
|
||||
module offset_sweep(path, height,
|
||||
bottom, top,
|
||||
h, l, length, ends, bot,
|
||||
offset="round", r=0, steps=16,
|
||||
quality=1, check_valid=true,
|
||||
extra=0,
|
||||
extra=0, top_hole, bot_hole, bottom_hole, ends_hole,
|
||||
cut=undef, chamfer_width=undef, chamfer_height=undef,
|
||||
joint=undef, k=0.75, angle=45,
|
||||
convexity=10,anchor="base",cp="centroid",
|
||||
spin=0, orient=UP, atype="hull")
|
||||
spin=0, orient=UP, atype="hull", _flipdir)
|
||||
{
|
||||
assert(in_list(atype, ["intersect","hull","surf_hull","surf_intersect"]), "Anchor type must be \"hull\" or \"intersect\"");
|
||||
vnf_h = offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends,
|
||||
offset=offset, r=r, steps=steps,
|
||||
quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width,
|
||||
chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true);
|
||||
vnf = vnf_h[0];
|
||||
height = vnf_h[1];
|
||||
anchors = [
|
||||
named_anchor("zcenter", [0,0,0], UP),
|
||||
named_anchor("base", [0,0,-height/2], UP),
|
||||
named_anchor("top", [0,0,height/2], UP)
|
||||
];
|
||||
if (in_list(atype,["hull","intersect"]))
|
||||
attachable(anchor,spin,orient,region=force_region(path),h=height,cp=cp,anchors=anchors,extent=atype=="hull"){
|
||||
down(height/2)polyhedron(vnf[0],vnf[1],convexity=convexity);
|
||||
children();
|
||||
}
|
||||
else
|
||||
attachable(anchor,spin.orient,vnf=vnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){
|
||||
vnf_polyhedron(vnf,convexity=convexity);
|
||||
children();
|
||||
}
|
||||
if (is_region(path) && len(path)>1)
|
||||
_offset_sweep_region(region=path, height=height, bottom=bottom, top=top, h=h, l=l, length=length, ends=ends, bot=bot,
|
||||
offset=offset, r=r, steps=steps, quality=quality, check_valid=check_valid, extra=extra,
|
||||
cut=cut, chamfer_width=chamfer_width, chamfer_height=chamfer_height, joint=joint, k=k, angle=angle,
|
||||
bot_hole=bot_hole,top_hole=top_hole,bottom_hole=bottom_hole,ends_hole=ends_hole,
|
||||
convexity=convexity, anchor=anchor, cp=cp, spin=spin, orient=orient, atype=atype) children();
|
||||
else {
|
||||
vnf_h = offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends,
|
||||
offset=offset, r=r, steps=steps,
|
||||
quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width,
|
||||
chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true, _flipdir=_flipdir);
|
||||
vnf = vnf_h[0];
|
||||
height = vnf_h[1];
|
||||
anchors = [
|
||||
named_anchor("zcenter", [0,0,0], UP),
|
||||
named_anchor("base", [0,0,-height/2], UP),
|
||||
named_anchor("top", [0,0,height/2], UP)
|
||||
];
|
||||
if (in_list(atype,["hull","intersect"]))
|
||||
attachable(anchor,spin,orient,region=force_region(path),h=height,cp=cp,anchors=anchors,extent=atype=="hull"){
|
||||
down(height/2)polyhedron(vnf[0],vnf[1],convexity=convexity);
|
||||
children();
|
||||
}
|
||||
else
|
||||
attachable(anchor,spin.orient,vnf=vnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){
|
||||
vnf_polyhedron(vnf,convexity=convexity);
|
||||
children();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function os_circle(r,cut,extra,check_valid, quality,steps, offset) =
|
||||
assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`")
|
||||
function os_circle(r,cut,h,height,clip_angle,extra,check_valid, quality,steps, offset) =
|
||||
assert(is_undef(clip_angle) || is_finite(clip_angle) && clip_angle>0 && clip_angle<=90, "clip angle must a number be in the interval (0,90]")
|
||||
let(
|
||||
h = one_defined([h,height],"h,height",dflt=undef),
|
||||
r_ang = is_def(clip_angle) ?
|
||||
assert(num_defined([r,h,cut])==1, "When clip_angle is given must give exactly one of r, h/height, or cut")
|
||||
is_def(r) ? [r,clip_angle]
|
||||
: is_def(cut) ? [cut/(sqrt(2)-1),clip_angle]
|
||||
: [h / sin(clip_angle),clip_angle]
|
||||
:
|
||||
assert(num_defined([r,cut])<=1, "Cannot give both r and cut")
|
||||
let(
|
||||
r = is_def(r) ? r
|
||||
: is_def(cut) ? cut/(sqrt(2)-1)
|
||||
: undef
|
||||
)
|
||||
is_def(r) ? [r, is_def(h) ? assert(h<=r, "height cannot be larger than radius") asin(h/r) : 90]
|
||||
: [h, 90]
|
||||
)
|
||||
_remove_undefined_vals([
|
||||
"for", "offset_sweep",
|
||||
"type", "circle",
|
||||
"r",r,
|
||||
"cut",cut,
|
||||
"r",r_ang[0],
|
||||
"angle",r_ang[1],
|
||||
"extra",extra,
|
||||
"check_valid",check_valid,
|
||||
"quality", quality,
|
||||
@@ -1772,6 +1890,7 @@ function os_circle(r,cut,extra,check_valid, quality,steps, offset) =
|
||||
"offset", offset
|
||||
]);
|
||||
|
||||
|
||||
function os_teardrop(r,cut,extra,check_valid, quality,steps, offset) =
|
||||
assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`")
|
||||
_remove_undefined_vals([
|
||||
@@ -1874,7 +1993,7 @@ function os_mask(mask, out=false, extra,check_valid, quality, offset) =
|
||||
// .
|
||||
// - profile: os_profile(points)
|
||||
// Define the offset profile with a list of points. The first point must be [0,0] and the roundover should rise in the positive y direction, with positive x values for inward motion (standard roundover) and negative x values for flaring outward. If the y value ever decreases then you might create a self-intersecting polyhedron, which is invalid. Such invalid polyhedra create cryptic assertion errors when you render your model and it is your responsibility to avoid creating them. Note that the starting point of the profile is the center of the extrusion. If you use a profile as the top, it rises upward. If you use it as the bottom, it is inverted and goes downward.
|
||||
// - circle: os_circle(r|cut). Define circular rounding either by specifying the radius or cut distance.
|
||||
// - circle: os_circle(r|cut=,height=|h=,[clip_angle=],). Define circular rounding or clipped circle rounding. You specify a full circle rounding by giving the radius, cut distance or height (which is equivalent to radius in this case). For a clipped circle rounding you can use two methods. You can specify the clip angle and then give a radius, cut, or height. (The cut distance in this case is the usual cut for a full circular arc.) Alternatively you can give a height and radius (or cut) and the appropriate clip angle is chosen for you.
|
||||
// - smooth: os_smooth(cut|joint, [k]). Define continuous curvature rounding, with `cut` and `joint` as for round_corners. The k parameter controls how fast the curvature changes and should be between 0 and 1.
|
||||
// - teardrop: os_teardrop(r|cut). Rounding using a 1/8 circle that then changes to a 45 degree chamfer. The chamfer is at the end, and enables the object to be 3d printed without support. The radius gives the radius of the circular part.
|
||||
// - chamfer: os_chamfer([height], [width], [cut], [angle]). Chamfer the edge at desired angle or with desired height and width. You can specify height and width together and the angle is ignored, or specify just one of height and width and the angle is used to determine the shape. Alternatively, specify "cut" along with angle to specify the cut back distance of the chamfer.
|
||||
|
755
scripts/img2scad.html
Normal file
755
scripts/img2scad.html
Normal file
@@ -0,0 +1,755 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!--
|
||||
Standalone web app to convert an image file to an OpenSCAD array, for use with BOSL2 textures.
|
||||
Versions 1-5: 22 April 2025 - by Alex Matulich (collaborating with ChatGPT for crop panel CSS, file loading and saving, and Gaussian blur)
|
||||
Version 6: 23 April 2025 - added cropping UI
|
||||
Version 7: 25 April 2025 - added contrast and threshold sliders
|
||||
Version 8: 26 April 2025 - added file size estimate to output section
|
||||
-->
|
||||
<title>Image to OpenSCAD array, v8</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: sans-serif; padding-left:1em; padding-right:1em;}
|
||||
h1,h2,h3,h4 { font-family: serif; }
|
||||
fieldset {
|
||||
border: 2px ridge silver;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
min-width: 300px;
|
||||
}
|
||||
legend {
|
||||
font-weight: bold;
|
||||
font-family: Serif;
|
||||
font-size: larger;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.slider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.slider-label {
|
||||
width: 9ch;
|
||||
}
|
||||
.slider-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
min-width: 0;
|
||||
}
|
||||
.slider-value {
|
||||
width: 4ch;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.uiContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
#inputArea {
|
||||
background-color: #FFFFBB;
|
||||
border: 6px outset #DDDD99;
|
||||
padding: 1em;
|
||||
}
|
||||
#outputArea {
|
||||
background-color: #EEFFEE;
|
||||
border: 6px outset #BBDDBB;
|
||||
padding: 1em;
|
||||
}
|
||||
.canvasWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
font-size: small;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
|
||||
/* Position the tooltip */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* cropping control panel stuff */
|
||||
|
||||
.crop-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"top top top"
|
||||
"left center right"
|
||||
"bottom bottom bottom";
|
||||
grid-template-columns: auto 60px auto;
|
||||
grid-template-rows: auto 60px auto;
|
||||
gap: 4px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.crop-center {
|
||||
grid-area: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px dashed #ccc;
|
||||
background-color: #eee;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.crop-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.crop-control input[type="number"] {
|
||||
width: 6ch;
|
||||
padding: 2px;
|
||||
font-size: 1rem;
|
||||
text-align: right;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.crop-top { grid-area: top; }
|
||||
.crop-left { grid-area: left; }
|
||||
.crop-right { grid-area: right; }
|
||||
.crop-bottom {
|
||||
grid-area: bottom;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Convert image to OpenSCAD array</h1>
|
||||
<p>This utility accepts an image that can be displayed in your browser, and converts it to grayscale
|
||||
expanded to use the maximum possible luminance range. The file types supported depend on your browser.
|
||||
Alpha channel is ignored. After processing the image as desired, you may save it as an OpenSCAD array.</p>
|
||||
<p>Keep the output image width small! A large size results in a huge output file when converting an image to text data.</p>
|
||||
<hr>
|
||||
<div id="content">
|
||||
<div class="uiContainer" id="inputArea" tabindex="0">
|
||||
<fieldset>
|
||||
<legend>Select an image</legend>
|
||||
<input type="file" id="imageInput" accept="image/*">
|
||||
<p><em>You can also paste an image (Ctrl+V) into this section from your clipboard.</em></p>
|
||||
</fieldset>
|
||||
<!-- Original image canvas -->
|
||||
<div class="canvasWrapper">
|
||||
<p id="originalSize"></p>
|
||||
<canvas id="originalCanvas" width="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uiContainer" id="outputArea">
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Transformations</legend>
|
||||
<label for="resizeWidth">Rescale original width (px):</label>
|
||||
<input type="number" id="resizeWidth" size="5" min="1" max="9000" value="100"><br>
|
||||
<button id="rotateLeft">⟲ Rotate left</button>
|
||||
<button id="rotateRight">⟳ Rotate right</button><br>
|
||||
<button id="flipHorizontal">⇋ Flip horizontal</button>
|
||||
<button id="flipVertical">⇵ Flip vertical</button>
|
||||
|
||||
<div class="crop-container">
|
||||
<div class="crop-control crop-top">
|
||||
<label for="crop-top">Top</label>
|
||||
<input type="number" id="cropTop" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-control crop-left">
|
||||
<label for="crop-left">Left</label>
|
||||
<input type="number" id="cropLeft" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-center">Crop</div>
|
||||
<div class="crop-control crop-right">
|
||||
<label for="crop-right">Right</label>
|
||||
<input type="number" id="cropRight" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-control crop-bottom">
|
||||
<input type="number" id="cropBottom" min="0" max="9999" value="0">
|
||||
<label for="crop-bottom">Bottom</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Appearance</legend>
|
||||
<input type="radio" name="grayModel" value="ntsc" checked><label for "grayModel" class="tooltip"> NTSC grayscale formula
|
||||
<span class="tooltiptext">0.299R + 0.587G + 0.114B<br>Based on average human perception of color luminance</span></label><br>
|
||||
<input type="radio" name="grayModel" value="linear"><label for="grayModel" class="tooltip"> Linear luminance
|
||||
<span class="tooltiptext">0.2126R + 0.7152G + 0.0722B<br>Used by OpenSCAD surface()</span></label>
|
||||
|
||||
<div style="margin-top:8px;">
|
||||
<label><input type="checkbox" id="invertBrightness"> Invert brightness</label>
|
||||
</div>
|
||||
<div style="margin:8px 0;">
|
||||
<label for="blurRadius">Gaussian blur radius (pixels):</label>
|
||||
<input type="number" id="blurRadius" size="5" min="0" max="20" value="0">
|
||||
</div>
|
||||
|
||||
<div class="slider-row">
|
||||
<label for="contrast" class="slider-label tooltip">Contrast
|
||||
<span class="tooltiptext">Compress brightness above and below threshold<br>to maximum and minimum brightness.</span></label>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="contrast" min="0" max="100" value="0">
|
||||
<span id="contrastValue" class="slider-value">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-row">
|
||||
<label for="threshold" class="slider-label tooltip">Threshold
|
||||
<span class="tooltiptext">Level between black (-128) and white (127)<br>around which to adjust contrast.</span></label>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="threshold" min="-128" max="127" value="0">
|
||||
<span id="thresholdValue" class="slider-value">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Output</legend>
|
||||
<label><input type="checkbox" id="normalizeToUnit" checked> Normalize range to [0,1] — uses [0,255] if unset</label>
|
||||
<div style="margin-top:8px;">
|
||||
<label for="arrayName">Name of array:</label>
|
||||
<input type="text" id="arrayName" value="image_array" onkeypress="return event.charCode != 32">
|
||||
<div style="margin-top:8px;">
|
||||
<button id="downloadButton">Save as OpenSCAD array</button> ≈ <strong><span id="kbytes">0 bytes</span></strong>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- Grayscale output image canvas -->
|
||||
<div class="canvasWrapper">
|
||||
<p id="grayscaleSize"></p>
|
||||
<div id="outcontainer">
|
||||
<canvas id="grayscaleCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<script>
|
||||
// get page element handles
|
||||
|
||||
const imageInput = document.getElementById('imageInput');
|
||||
const downloadButton = document.getElementById('downloadButton');
|
||||
const resizeWidthInput = document.getElementById('resizeWidth');
|
||||
const originalSizeText = document.getElementById('originalSize');
|
||||
const grayscaleSizeText = document.getElementById('grayscaleSize');
|
||||
const invertBrightnessCheckbox = document.getElementById('invertBrightness');
|
||||
const normalizeToUnitCheckbox = document.getElementById('normalizeToUnit');
|
||||
const rotateLeftBtn = document.getElementById('rotateLeft');
|
||||
const rotateRightBtn = document.getElementById('rotateRight');
|
||||
const flipHorizontalBtn = document.getElementById('flipHorizontal');
|
||||
const flipVerticalBtn = document.getElementById('flipVertical');
|
||||
const cropTop = document.getElementById('cropTop');
|
||||
const cropLeft = document.getElementById('cropLeft');
|
||||
const cropRight = document.getElementById('cropRight');
|
||||
const cropBottom = document.getElementById('cropBottom');
|
||||
const blurRadiusInput = document.getElementById('blurRadius');
|
||||
const contrastInput = document.getElementById('contrast');
|
||||
const contrastValue = document.getElementById('contrastValue');
|
||||
const thresholdInput = document.getElementById('threshold');
|
||||
const thresholdValue = document.getElementById('thresholdValue');
|
||||
const arrayName = document.getElementById('arrayName');
|
||||
const inputArea = document.getElementById('inputArea');
|
||||
const originalCanvas = document.getElementById('originalCanvas');
|
||||
const grayscaleCanvas = document.getElementById('grayscaleCanvas');
|
||||
const kbytes = document.getElementById('kbytes');
|
||||
|
||||
// other initializations
|
||||
|
||||
const originalCtx = originalCanvas.getContext('2d');
|
||||
const grayscaleCtx = grayscaleCanvas.getContext('2d');
|
||||
|
||||
const cropID = [ cropRight, cropTop, cropLeft, cropBottom ]; // counterclockwise from right
|
||||
let edgeID = [ 0, 1, 2 ,3 ]; // counterclockwise from right: right, top, left, bottom
|
||||
const edgeconfig = [
|
||||
// IDs of crop gadgets corresponding to image edges, from right edge counterclockwise,
|
||||
// in all combinations of rotations and flips.
|
||||
// no flip flipH flipV flipV+H
|
||||
/* 0*/ [[0,1,2,3], [2,1,0,3], [0,3,2,1], [2,3,0,1]],
|
||||
/* 90*/ [[3,0,1,2], [1,0,3,2], [3,2,1,0], [1,2,3,0]],
|
||||
/*180*/ [[2,3,0,1], [0,3,2,1], [2,1,0,3], [0,1,2,3]],
|
||||
/*270*/ [[1,2,3,0], [3,2,1,0], [1,0,3,2], [3,0,1,2]]
|
||||
];
|
||||
let grayscaleMatrix = [];
|
||||
let currentImage = new Image();
|
||||
let rotation = 0;
|
||||
let flipH = false;
|
||||
let flipV = false;
|
||||
let fileSuffix = "";
|
||||
let origDim = { width:0, height:0 };
|
||||
let uncropDim = { width:0, height:0 };
|
||||
let cropDim = { width:0, height:0 };
|
||||
let invertBrightness = false;
|
||||
let contrast = 0.0001; // ranges from 0.0001 to 100.0001
|
||||
let threshold = 128.0/255.0; // ranges from 0. to 1.0
|
||||
|
||||
// image processing functions
|
||||
|
||||
function applyGaussianBlur(matrix, radius) {
|
||||
if (radius <= 0) return matrix;
|
||||
const sigma = radius > 0 ? radius / 3 : 1;
|
||||
const kernel = [];
|
||||
let sum = 0;
|
||||
for (let i = -radius; i <= radius; i++) { // kernel size = 2 * radius + 1;
|
||||
const value = Math.exp(-(i * i) / (2 * sigma * sigma));
|
||||
kernel.push(value);
|
||||
sum += value;
|
||||
}
|
||||
kernel.forEach((v, i) => kernel[i] = v / sum);
|
||||
|
||||
const width = matrix[0].length;
|
||||
const height = matrix.length;
|
||||
const horizontalBlur = [];
|
||||
// blur pixels horizontally, put in horizontalBlur[]
|
||||
for (let y = 0; y < height; y++) {
|
||||
horizontalBlur[y] = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
let val = 0;
|
||||
let weightSum = 0;
|
||||
for (let k = -radius; k <= radius; k++) {
|
||||
const nx = x + k;
|
||||
if (nx >= 0 && nx < width) {
|
||||
val += matrix[y][nx] * kernel[k + radius];
|
||||
weightSum += kernel[k + radius];
|
||||
}
|
||||
}
|
||||
horizontalBlur[y][x] = val / weightSum;
|
||||
}
|
||||
}
|
||||
// blur pixels vertically in horizontalBlur[], return result in output[]
|
||||
const output = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
output[y] = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
let val = 0;
|
||||
let weightSum = 0;
|
||||
for (let k = -radius; k <= radius; k++) {
|
||||
const ny = y + k;
|
||||
if (ny >= 0 && ny < height) {
|
||||
val += horizontalBlur[ny][x] * kernel[k + radius];
|
||||
weightSum += kernel[k + radius];
|
||||
}
|
||||
}
|
||||
output[y][x] = val / weightSum;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function sigmoid(z) { return 1.0 / (1+Math.exp(-z)); } // used by contrastAdj
|
||||
|
||||
function contrastAdj(brightness) { // return an adjusted brightness based on contrast and threshold
|
||||
const x = brightness/255.0;
|
||||
const c = 2.0*contrast; // attempt to balance the sigmoid response to the contrast control
|
||||
const sigterm = sigmoid(-c*threshold);
|
||||
const adj = contrast>100.0 ? (x<threshold ? 0 : x>threshold ? 1 : threshold) // jump to 100% contrast at max contrast
|
||||
: (sigmoid(c*(x-threshold)) - sigterm) / (sigmoid(c*(1.0-threshold)) - sigterm);
|
||||
return adj * 255.0;
|
||||
}
|
||||
|
||||
function processImage() {
|
||||
if (!currentImage.src) return;
|
||||
|
||||
origDim.width = currentImage.naturalWidth;
|
||||
origDim.height = currentImage.naturalHeight;
|
||||
|
||||
// display thumbnail original image
|
||||
const thumbWidth = 200;
|
||||
const thumbHeight = Math.round((origDim.height / origDim.width) * thumbWidth);
|
||||
originalCanvas.width = thumbWidth;
|
||||
originalCanvas.height = thumbHeight;
|
||||
originalCtx.clearRect(0, 0, thumbWidth, thumbHeight);
|
||||
originalCtx.drawImage(currentImage, 0, 0, thumbWidth, thumbHeight);
|
||||
originalSizeText.textContent = `Original size: ${origDim.width}×${origDim.height}`;
|
||||
|
||||
// get output image dimensions
|
||||
uncropDim.width = origDim.width;
|
||||
uncropDim.height = origDim.height;
|
||||
const newWidth = parseInt(resizeWidthInput.value);
|
||||
if (!isNaN(newWidth) && newWidth > 0) {
|
||||
uncropDim.width = newWidth;
|
||||
uncropDim.height = Math.round(newWidth * origDim.height / origDim.width);
|
||||
}
|
||||
|
||||
// put original image in a temporary canvas with output dimensions and get image data
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = uncropDim.width;
|
||||
tempCanvas.height = uncropDim.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCtx.drawImage(currentImage, 0, 0, uncropDim.width, uncropDim.height);
|
||||
const imgData = tempCtx.getImageData(0, 0, uncropDim.width, uncropDim.height);
|
||||
const data = imgData.data;
|
||||
|
||||
// convert image data to grayscale
|
||||
const brightnessMatrix = [];
|
||||
const model = document.querySelector('input[name="grayModel"]:checked').value;
|
||||
const weights = model === 'linear' ? [0.2126, 0.7152, 0.0722] : [0.299, 0.587, 0.114];
|
||||
for (let y = 0; y < uncropDim.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < uncropDim.width; x++) {
|
||||
const i = (y * uncropDim.width + x) * 4;
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
let brightness = weights[0] * r + weights[1] * g + weights[2] * b;
|
||||
row.push(brightness);
|
||||
}
|
||||
brightnessMatrix.push(row);
|
||||
}
|
||||
|
||||
// apply blurring to the grayscale image
|
||||
const blurRadius = parseInt(blurRadiusInput.value) || 0;
|
||||
const blurredMatrix = applyGaussianBlur(brightnessMatrix, blurRadius);
|
||||
|
||||
// crop the blurred matrix, gather min and max values in crop area
|
||||
const cropMatrix = [];
|
||||
let cropx1 = parseInt(cropID[edgeID[2]].value) || 0;
|
||||
let cropx2 = parseInt(cropID[edgeID[0]].value) || 0;
|
||||
let cropy1 = parseInt(cropID[edgeID[1]].value) || 0;
|
||||
let cropy2 = parseInt(cropID[edgeID[3]].value) || 0;
|
||||
let min = 255;
|
||||
let max = 0;
|
||||
for (let y=cropy1; y<uncropDim.height-cropy2; y++) {
|
||||
const row = [];
|
||||
for(let x=cropx1; x<uncropDim.width-cropx2; x++) {
|
||||
row.push(blurredMatrix[y][x]);
|
||||
min = Math.min(min, blurredMatrix[y][x]);
|
||||
max = Math.max(max, blurredMatrix[y][x]);
|
||||
}
|
||||
cropMatrix.push(row);
|
||||
}
|
||||
cropDim.width = uncropDim.width - cropx1 - cropx2;
|
||||
cropDim.height = uncropDim.height - cropy1 - cropy2;
|
||||
|
||||
// normalize cropped image brightness to 0-255 range, invert brightness if checkbox is selected
|
||||
// adjust contrast if needed
|
||||
const range = max - min || 1;
|
||||
grayscaleMatrix = [];
|
||||
const grayImgData = grayscaleCtx.createImageData(cropDim.width, cropDim.height);
|
||||
const grayData = grayImgData.data;
|
||||
for (let y = 0; y < cropDim.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < cropDim.width; x++) {
|
||||
let brightness = cropMatrix[y][x];
|
||||
brightness = ((brightness - min) / range) * 255;
|
||||
if (contrast>0.0002) // adjust contrast if contrast control > 0
|
||||
brightness = contrastAdj(brightness);
|
||||
if (invertBrightness)
|
||||
brightness = 255 - brightness;
|
||||
brightness = Math.max(0, Math.min(255, Math.round(brightness)));
|
||||
const i = (y * cropDim.width + x) * 4;
|
||||
grayData[i] = grayData[i+1] = grayData[i+2] = brightness;
|
||||
grayData[i + 3] = 255;
|
||||
row.push(brightness);
|
||||
}
|
||||
grayscaleMatrix.push(row);
|
||||
}
|
||||
|
||||
// rotate and flip image
|
||||
const rotated = (rotation % 180 !== 0);
|
||||
const finalWidth = rotated ? cropDim.height : cropDim.width;
|
||||
const finalHeight = rotated ? cropDim.width : cropDim.height;
|
||||
grayscaleCanvas.width = finalWidth;
|
||||
grayscaleCanvas.height = finalHeight;
|
||||
|
||||
const tempDrawCanvas = document.createElement('canvas');
|
||||
tempDrawCanvas.width = cropDim.width;
|
||||
tempDrawCanvas.height = cropDim.height;
|
||||
const tempDrawCtx = tempDrawCanvas.getContext('2d');
|
||||
tempDrawCtx.putImageData(grayImgData, 0, 0);
|
||||
|
||||
grayscaleCtx.save();
|
||||
grayscaleCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
grayscaleCtx.clearRect(0, 0, finalWidth, finalHeight);
|
||||
grayscaleCtx.translate(finalWidth / 2, finalHeight / 2);
|
||||
grayscaleCtx.rotate(rotation * Math.PI / 180);
|
||||
grayscaleCtx.scale(flipH ? -1 : 1, flipV ? -1 : 1);
|
||||
grayscaleCtx.drawImage(tempDrawCanvas, -cropDim.width / 2, -cropDim.height / 2);
|
||||
grayscaleCtx.restore();
|
||||
|
||||
grayscaleSizeText.textContent = `Output size: ${finalWidth}×${finalHeight}`;
|
||||
fileSuffix = finalWidth.toString()+"x"+finalHeight.toString();
|
||||
updateKbytes();
|
||||
}
|
||||
|
||||
// image loading functions
|
||||
|
||||
function resetInputs() { // executed after an image loads
|
||||
cropLeft.value="0";
|
||||
cropRight.value="0";
|
||||
cropTop.value="0";
|
||||
cropBottom.value="0";
|
||||
rotation = 0;
|
||||
flipV = flipH = false;
|
||||
resizeWidthInput.value = "100";
|
||||
blurRadiusInput.value = "0";
|
||||
invertBrightnessCheckbox.checked = invertBrightness = false;
|
||||
contrastInput.value = contrastValue.textContent = "0";
|
||||
contrast = 0.0001;
|
||||
thresholdInput.value = thresholdValue.textContent = "0";
|
||||
threshold = 128.0/255.0;
|
||||
}
|
||||
|
||||
// user pressed button to load image from disk
|
||||
imageInput.addEventListener('change', function () {
|
||||
const file = this.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
currentImage.onload = function () {
|
||||
resetInputs();
|
||||
processImage();
|
||||
};
|
||||
currentImage.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// user pasted an image from the clipboard into the input area
|
||||
inputArea.addEventListener('paste', function (event) {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
for (const item of items) {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const blob = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
currentImage.onload = function () {
|
||||
resetInputs();
|
||||
processImage();
|
||||
};
|
||||
currentImage.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// set up event listeners for all the input gadgets
|
||||
|
||||
[blurRadiusInput, contrastInput, thresholdInput,
|
||||
...document.querySelectorAll('input[name="grayModel"]')].forEach(el => el.addEventListener('input', processImage));
|
||||
|
||||
resizeWidthInput.addEventListener('input', function () {
|
||||
let min = parseInt(this.min);
|
||||
if (parseInt(this.value) < min) this.value = min;
|
||||
processImage();
|
||||
});
|
||||
|
||||
cropLeft.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropLeft.value="0"; return; }
|
||||
const cl = parseInt(cropLeft.value) || 0;
|
||||
const cr = parseInt(cropRight.value) || 0;
|
||||
const newcl = uncropDim.width - cl - cr < 2 ? uncropDim.width - cr - 2 : cl;
|
||||
cropLeft.value = newcl.toString();
|
||||
resizeWidthInput.min = newcl + cr + 2;
|
||||
processImage();
|
||||
});
|
||||
cropTop.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropTop.value="0"; return; }
|
||||
const ct = parseInt(cropTop.value) || 0;
|
||||
const cb = parseInt(cropBottom.value) || 0;
|
||||
if(uncropDim.width - ct - cb < 2) cropTop.value = (uncropDim.height - cb - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
cropRight.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropRight.value="0"; return; }
|
||||
const cl = parseInt(cropLeft.value) || 0;
|
||||
const cr = parseInt(cropRight.value) || 0;
|
||||
const newcr = uncropDim.width - cl - cr < 2 ? uncropDim.width - cl - 2 : cr;
|
||||
cropRight.value = newcr.toString();
|
||||
resizeWidthInput.min = cl + newcr + 2;
|
||||
processImage();
|
||||
});
|
||||
cropBottom.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropBottom.value="0"; return; }
|
||||
const ct = parseInt(cropTop.value) || 0;
|
||||
const cb = parseInt(cropBottom.value) || 0;
|
||||
if(uncropDim.width - ct - cb < 2) cropBottom.value = (uncropDim.height - ct - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
|
||||
function updateEdgeID(out="") {
|
||||
const fi = (flipH ? 1 : 0) + (flipV ? 2 : 0);
|
||||
const ri = Math.round(rotation/90);
|
||||
edgeID = edgeconfig[ri][fi];
|
||||
if (out.length>0) console.log(out, rotation, flipH, flipV, edgeID);
|
||||
}
|
||||
|
||||
rotateLeftBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
rotation = (rotation - 90 + 360) % 360;
|
||||
const tmp = cropTop.value;
|
||||
cropTop.value = cropRight.value;
|
||||
cropRight.value = cropBottom.value;
|
||||
cropBottom.value = cropLeft.value;
|
||||
cropLeft.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
rotateRightBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
rotation = (rotation + 90) % 360;
|
||||
const tmp = cropTop.value;
|
||||
cropTop.value = cropLeft.value;
|
||||
cropLeft.value = cropBottom.value;
|
||||
cropBottom.value = cropRight.value;
|
||||
cropRight.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
flipHorizontalBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
flipH = !flipH;
|
||||
let tmp = cropRight.value;
|
||||
cropRight.value = cropLeft.value;
|
||||
cropLeft.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
flipVerticalBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
flipV = !flipV;
|
||||
let tmp = cropTop.value;
|
||||
cropTop.value = cropBottom.value;
|
||||
cropBottom.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
|
||||
invertBrightnessCheckbox.addEventListener('input', () => {
|
||||
if (invertBrightness != invertBrightnessCheckbox.checked) {
|
||||
const t = Math.min(127, -parseInt(thresholdInput.value));
|
||||
threshold = (128.0+t)/255.0;
|
||||
thresholdInput.value = thresholdValue.textContent = t.toString();
|
||||
}
|
||||
invertBrightness = invertBrightnessCheckbox.checked;
|
||||
processImage();
|
||||
});
|
||||
|
||||
contrastInput.addEventListener('input', function() {
|
||||
contrastValue.textContent = this.value;
|
||||
const c = parseFloat(this.value);
|
||||
contrast = c*c/100.0 + 0.0001;
|
||||
processImage();
|
||||
});
|
||||
thresholdInput.addEventListener('input', function() {
|
||||
thresholdValue.textContent = this.value;
|
||||
threshold = (parseFloat(this.value) + 128.0) / 255.0;
|
||||
processImage();
|
||||
});
|
||||
|
||||
const Gbyte = 1073741824.0;
|
||||
const Mbyte = 1048576.0;
|
||||
const Kbyte = 1024.0;
|
||||
// update file size estimate based on normalize type and size of output image
|
||||
function updateKbytes() {
|
||||
// length of a number for [0,1] range: mostly 6 characters "0.xxx," but occasionally less, using 5.95.
|
||||
// length of a number for [0,255] range: assume 0-255 are uniformly distributed, use weighted average of digits plus comma
|
||||
const avglen = normalizeToUnitCheckbox.checked ? 5.95 : (10.0+90.0*2.0+156.0*3.0)/256.0+1.0;
|
||||
// each row has 6 extra characters " [],\r\n" at most, plus 5 characters after array name and 4 characters at the end
|
||||
const estsize = (avglen*cropDim.width + 6.0) * cropDim.height + 9 + arrayName.value.length;
|
||||
let unitName = "bytes";
|
||||
let unit = 1.0;
|
||||
if (estsize > Gbyte) { unit = Gbyte; unitName = "GiB"; }
|
||||
else if (estsize > Mbyte) { unit = Mbyte; unitName = "MiB"; }
|
||||
else if (estsize > 10.0*Kbyte) { unit = Kbyte; unitName = "KiB"; }
|
||||
const sizeOut = (estsize/unit).toFixed(unit==1.0?0:1);
|
||||
kbytes.textContent = `${sizeOut} ${unitName}`;
|
||||
}
|
||||
|
||||
normalizeToUnitCheckbox.addEventListener('input', () => {
|
||||
updateKbytes();
|
||||
});
|
||||
|
||||
// file output functions
|
||||
|
||||
// try to use "Save As" file picker,
|
||||
// fall back to saving with a default name to browser's downloads directory.
|
||||
|
||||
downloadButton.addEventListener('click', () => {
|
||||
if (grayscaleMatrix.length === 0) return alert("No data to save.");
|
||||
const useUnit = normalizeToUnitCheckbox.checked;
|
||||
const arrayContent = grayscaleMatrix.map(row => {
|
||||
return " [" + row.map(val => useUnit ? parseFloat((val/255.0).toFixed(3)) : val).join(",") + "]";
|
||||
}).join(",\n");
|
||||
const openscadArray = (arrayName.value.length>0 ? arrayName.value : 'image_array')+" = [\n" + arrayContent + "\n];";
|
||||
const blob = new Blob([openscadArray], { type: "text/plain" });
|
||||
let suffix = fileSuffix.length>0 ? (arrayName.value.length>0 ? fileSuffix : "image"+fileSuffix) : "image";
|
||||
let filename = arrayName.value.length>0 ? arrayName.value+'_'+suffix+'.scad' : suffix+'.scad';
|
||||
if (window.showSaveFilePicker) {
|
||||
saveWithFilePicker(blob, filename);
|
||||
} else {
|
||||
fallbackSave(blob, filename);
|
||||
}
|
||||
});
|
||||
|
||||
async function saveWithFilePicker(blob, filename) {
|
||||
try {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [{ description: 'OpenSCAD Data File', accept: { 'text/plain': ['.scad'] } }]
|
||||
});
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch (err) {
|
||||
alert('Save cancelled or failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackSave(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -6,24 +6,60 @@ import sys
|
||||
import os.path
|
||||
import argparse
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageFilter, ImageOps
|
||||
|
||||
|
||||
def img2scad(filename, varname, resize, outf):
|
||||
def img2tex(filename, opts, outf):
|
||||
indent = " " * 4
|
||||
im = Image.open(filename).convert('L')
|
||||
if resize:
|
||||
print("Resizing to {}x{}".format(resize[0],resize[1]))
|
||||
im = im.resize(resize)
|
||||
if opts.resize:
|
||||
print("Resizing to {}x{}".format(opts.resize[0], opts.resize[1]))
|
||||
im = im.resize(opts.resize)
|
||||
if opts.invert:
|
||||
print("Inverting luminance.")
|
||||
im = ImageOps.invert(im)
|
||||
if opts.blur:
|
||||
print("Blurring, radius={}.".format(opts.blur))
|
||||
im = im.filter(ImageFilter.BoxBlur(opts.blur))
|
||||
if opts.rotate:
|
||||
if opts.rotate in (-90, 270):
|
||||
print("Rotating 90 degrees clockwise.".format(opts.rotate))
|
||||
elif opts.rotate in (90, -270):
|
||||
print("Rotating 90 degrees counter-clockwise.".format(opts.rotate))
|
||||
elif opts.rotate in (180, -180):
|
||||
print("Rotating 180 degrees.".format(opts.rotate))
|
||||
im = im.rotate(opts.rotate, expand=True)
|
||||
if opts.mirror_x:
|
||||
print("Mirroring left-to-right.")
|
||||
im = im.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
if opts.mirror_y:
|
||||
print("Mirroring top-to-bottom.")
|
||||
im = im.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
pix = im.load()
|
||||
width, height = im.size
|
||||
print("// Image {} ({}x{})".format(filename, width, height), file=outf)
|
||||
print("{} = [".format(varname), file=outf)
|
||||
line = indent
|
||||
for x in range(width):
|
||||
line += "[ "
|
||||
|
||||
if opts.range == "dynamic":
|
||||
pixmin = 255;
|
||||
pixmax = 0;
|
||||
for y in range(height):
|
||||
line += "{:d}, ".format(pix[x,y])
|
||||
for x in range(width):
|
||||
pixmin = min(pixmin, pix[x,y])
|
||||
pixmax = max(pixmax, pix[x,y])
|
||||
else:
|
||||
pixmin = 0;
|
||||
pixmax = 255;
|
||||
print("// Original luminances: min={}, max={}".format(pixmin, pixmax), file=outf)
|
||||
print("// Texture heights: min={}, max={}".format(opts.minout, opts.maxout), file=outf)
|
||||
|
||||
print("{} = [".format(opts.varname), file=outf)
|
||||
line = indent
|
||||
for y in range(height):
|
||||
line += "[ "
|
||||
for x in range(width):
|
||||
u = (pix[x,y] - pixmin) / (pixmax - pixmin)
|
||||
val = u * (opts.maxout - opts.minout) + opts.minout
|
||||
line += "{:.3f}".format(val).rstrip('0').rstrip('.') + ", "
|
||||
if len(line) > 60:
|
||||
print(line, file=outf)
|
||||
line = indent * 2
|
||||
@@ -35,14 +71,38 @@ def img2scad(filename, varname, resize, outf):
|
||||
print("", file=outf)
|
||||
|
||||
|
||||
def check_nonneg_float(value):
|
||||
val = float(value)
|
||||
if val < 0:
|
||||
raise argparse.ArgumentTypeError("{} is an invalid non-negative float value".format(val))
|
||||
return val
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='img2scad')
|
||||
parser.add_argument('-o', '--outfile',
|
||||
help='Output .scad file.')
|
||||
parser.add_argument('-v', '--varname',
|
||||
help='Variable to use in .scad file.')
|
||||
parser.add_argument('-i', '--invert', action='store_true',
|
||||
help='Invert luminance values.')
|
||||
parser.add_argument('-r', '--resize',
|
||||
help='Resample image to WIDTHxHEIGHT.')
|
||||
parser.add_argument('-R', '--rotate', choices=(-270, -180, -90, 0, 90, 180, 270), default=0, type=int,
|
||||
help='Rotate output by the given number of degrees.')
|
||||
parser.add_argument('--mirror-x', action="store_true",
|
||||
help='Mirror output in the X direction.')
|
||||
parser.add_argument('--mirror-y', action="store_true",
|
||||
help='Mirror output in the Y direction.')
|
||||
parser.add_argument('--blur', type=check_nonneg_float, default=0,
|
||||
help='Perform a box blur on the output with the given radius.')
|
||||
parser.add_argument('--minout', type=float, default=0.0,
|
||||
help='The value to output for the minimum luminance.')
|
||||
parser.add_argument('--maxout', type=float, default=1.0,
|
||||
help='The value to output for the maximum luminance.')
|
||||
parser.add_argument('--range', choices=["dynamic", "full"], default="dynamic",
|
||||
help='If "dynamic", the lowest to brightest luminances are scaled to the minout/maxout range.\n'
|
||||
'If "full", 0 to 255 luminances will be scaled to the minout/maxout range.')
|
||||
parser.add_argument('infile', help='Input image file.')
|
||||
opts = parser.parse_args()
|
||||
|
||||
@@ -54,6 +114,9 @@ def main():
|
||||
else:
|
||||
opts.varname = "image_data"
|
||||
size_pat = re.compile(r'^([0-9][0-9]*)x([0-9][0-9]*)$')
|
||||
|
||||
opts.invert = bool(opts.invert)
|
||||
|
||||
if opts.resize:
|
||||
m = size_pat.match(opts.resize)
|
||||
if not m:
|
||||
@@ -67,9 +130,9 @@ def main():
|
||||
|
||||
if opts.outfile:
|
||||
with open(opts.outfile, "w") as outf:
|
||||
img2scad(opts.infile, opts.varname, opts.resize, outf)
|
||||
img2tex(opts.infile, opts, outf)
|
||||
else:
|
||||
img2scad(opts.infile, opts.varname, opts.resize, sys.stdout)
|
||||
img2tex(opts.infile, opts, sys.stdout)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
@@ -1,142 +0,0 @@
|
||||
#!env python3
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
import argparse
|
||||
|
||||
from PIL import Image, ImageFilter, ImageOps
|
||||
|
||||
|
||||
def img2tex(filename, opts, outf):
|
||||
indent = " " * 4
|
||||
im = Image.open(filename).convert('L')
|
||||
if opts.resize:
|
||||
print("Resizing to {}x{}".format(opts.resize[0], opts.resize[1]))
|
||||
im = im.resize(opts.resize)
|
||||
if opts.invert:
|
||||
print("Inverting luminance.")
|
||||
im = ImageOps.invert(im)
|
||||
if opts.blur:
|
||||
print("Blurring, radius={}.".format(opts.blur))
|
||||
im = im.filter(ImageFilter.BoxBlur(opts.blur))
|
||||
if opts.rotate:
|
||||
if opts.rotate in (-90, 270):
|
||||
print("Rotating 90 degrees clockwise.".format(opts.rotate))
|
||||
elif opts.rotate in (90, -270):
|
||||
print("Rotating 90 degrees counter-clockwise.".format(opts.rotate))
|
||||
elif opts.rotate in (180, -180):
|
||||
print("Rotating 180 degrees.".format(opts.rotate))
|
||||
im = im.rotate(opts.rotate, expand=True)
|
||||
if opts.mirror_x:
|
||||
print("Mirroring left-to-right.")
|
||||
im = im.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
if opts.mirror_y:
|
||||
print("Mirroring top-to-bottom.")
|
||||
im = im.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
pix = im.load()
|
||||
width, height = im.size
|
||||
print("// Image {} ({}x{})".format(filename, width, height), file=outf)
|
||||
|
||||
if opts.range == "dynamic":
|
||||
pixmin = 255;
|
||||
pixmax = 0;
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
pixmin = min(pixmin, pix[x,y])
|
||||
pixmax = max(pixmax, pix[x,y])
|
||||
else:
|
||||
pixmin = 0;
|
||||
pixmax = 255;
|
||||
print("// Original luminances: min={}, max={}".format(pixmin, pixmax), file=outf)
|
||||
print("// Texture heights: min={}, max={}".format(opts.minout, opts.maxout), file=outf)
|
||||
|
||||
print("{} = [".format(opts.varname), file=outf)
|
||||
line = indent
|
||||
for y in range(height):
|
||||
line += "[ "
|
||||
for x in range(width):
|
||||
u = (pix[x,y] - pixmin) / (pixmax - pixmin)
|
||||
val = u * (opts.maxout - opts.minout) + opts.minout
|
||||
line += "{:.3f}".format(val).rstrip('0').rstrip('.') + ", "
|
||||
if len(line) > 60:
|
||||
print(line, file=outf)
|
||||
line = indent * 2
|
||||
line += " ],"
|
||||
if line != indent:
|
||||
print(line, file=outf)
|
||||
line = indent
|
||||
print("];", file=outf)
|
||||
print("", file=outf)
|
||||
|
||||
|
||||
def check_nonneg_float(value):
|
||||
val = float(value)
|
||||
if val < 0:
|
||||
raise argparse.ArgumentTypeError("{} is an invalid non-negative float value".format(val))
|
||||
return val
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='img2tex')
|
||||
parser.add_argument('-o', '--outfile',
|
||||
help='Output .scad file.')
|
||||
parser.add_argument('-v', '--varname',
|
||||
help='Variable to use in .scad file.')
|
||||
parser.add_argument('-i', '--invert', action='store_true',
|
||||
help='Invert luminance values.')
|
||||
parser.add_argument('-r', '--resize',
|
||||
help='Resample image to WIDTHxHEIGHT.')
|
||||
parser.add_argument('-R', '--rotate', choices=(-270, -180, -90, 0, 90, 180, 270), default=0, type=int,
|
||||
help='Rotate output by the given number of degrees.')
|
||||
parser.add_argument('--mirror-x', action="store_true",
|
||||
help='Mirror output in the X direction.')
|
||||
parser.add_argument('--mirror-y', action="store_true",
|
||||
help='Mirror output in the Y direction.')
|
||||
parser.add_argument('--blur', type=check_nonneg_float, default=0,
|
||||
help='Perform a box blur on the output with the given radius.')
|
||||
parser.add_argument('--minout', type=float, default=0.0,
|
||||
help='The value to output for the minimum luminance.')
|
||||
parser.add_argument('--maxout', type=float, default=1.0,
|
||||
help='The value to output for the maximum luminance.')
|
||||
parser.add_argument('--range', choices=["dynamic", "full"], default="dynamic",
|
||||
help='If "dynamic", the lowest to brightest luminances are scaled to the minout/maxout range.\n'
|
||||
'If "full", 0 to 255 luminances will be scaled to the minout/maxout range.')
|
||||
parser.add_argument('infile', help='Input image file.')
|
||||
opts = parser.parse_args()
|
||||
|
||||
non_alnum = re.compile(r'[^a-zA-Z0-9_]')
|
||||
if not opts.varname:
|
||||
if opts.outfile:
|
||||
opts.varname = os.path.splitext(os.path.basename(opts.outfile))[0]
|
||||
opts.varname = non_alnum.sub("", opts.varname)
|
||||
else:
|
||||
opts.varname = "image_data"
|
||||
size_pat = re.compile(r'^([0-9][0-9]*)x([0-9][0-9]*)$')
|
||||
|
||||
opts.invert = bool(opts.invert)
|
||||
|
||||
if opts.resize:
|
||||
m = size_pat.match(opts.resize)
|
||||
if not m:
|
||||
print("Expected WIDTHxHEIGHT resize format.", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
opts.resize = (int(m.group(1)), int(m.group(2)))
|
||||
|
||||
if not opts.varname or non_alnum.search(opts.varname):
|
||||
print("Bad variable name: {}".format(opts.varname), file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
|
||||
if opts.outfile:
|
||||
with open(opts.outfile, "w") as outf:
|
||||
img2tex(opts.infile, opts, outf)
|
||||
else:
|
||||
img2tex(opts.infile, opts, sys.stdout)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
185
shapes3d.scad
185
shapes3d.scad
@@ -996,94 +996,26 @@ function regular_prism(n,
|
||||
assert(is_vector(shift,2), "shift must be a 2D vector.")
|
||||
let(
|
||||
vnf = any_defined([chamfer, chamfer1, chamfer2, rounding, rounding1, rounding2])
|
||||
? assert(is_undef(texture), "Cannot combine roundings or chamfers with texturing")
|
||||
let(
|
||||
vang = atan2(r1-r2,height),
|
||||
_chamf1 = first_defined([chamfer1, if (is_undef(rounding1)) chamfer, 0]),
|
||||
_chamf2 = first_defined([chamfer2, if (is_undef(rounding2)) chamfer, 0]),
|
||||
_fromend1 = first_defined([from_end1, from_end, false]),
|
||||
_fromend2 = first_defined([from_end2, from_end, false]),
|
||||
chang1 = first_defined([chamfang1, chamfang, 45+sign(_chamf1)*vang/2]),
|
||||
chang2 = first_defined([chamfang2, chamfang, 45-sign(_chamf2)*vang/2]),
|
||||
round1 = first_defined([rounding1, if (is_undef(chamfer1)) rounding, 0]),
|
||||
round2 = first_defined([rounding2, if (is_undef(chamfer2)) rounding, 0]),
|
||||
checks1 =
|
||||
assert(is_finite(_chamf1), "chamfer1 must be a finite number if given.")
|
||||
assert(is_finite(_chamf2), "chamfer2 must be a finite number if given.")
|
||||
assert(is_finite(chang1) && chang1>0, "chamfang1 must be a positive number if given.")
|
||||
assert(is_finite(chang2) && chang2>0, "chamfang2 must be a positive number if given.")
|
||||
assert(chang1<90+sign(_chamf1)*vang, "chamfang1 must be smaller than the cone face angle")
|
||||
assert(chang2<90-sign(_chamf2)*vang, "chamfang2 must be smaller than the cone face angle")
|
||||
assert(num_defined([chamfer1,rounding1])<2, "cannot define both chamfer1 and rounding1")
|
||||
assert(num_defined([chamfer2,rounding2])<2, "cannot define both chamfer2 and rounding2")
|
||||
assert(num_defined([chamfer,rounding])<2, "cannot define both chamfer and rounding")
|
||||
undef,
|
||||
chamf1r = !_chamf1? 0
|
||||
: !_fromend1? _chamf1
|
||||
: law_of_sines(a=_chamf1, A=chang1, B=180-chang1-(90-sign(_chamf2)*vang)),
|
||||
chamf2r = !_chamf2? 0
|
||||
: !_fromend2? _chamf2
|
||||
: law_of_sines(a=_chamf2, A=chang2, B=180-chang2-(90+sign(_chamf2)*vang)),
|
||||
chamf1l = !_chamf1? 0
|
||||
: _fromend1? abs(_chamf1)
|
||||
: abs(law_of_sines(a=_chamf1, A=180-chang1-(90-sign(_chamf1)*vang), B=chang1)),
|
||||
chamf2l = !_chamf2? 0
|
||||
: _fromend2? abs(_chamf2)
|
||||
: abs(law_of_sines(a=_chamf2, A=180-chang2-(90+sign(_chamf2)*vang), B=chang2)),
|
||||
facelen = adj_ang_to_hyp(height, abs(vang)),
|
||||
|
||||
roundlen1 = round1 >= 0 ? round1/tan(45-vang/2)
|
||||
: round1/tan(45+vang/2),
|
||||
roundlen2 = round2 >=0 ? round2/tan(45+vang/2)
|
||||
: round2/tan(45-vang/2),
|
||||
dy1 = abs(_chamf1 ? chamf1l : round1 ? roundlen1 : 0),
|
||||
dy2 = abs(_chamf2 ? chamf2l : round2 ? roundlen2 : 0),
|
||||
td_ang = teardrop == true? 45 :
|
||||
teardrop == false? 90 :
|
||||
assert(is_finite(teardrop))
|
||||
assert(teardrop>=0 && teardrop<=90)
|
||||
teardrop,
|
||||
|
||||
checks2 =
|
||||
assert(is_finite(round1), "rounding1 must be a number if given.")
|
||||
assert(is_finite(round2), "rounding2 must be a number if given.")
|
||||
assert(chamf1r <= r1, "chamfer1 is larger than the r1 radius of the cylinder.")
|
||||
assert(chamf2r <= r2, "chamfer2 is larger than the r2 radius of the cylinder.")
|
||||
assert(roundlen1 <= r1, "size of rounding1 is larger than the r1 radius of the cylinder.")
|
||||
assert(roundlen2 <= r2, "size of rounding2 is larger than the r2 radius of the cylinder.")
|
||||
assert(dy1+dy2 <= facelen, "Chamfers/roundings don't fit on the cylinder/cone. They exceed the length of the cylinder/cone face.")
|
||||
undef,
|
||||
path = [
|
||||
[0,-height/2],
|
||||
if (!approx(chamf1r,0))
|
||||
each [
|
||||
[r1, -height/2] + polar_to_xy(chamf1r,180),
|
||||
[r1, -height/2] + polar_to_xy(chamf1l,90+vang),
|
||||
]
|
||||
else if (!approx(round1,0) && td_ang < 90)
|
||||
each _teardrop_corner(r=round1, corner=[[max(0,r1-2*roundlen1),-height/2],[r1,-height/2],[r2,height/2]], ang=td_ang)
|
||||
else if (!approx(round1,0) && td_ang >= 90)
|
||||
each arc(r=abs(round1), corner=[[max(0,r1-2*roundlen1),-height/2],[r1,-height/2],[r2,height/2]])
|
||||
else [r1,-height/2],
|
||||
|
||||
if (is_finite(chamf2r) && !approx(chamf2r,0))
|
||||
each [
|
||||
[r2, height/2] + polar_to_xy(chamf2l,270+vang),
|
||||
[r2, height/2] + polar_to_xy(chamf2r,180),
|
||||
]
|
||||
else if (is_finite(round2) && !approx(round2,0))
|
||||
each arc(r=abs(round2), corner=[[r1,-height/2],[r2,height/2],[max(0,r2-2*roundlen2),height/2]])
|
||||
else [r2,height/2],
|
||||
[0,height/2],
|
||||
]
|
||||
)
|
||||
rotate_sweep(path,closed=false,$fn=n)
|
||||
: is_undef(texture) ? cylinder(h=height, r1=r1, r2=r2, center=true, $fn=n)
|
||||
: linear_sweep(regular_ngon(n=n,r=r1),scale=r2/r1,height=height,center=true,
|
||||
texture=texture, tex_reps=tex_reps, tex_size=tex_size,
|
||||
tex_inset=tex_inset, tex_rot=tex_rot,
|
||||
tex_depth=tex_depth, tex_samples=tex_samples,
|
||||
style=style),
|
||||
? assert(is_undef(texture), "Cannot combine roundings or chamfers with texturing")
|
||||
let(
|
||||
path = [
|
||||
[0,-height/2],
|
||||
each _cyl_path(r1, r2, height,
|
||||
chamfer, chamfer1, chamfer2,
|
||||
chamfang, chamfang1, chamfang2,
|
||||
rounding, rounding1, rounding2,
|
||||
from_end, from_end1, from_end2,
|
||||
teardrop),
|
||||
[0,height/2]
|
||||
]
|
||||
)
|
||||
rotate_sweep(path,caps=true,$fn=n)
|
||||
: is_undef(texture) ? cylinder(h=height, r1=r1, r2=r2, center=true, $fn=n)
|
||||
: linear_sweep(regular_ngon(n=n,r=r1),scale=r2/r1,height=height,center=true,
|
||||
texture=texture, tex_reps=tex_reps, tex_size=tex_size,
|
||||
tex_inset=tex_inset, tex_rot=tex_rot,
|
||||
tex_depth=tex_depth, tex_samples=tex_samples,
|
||||
style=style),
|
||||
skmat = down(height/2) *
|
||||
skew(sxz=shift.x/height, syz=shift.y/height) *
|
||||
up(height/2) *
|
||||
@@ -1151,7 +1083,7 @@ function regular_prism(n,
|
||||
// Synopsis: Creates a cube or trapezoidal prism with a textured top face for attaching to objects.
|
||||
// SynTags: Geom, VNF
|
||||
// Topics: Shapes (3D), Attachable, VNF Generators, Textures
|
||||
// See Also: cuboid(), prismoid(), texture(), cyl(), rotate_sweep(), linear_sweep()
|
||||
// See Also: cuboid(), prismoid(), texture(), cyl(), rotate_sweep(), linear_sweep(), plot3d()
|
||||
// Usage:
|
||||
// textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [diff=], [tex_extra=], [tex_skip=], ...) [ATTACHMENTS];
|
||||
// vnf = textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [tex_extra=], [tex_skip=], ...);
|
||||
@@ -1166,6 +1098,10 @@ function regular_prism(n,
|
||||
// then the texture actually sinks into its base, so the default is set to the 0.1 more than the inset depth. To ensure a valid geometry, with a positive
|
||||
// `inset` or a texture that has negative values you must select a thickness strictly **larger** than the depth the texture extends below zero.
|
||||
// .
|
||||
// Textures are meant to be between 0 and 1 so that `tex_depth` and `tex_inset` behave as expected. If you have a custom textures that
|
||||
// has a different range you can still use it directly, but you may find it more convenient to rescale a height map texture using {{fit_to_range()}]
|
||||
// or a VNF texture using {{fit_to_box()}}.
|
||||
// .
|
||||
// You can also specify a trapzoidal prism using parameters equivalent to those accepted by {{trapezoid()}}, with one change:
|
||||
// `ysize` specifies the width of the prism in the Y direction, and `h`, `height` or `thickness` are used to specify the height
|
||||
// in the Z direction. When you texture a trapezoid the texture will be scaled to the `w1` length if you specify it by size using `tex_size`. The
|
||||
@@ -1174,7 +1110,7 @@ function regular_prism(n,
|
||||
// Two anchor types are available. The default atype is "tex" which assumes you want to place the texture on another object using
|
||||
// {{attach()}}. It provides anchors that ignore the base object and place the BOTTOM anchor at the bottom of the texture. The TOP anchor
|
||||
// will be at the top face of the texture. Note that if your texture doesn't span the range from [0,1] these anchors won't be correctly located.
|
||||
// For an inset texture, the "tex" anchors are all at the top of the texture. This anchor type works with `anchor(face,BOT)` where `face` is some
|
||||
// For an inset texture, the "tex" anchors are all at the top of the texture. This anchor type works with `attach(face,BOT)` where `face` is some
|
||||
// face on a parent object that needs a texture. If you want to use the textured object directly the "std" anchors are probably more useful.
|
||||
// These anchors are the usual anchors for the base object, ignoring the applied texture. If you want the anchors to be on top of the texture,
|
||||
// set `tex_inset=true`.
|
||||
@@ -1216,35 +1152,40 @@ function regular_prism(n,
|
||||
// tex_extra = number of extra lines of a hightfield texture to add at the end. Can be a scalar or 2-vector to give x and y values. Default: 0 if `tex_reps=[1,1]`, 1 otherwise
|
||||
// tex_skip = number of lines of a heightfield texture to skip when starting. Can be a scalar or two vector to give x and y values. Default: 0
|
||||
// style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "min_edge"
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `BOTTOM` if `astyle` is "tex", `CENTER` otherwise
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
|
||||
// Example(3D,NoScales,VPT=[-0.257402,0.467403,-0.648606],VPR=[46.6,0,16.6],VPD=29.2405): Basic textured tile
|
||||
// textured_tile("trunc_diamonds", 10, tex_reps=[5,5]);
|
||||
// Example(3D,NoAxes,VPT=[-0.0852782,0.259593,0.139667],VPR=[58.5,0,345.1],VPD=36.0994): Attaching a tile to a cube
|
||||
// cuboid([12,12,4]) attach(TOP,BOT)
|
||||
// textured_tile("trunc_pyramids", 10, tex_reps=[5,5], style="convex");
|
||||
// textured_tile("trunc_pyramids", 10, tex_reps=[5,5],
|
||||
// style="convex");
|
||||
// Example(3D,NoScales,VPT = [-0.0788193, 0.10015, -0.0938629], VPR = [57.8, 0, 34.1], VPD = 29.2405): This inset texture doesn't look obviously different, but you can see that the object is below the XY plane.
|
||||
// textured_tile("trunc_pyramids_vnf", 10, tex_reps=[5,5], tex_inset=true);
|
||||
// textured_tile("trunc_pyramids_vnf", 10, tex_reps=[5,5],
|
||||
// tex_inset=true);
|
||||
// Example(3D,NoAxes,VPT=[0.242444,0.170054,-0.0714754],VPR=[67.6,0,33.4],VPD=36.0994): Here we use the `diff` option combined with {{diff()}} to attach the inset texture to the front of a parent cuboid.
|
||||
// diff()
|
||||
// cuboid([12,5,10]) attach(FRONT, BOT)
|
||||
// textured_tile("trunc_pyramids_vnf", [10,8],
|
||||
// tex_reps=[5,5], tex_inset=true, diff=true);
|
||||
// Example(3D,NoAxes,VPT=[5.86588,-0.107082,-0.311155],VPR=[17.2,0,9.6],VPD=32.4895): Tile shaped like a rhombic prism
|
||||
// textured_tile("ribs", w1=10, w2=10, shift=4, ysize=7, tex_reps=[5,1]);
|
||||
// textured_tile("ribs", w1=10, w2=10, shift=4, ysize=7,
|
||||
// tex_reps=[5,1]);
|
||||
// Example(3D,NoAxes,VPT=[-0.487417,-0.398897,-0.143258],VPR=[10.2,0,12.4],VPD=26.3165): A tile shaped like a trapezoidal prism. Note that trapezoidal tiles will always distort the texture, resulting in curves
|
||||
// textured_tile("diamonds", w1=10, w2=7, ysize=7, tex_reps=5);
|
||||
// Example(3D,NoAxes,VPT=[-0.0889877,-0.31974,0.554444],VPR=[22.1,0,22.2],VPD=32.4895): An inset trapezoidal tile placed into a cube
|
||||
// diff()cuboid([10,10,2])
|
||||
// attach(TOP,BOT)
|
||||
// textured_tile("trunc_diamonds", tex_reps=[5,5], tex_inset=true,
|
||||
// w1=8, w2=4, ysize=8, diff=true);
|
||||
// textured_tile("trunc_diamonds", tex_reps=[5,5],
|
||||
// tex_inset=true, diff=true,
|
||||
// w1=8, w2=4, ysize=8);
|
||||
// Example(3D,NoAxes,VPT=[-0.0889877,-0.31974,0.554444],VPR=[58.5,0,21.5],VPD=32.4895): This example shows what happens if you set `tex_extra` to zero for the "pyramids" texture. Note that the texture doesn't finish. The default of `tex_extra=1` produces the correct result.
|
||||
// textured_tile("pyramids", 10, tex_reps=[5,5], tex_extra=0);
|
||||
// Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): This texture has an asymmetry even with the default `tex_extra=1`.
|
||||
// Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): This texture has an asymmetry with the default `tex_extra=1`.
|
||||
// textured_tile("trunc_ribs", 10, tex_reps=[5,1]);
|
||||
// Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): It could be fixed by setting `tex_extra=2`, which would place an extra flat strip on the right. But another option is to use the `tex_skip` parameter to trim the flat part from the left. Note that we are also skipping in the y direction, but it doesn't make a difference for this texture, except that you need to have enough texture tiles to accommodate the skip, so we increased the Y reps value to 2. You can also set `tex_skip` to a vector.
|
||||
// textured_tile("trunc_ribs", 10, tex_reps=[5,2], tex_skip=1);
|
||||
// Example(3D,NoAxes): Textures can be used to place images onto objects. Here we place a very simple image into a cube, leaving a border around the image.
|
||||
// textured_tile("trunc_ribs", 10, tex_reps=[5,2], tex_skip=1);
|
||||
// img = [
|
||||
@@ -1278,10 +1219,10 @@ module textured_tile(
|
||||
tex_skip=0,
|
||||
style="min_edge",
|
||||
atype="tex",
|
||||
anchor=CENTER, spin=0, orient=UP
|
||||
anchor, spin=0, orient=UP
|
||||
)
|
||||
{
|
||||
|
||||
anchor = default(anchor, atype=="tex" ? BOTTOM : CENTER);
|
||||
vnf_data = textured_tile(size=size,
|
||||
ysize=ysize, height=height, w1=w1, w2=w2, ang=ang, h=h, shift=shift,
|
||||
texture=texture, tex_size=tex_size, tex_reps=tex_reps,tex_extra=tex_extra,
|
||||
@@ -1329,16 +1270,17 @@ function textured_tile(
|
||||
atype="tex",
|
||||
tex_extra,
|
||||
tex_skip=0,
|
||||
anchor=CENTER, spin=0, orient=UP,
|
||||
anchor, spin=0, orient=UP,
|
||||
_return_anchor=false
|
||||
) =
|
||||
) =
|
||||
assert(in_list(atype,["tex","std"]), "atype must be \"tex\" or \"std\"")
|
||||
assert(is_undef(tex_reps) || is_int(tex_reps) || (all_integer(tex_reps) && len(tex_reps)==2), "tex_reps must be an integer or list of two integers")
|
||||
assert(is_undef(tex_size) || is_vector(tex_size,2) || is_finite(tex_size))
|
||||
assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps")
|
||||
assert(num_defined([tex_size, tex_reps])==1, "Must give exactly one of tex_size and tex_reps")
|
||||
assert(is_undef(size) || is_num(size) || is_vector(size,2) || is_vector(size,3), "size must be a 2-vector or 3-vector")
|
||||
assert(is_undef(size) || num_defined([ysize,h, height, thickness, w1,w2,ang])==0, "Cannot combine size with any other dimensional specifications")
|
||||
|
||||
let(
|
||||
anchor = default(anchor, atype=="tex" ? BOTTOM : CENTER),
|
||||
inset = is_num(tex_inset)? tex_inset : tex_inset? 1 : 0,
|
||||
default_thick = inset>0 ? 0.1+abs(tex_depth)*inset : 0.1,
|
||||
extra_ht = max(0,abs(tex_depth)*(1-inset)),
|
||||
@@ -1362,10 +1304,10 @@ function textured_tile(
|
||||
texture = _get_texture(texture, tex_rot),
|
||||
|
||||
tex_reps = is_def(tex_reps) ? force_list(tex_reps,2)
|
||||
: let(tex_size=is_undef(tex_size)? [5,5] : force_list(tex_size,2))
|
||||
: let(tex_size=force_list(tex_size,2))
|
||||
[round(size.x/tex_size.x), round(size.y/tex_size.y)],
|
||||
extra = is_undef(extra)? tex_reps == [1,1] ? [0,0] : [1,1]
|
||||
: force_list(tex_extra,2),
|
||||
extra = is_undef(tex_extra)? tex_reps == [1,1] ? [0,0] : [1,1]
|
||||
: force_list(tex_extra,2),
|
||||
skip = force_list(tex_skip,2),
|
||||
scale = [size.x/tex_reps.x, size.y/tex_reps.y],
|
||||
setz=function (v,z) [v.x,v.y,z],
|
||||
@@ -1381,7 +1323,7 @@ function textured_tile(
|
||||
scaled_tex = tex_depth < 0 ? [for(row=texture) [for(p=row) -(1-p-inset)*tex_depth]]
|
||||
: [for(row=texture) [for(p=row) (p-inset)*tex_depth]],
|
||||
check = [for(row=scaled_tex, p=row) if (p<=-height) p],
|
||||
dummy=assert(check==[], str("texture extends too far below zero (",min([each check,0]),") to fit in cube with height ",height)),
|
||||
dummy=assert(check==[], str("texture extends too far below zero (",min([each check,0]),") to fit entirely within height ",height)),
|
||||
pts=[for(y=idx(ypts))
|
||||
[ [xpts[0],ypts[y],-height/2],
|
||||
for(x=idx(xpts))
|
||||
@@ -1394,9 +1336,12 @@ function textured_tile(
|
||||
:
|
||||
let(
|
||||
zadj_vnf = [
|
||||
[for(p=texture[0]) [p.x, p.y, tex_depth<0 ? height/2-(1-p.z-inset)*tex_depth : height/2+(p.z-inset)*tex_depth]],
|
||||
[for(p=texture[0]) [p.x, p.y, height/2 + _tex_height(tex_depth,inset,p.z)]],
|
||||
texture[1]
|
||||
],
|
||||
minz = min(column(zadj_vnf[0],2)),
|
||||
dummy=assert(minz>-height/2, str("texture extends too far below zero (",minz-height/2,") to fit entirely within height ",height)),
|
||||
|
||||
scaled_vnf = scale(scale, zadj_vnf),
|
||||
tiled_vnf = [for(i=[0:1:tex_reps.x-1], j=[0:1:tex_reps.y-1]) move([scale.x*i,scale.y*j], scaled_vnf)],
|
||||
|
||||
@@ -1778,12 +1723,12 @@ function rect_tube(
|
||||
// wedge([20, 40, 15], center=true);
|
||||
// Example: *Non*-Centered
|
||||
// wedge([20, 40, 15]);
|
||||
// Example: Standard Anchors
|
||||
// Example(3D,Med,VPR=[59.50,0.00,36.90],VPD=257.38,VPT=[5.60,-1.98,3.65]): Standard Anchors
|
||||
// wedge([40, 80, 30], center=true)
|
||||
// show_anchors(custom=false);
|
||||
// color([0.5,0.5,0.5,0.1])
|
||||
// cube([40, 80, 30], center=true);
|
||||
// Example: Named Anchors
|
||||
// Example(3D,Med,VPR=[55.00,0.00,25.00],VPD=151.98,VPT=[2.30,-11.81,-5.66]): Named Anchors
|
||||
// wedge([40, 80, 30], center=true)
|
||||
// show_anchors(std=false);
|
||||
|
||||
@@ -2085,7 +2030,7 @@ function cylinder(h, r1, r2, center, r, d, d1, d2, anchor, spin=0, orient=UP) =
|
||||
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
|
||||
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
|
||||
// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
|
||||
// tex_taper = If given as a number, tapers the texture height to zero over the first and last given percentage of the path. If given as a lookup table with indices between 0 and 100, uses the percentage lookup table to ramp the texture heights. Default: `undef` (no taper)
|
||||
// tex_taper = The `tex_taper` parameter to {{rotate_sweep()}}. If given as a number, tapers the texture depth to zero at the ends over the specified fraction of the height. You can also give a lookup table or function to create custom depth taperings across the height of the cylinder. The lookup table or function should be defined on the interval [0,1] where 0 gives the depth multiplier at the bottom and 1 the depth multiplier at the top. Default: no taper
|
||||
// style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "min_edge"
|
||||
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
|
||||
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
|
||||
@@ -2179,10 +2124,10 @@ function cylinder(h, r1, r2, center, r, d, d1, d2, anchor, spin=0, orient=UP) =
|
||||
//
|
||||
// Example: Taper Texture over First and Last 10%
|
||||
// cyl(d1=25, d2=20, h=30, rounding=5,
|
||||
// texture="trunc_ribs", tex_taper=10,
|
||||
// texture="trunc_ribs", tex_taper=0.1,
|
||||
// tex_size=[5,1]);
|
||||
//
|
||||
// Example: Making a Clay Pattern Roller
|
||||
// Example(3D,Med,NoAxes): Making a Clay Pattern Roller
|
||||
// tex = [
|
||||
// [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],
|
||||
// [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],
|
||||
@@ -2205,7 +2150,7 @@ function cylinder(h, r1, r2, center, r, d, d1, d2, anchor, spin=0, orient=UP) =
|
||||
// diff()
|
||||
// cyl(d=20*10/PI, h=10, chamfer=0,
|
||||
// texture=tex, tex_reps=[20,1], tex_depth=-1,
|
||||
// tex_taper=undef, style="concave") {
|
||||
// style="concave") {
|
||||
// attach([TOP,BOT]) {
|
||||
// cyl(d1=20*10/PI, d2=30, h=5, anchor=BOT)
|
||||
// attach(TOP) {
|
||||
@@ -2366,7 +2311,7 @@ function cyl(
|
||||
texture=texture, tex_reps=tex_reps, tex_size=tex_size,
|
||||
tex_inset=tex_inset, tex_rot=tex_rot,
|
||||
tex_depth=tex_depth, tex_samples=tex_samples,
|
||||
tex_taper=tex_taper, style=style, closed=false,
|
||||
tex_taper=tex_taper, style=style, caps=true,
|
||||
_tex_inhibit_y_slicing=true
|
||||
),
|
||||
skmat = down(l/2) *
|
||||
@@ -4115,7 +4060,7 @@ module path_text(path, text, font, size, thickness, lettersize, offset=0, revers
|
||||
frame_map(
|
||||
x=point3d(tangent-adjustment),
|
||||
y=point3d(usetop ? toppts[i] : -normpts[i])
|
||||
) left(lsize[0]/2) {
|
||||
) left(lsize[i]/2) {
|
||||
text(text[i], font=font, size=size, language=language, script=script);
|
||||
}
|
||||
}
|
||||
@@ -4234,7 +4179,7 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor
|
||||
// Synopsis: Generates a surface by evaluating a function on a 2D grid
|
||||
// SynTags: Geom, VNF
|
||||
// Topics: Function Plotting
|
||||
// See Also: plot_revolution()
|
||||
// See Also: plot_revolution(), textured_tile()
|
||||
// Usage: As Module
|
||||
// plot3d(f, x, y, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS];
|
||||
// Usage: As Function
|
||||
@@ -4404,13 +4349,13 @@ function plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0,
|
||||
// "intersect" = Anchors to the surface of the shape.
|
||||
// Named Anchors:
|
||||
// "origin" = Anchor at the origin, oriented UP.
|
||||
// Example(3D,NoScale):
|
||||
// Example(3D,NoScale,VPR=[85.10,0.00,127.90],VPD=292.71,VPT=[21.15,13.27,0.90):
|
||||
// f = function (x,y) 5*cos(5*norm([x*180/50,y*180/50]))+5;
|
||||
// plot_revolution(f, arclength=[-50:1:50], z=[-50:1:50], r=30);
|
||||
// Example(3D,NoScale): When specifying angle, the pattern shrinks at the top of the cone.
|
||||
// Example(3D,NoScale,VPR=[69.00,0.00,96.40],VPD=361.36,VPT=[14.10,5.03,4.53]): When specifying angle, the pattern shrinks at the top of the cone.
|
||||
// g = function (x,y) 5*sin(4*x)*cos(6*y)+5;
|
||||
// plot_revolution(g, z=[-60:2:60], angle=[-180:4:180], r1=30, r2=16);
|
||||
// Example(3D,NoScale): When specifying arc length, the shape wraps around more cone at the top
|
||||
// Example(3D,NoScale,VPR=[69.00,0.00,96.40],VPD=361.36,VPT=[14.10,5.03,4.53]): When specifying arc length, the shape wraps around more cone at the top
|
||||
// g = function (x,y) 5*sin(8*x)*cos(6*y)+5;
|
||||
// plot_revolution(g, z=[-60:.5:60], arclength=[-45:.5:45],r1=30,r2=16);
|
||||
// Example(3D,VPR=[60.60,0.00,100.60],VPD=100.87,VPT=[-1.84,-1.70,5.63]): Here we place a simple ridge function onto a cone using angle. Note how the ribs narrow with the radius.
|
||||
|
408
skin.scad
408
skin.scad
@@ -724,7 +724,9 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close
|
||||
// cyl(d=14.5,h=1,anchor=BOT,rounding=1/3,$fa=1,$fs=.5);
|
||||
// linear_sweep(circle(d=12), h=12, scale=1.3, texture=diag_weave_vnf,
|
||||
// tex_size=[5,5], convexity=12);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
module linear_sweep(
|
||||
region, height, center,
|
||||
@@ -894,10 +896,31 @@ function linear_sweep(
|
||||
// the sweep starts; set it to 180 to get the historical rotate_extrude() behavior.
|
||||
// .
|
||||
// The region or path that you provide to sweep is defined in the XY plane and cannot have any negative x values. By default a path is treated as a closed shape.
|
||||
// (Regions are always composed of closed polygons.) If you give a path and specify `closed=false` then the path will be connected to the Y axis by
|
||||
// a horizontal segment at each end, resulting in flat faces at the top and bottom. These flat faces do not receive any applied texture. No segment of of the
|
||||
// region---including the closing segments added to polygons---can lie on the Y axis. When `closed=false` you can terminate one or both ends of the path
|
||||
// on the Y axis if you want texturing to continue all the way to the center.
|
||||
// (Regions are always composed of closed polygons.) When you apply a texture, no path in your region can have more than one edge on the Y axis.
|
||||
// If you give a path whose endpoints are not on the Y axis and specify `caps=true` then the path
|
||||
// endpoints are connected to the Y axis by a horizontal segment at each end, and the corresponding top and bottom surfaces in the revolution do not receive texture.
|
||||
// You can terminate just one end of the path on the Y axis and in this case, you get a single untextured cap. If your texture is not zero at the
|
||||
// edges, the endcaps may appear textured: they will not be flat because the top perimeter will follow the texture.
|
||||
// .
|
||||
// When `caps=true` you can use `tex_taper` to change the depth of the texture along the length of the path given in `shape`. This
|
||||
// can be useful for forcing flat caps on a textured object by forcing the texture depth to zero at the ends.
|
||||
// The simplest option is to set `tex_taper` to a value between 0 and 0.5. In this case, the texture depth linearly falls to zero
|
||||
// at both ends, starting at the specified fraction from the end. For example, if `tex_taper=1/3` then the center third of the object
|
||||
// will have the normal texture depth, and the texture will fall to zero over the top and bottom thirds. For more control over the texture
|
||||
// tapering you can also set `tex_taper` to a lookup table suited to the `lookup()` function. The lookup table will be evaluated at 0 to
|
||||
// determine the texture depth multiplier at the bottom and at 1 to determine the texture depth multiplier at the top. The final option is
|
||||
// to set `tex_taper` to a function which takes one parameter and is defined on [0,1]. Using these more sophisticated methods you can actually
|
||||
// change the shape of the object. If you want to ensure flat caps, simply make sure that your lookup table or function maps both zero and one to zero.
|
||||
// Texture multipliers can be any number. If the multiplier is negative it will invert the texture, and if the multiplier exceeds one, the texture will
|
||||
// scale to larger than your specified `tex_depth` value.
|
||||
// .
|
||||
// If you want to place just one or a few copies of a texture onto an object rather than texturing the entire object you can do that by using
|
||||
// and angle smaller than 360. However, if you want to control the aspect ratio of the resulting texture you will have to carefully calculate the proper
|
||||
// angle to use to ensure that the arc length in the horizontal direction is the proper length compared to the arc length in the vertical direction.
|
||||
// To simplify this process you can use `pixel_aspect` or `tex_aspect`. You can set `tex_aspect` for any type of tile and it specifies
|
||||
// the desired aspect ratio (width/height) for the tiles. You must specify `tex_reps` in order to use this feature. For heightfields you can instead provide
|
||||
// a pixel aspect ratio, which is suited to the case where your texture is a non-square image that you want to place on a curved object. For a simple cylinder
|
||||
// it is obvious what the horizontal arc length is; for other objects this is computed based on the average radius of the longest path in `shape`.
|
||||
// Arguments:
|
||||
// shape = The polygon or [region](regions.scad) to sweep around the Z axis.
|
||||
// angle = If given, specifies the number of degrees to sweep the region around the Z axis, counterclockwise from the X+ axis. Default: 360 (full rotation)
|
||||
@@ -910,9 +933,11 @@ function linear_sweep(
|
||||
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
|
||||
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
|
||||
// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
|
||||
// tex_taper = If given as a number, tapers the texture height to zero over the first and last given percentage of the path. If given as a lookup table with indices between 0 and 100, uses the percentage lookup table to ramp the texture heights. Default: `undef` (no taper)
|
||||
// tex_taper = If `caps=true`, scales the texture depth along the path given in `shape`. If set to a scalar between 0 and 0.5, adjusts the specfied top and bottom fraction of the path linearly to zero depth. You can also provide a lookup table or function defining the scala factor over the range [0,1]. Default: no taper
|
||||
// tex_aspect = Choose the angle of the revolution to maintain this aspect ratio for the tiles. You must specify tex_reps. Overrides any angle specified.
|
||||
// pixel_aspect = Choose the angle of the revolution to maintain this apsect ratio for pixels in a heightfield texture. You must specify tex_reps. Overrides any angle specified.
|
||||
// style = {{vnf_vertex_array()}} style. Default: "min_edge"
|
||||
// closed = If false, and `shape` is a path, then the revolved path is connected to the axis of rotation with untextured caps. Ignored if `shape` is not a path. Default: `true`
|
||||
// caps = If true and `shape` is a path whose endpoints are to the right of the Y axis, then adds untextured caps to the top and/or bottom of the revolved surface. Ignored if `shape` is not a path or if its endpoints are on the Y axis. Default: `false`
|
||||
// convexity = (Module only) Convexity setting for use with polyhedron. Default: 10
|
||||
// cp = Centerpoint for determining "intersect" anchors or centering the shape. Determintes the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid"
|
||||
// atype = Select "hull" or "intersect" anchor types. Default: "hull"
|
||||
@@ -924,7 +949,12 @@ function linear_sweep(
|
||||
// Anchor Types:
|
||||
// "hull" = Anchors to the virtual convex hull of the shape.
|
||||
// "intersect" = Anchors to the surface of the shape.
|
||||
// Example:
|
||||
// Example(3D,NoAxes,VPR=[60.20,0.00,41.80],VPD=151.98,VPT=[0.85,-2.95,3.10]): Sweeping a shape that looks like a plus sign
|
||||
// rgn = right(30,
|
||||
// union([for (a = [0, 90])
|
||||
// zrot(a, rect([15,5]))]));
|
||||
// rotate_sweep(rgn);
|
||||
// Example(3D,NoAxes,VPR=[50.40,0.00,28.50],VPD=208.48,VPT=[0.23,-1.89,5.20]): Sweeping a region with multiple components
|
||||
// rgn = [
|
||||
// for (a = [0, 120, 240]) let(
|
||||
// cp = polar_to_xy(15, a) + [30,0]
|
||||
@@ -934,19 +964,35 @@ function linear_sweep(
|
||||
// ]
|
||||
// ];
|
||||
// rotate_sweep(rgn, angle=240);
|
||||
// Example:
|
||||
// rgn = right(30, p=union([for (a = [0, 90]) rot(a, p=rect([15,5]))]));
|
||||
// rotate_sweep(rgn);
|
||||
// Example:
|
||||
// Example(3D,NoAxes,VPR=[55.00,0.00,25.00],VPD=292.71,VPT=[1.59,1.80,-1.35]): Torus with bricks texture
|
||||
// path = right(50, p=circle(d=40));
|
||||
// rotate_sweep(path, texture="bricks_vnf", tex_size=[10,10], tex_depth=0.5, style="concave");
|
||||
// Example(NoAxes): The simplest way to create a cylinder with just a single line segment and `closed=false`. Note that all cylinder models will require `closed=false` because otherwise a closing line segment lies on the Y axis. With this cylinder, the top and bottom have no texture.
|
||||
// rotate_sweep([[20,-10],[20,10]], texture="dots", tex_reps=[6,2],closed=false);
|
||||
// Example(NoAxes): If we manually connect the top and bottom then they also receive texture. Note that `closed` is still false, but the caps have zero area.
|
||||
// rotate_sweep([[0,-10],[20,-10],[20,10],[0,10]], texture="dots", tex_reps=[6,6],closed=false,tex_depth=1.5);
|
||||
// rotate_sweep(path, texture="bricks_vnf",tex_size=10,
|
||||
// tex_depth=0.5, style="concave");
|
||||
// Example(3D,NoAxes,VPR=[76.30,0.00,44.60],VPD=257.38,VPT=[2.58,-5.21,0.37]): Applying a texture to a region. Both the inside and outside receive texture.
|
||||
// rgn = [
|
||||
// right(40, p=circle(d=50)),
|
||||
// right(40, p=circle(d=40,$fn=6)),
|
||||
// ];
|
||||
// rotate_sweep(
|
||||
// rgn, texture="diamonds",
|
||||
// tex_size=[10,10], tex_depth=1,
|
||||
// angle=240, style="concave");
|
||||
// Example(NoAxes): The simplest way to create a cylinder with just a single line segment and `caps=true`. With this cylinder, the top and bottom have no texture.
|
||||
// rotate_sweep([[20,-10],[20,10]], texture="dots",
|
||||
// tex_reps=[6,2],caps=true);
|
||||
// Example(NoAxes): If we manually connect the top and bottom then they also receive texture.
|
||||
// rotate_sweep([[0,-10],[20,-10],[20,10],[0,10]],
|
||||
// tex_reps=[6,6],,tex_depth=1.5,
|
||||
// texture="dots");
|
||||
// Example(NoAxes,VPR=[95.60,0.00,69.80],VPD=74.40,VPT=[5.81,5.74,1.97]): You can connect just the top or bottom alone instead of both to get texture on one and a flat cap on the other. Here you can see that the sloped top has texture but the bottom does not. Also note that the texture doesn't fit neatly on the side and top like it did in the previous two examples, but makes a somewhat ugly transition across the corner. You have to size your object carefully so that the tops and sides each fit an integer number of texture tiles to avoid this type of transition.
|
||||
// rotate_sweep([[15,-10],[15,10],[0,15]], texture="dots", tex_reps=[6,6],angle=90,closed=false,tex_depth=1.5);
|
||||
// Example:
|
||||
// rotate_sweep([[15,-10],[15,10],[0,15]],
|
||||
// texture="dots", tex_reps=[6,6],
|
||||
// angle=90,caps=true,tex_depth=1.5);
|
||||
// Example(NoAxes,VPR=[55.00,0.00,25.00],VPD=126.00,VPT=[1.37,0.06,-0.75]): Ribbed sphere.
|
||||
// path = arc(r=20, $fn=64, angle=[-90, 90]);
|
||||
// rotate_sweep(path, 360, texture = texture("wave_ribs",n=15),
|
||||
// tex_size=[8,1.5]);
|
||||
// Example(3D,NoAxes,VPR=[60.20,0.00,56.50],VPD=231.64,VPT=[4.18,-2.66,1.31]): This model uses `caps=true` to create the untextured caps with a user supplied texture. They are flat because the texture is zero at its edges.
|
||||
// tex = [
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
@@ -963,10 +1009,10 @@ function linear_sweep(
|
||||
// ];
|
||||
// path = arc(cp=[0,0], r=40, start=60, angle=-120);
|
||||
// rotate_sweep(
|
||||
// path, closed=false,
|
||||
// path, caps=true,
|
||||
// texture=tex, tex_size=[20,20],
|
||||
// tex_depth=1, style="concave");
|
||||
// Example:
|
||||
// Example(3D,NoAxes,VPR=[60.20,0.00,56.50],VPD=187.63,VPT=[2.07,-4.53,2.58]): An example with a more complicated path. Here the caps are not flat because the diamonds texture is not zero at the edges.
|
||||
// bezpath = [
|
||||
// [15, 30], [10,15],
|
||||
// [10, 0], [20, 10], [30,12],
|
||||
@@ -975,57 +1021,73 @@ function linear_sweep(
|
||||
// ];
|
||||
// path = bezpath_curve(bezpath, splinesteps=32);
|
||||
// rotate_sweep(
|
||||
// path, closed=false,
|
||||
// path, caps=true,
|
||||
// texture="diamonds", tex_size=[10,10],
|
||||
// tex_depth=1, style="concave");
|
||||
// Example:
|
||||
// path = [
|
||||
// [20, 30], [20, 20],
|
||||
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
|
||||
// [20,-20], [20,-30],
|
||||
// ];
|
||||
// vnf = rotate_sweep(
|
||||
// path, closed=false,
|
||||
// texture="trunc_pyramids",
|
||||
// tex_size=[5,5], tex_depth=1,
|
||||
// style="convex");
|
||||
// vnf_polyhedron(vnf, convexity=10);
|
||||
// Example:
|
||||
// rgn = [
|
||||
// right(40, p=circle(d=50)),
|
||||
// right(40, p=circle(d=40,$fn=6)),
|
||||
// ];
|
||||
// rotate_sweep(
|
||||
// rgn, texture="diamonds",
|
||||
// tex_size=[10,10], tex_depth=1,
|
||||
// angle=240, style="concave");
|
||||
// Example: Tapering off the ends of the texturing.
|
||||
// Example(3D,NoAxes,VPR=[70.00,0.00,58.60],VPD=208.48,VPT=[1.92,-3.81,2.21]): The normal direction at the ends is perpendicular to the Z axis, so even though the texture is not zero, the caps are flat, unlike the previous example.
|
||||
// path = [
|
||||
// [20, 30], [20, 20],
|
||||
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
|
||||
// [20,-20], [20,-30],
|
||||
// ];
|
||||
// rotate_sweep(
|
||||
// path, closed=false,
|
||||
// path, caps=true,
|
||||
// texture="diamonds",
|
||||
// tex_size=[5,5], tex_depth=1,
|
||||
// style="concave",
|
||||
// convexity=10);
|
||||
// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): The top cap is definitely not flat.
|
||||
// rotate_sweep(
|
||||
// arc(r=20,angle=[-45,45],n=45),
|
||||
// caps=true, texture="diamonds",
|
||||
// tex_size=[5,5], tex_depth=2,
|
||||
// convexity=10);
|
||||
// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): Setting `tex_taper=0` abruptly tapers right at the caps so that the cap is flat:
|
||||
// rotate_sweep(
|
||||
// arc(r=20,angle=[-45,45],n=45),
|
||||
// caps=true, texture="diamonds",
|
||||
// tex_size=[5,5], tex_depth=2,
|
||||
// tex_taper=0, convexity=10);
|
||||
// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): Setting `tex_taper=0.5` tapers gradually across the entire shape:
|
||||
// rotate_sweep(
|
||||
// arc(r=20,angle=[-45,45],n=45),
|
||||
// caps=true, texture="diamonds",
|
||||
// tex_size=[5,5], tex_depth=2,
|
||||
// tex_taper=.5, convexity=10);
|
||||
// Example(3D,VPR=[59.20,0.00,91.10],VPD=126.00,VPT=[4.29,2.29,2.31],NoAxes): The path given here starts and ends on the Y axis, but you can still request (zero size) caps so that you can use tapering, which is only permitted when caps are enabled.
|
||||
// rotate_sweep(
|
||||
// arc(r=20, angle=[-90,90], n=45), texture="dots",
|
||||
// caps=true, tex_reps=[15,10], tex_taper=0.5, tex_depth=2);
|
||||
// Example(3D, NoAxes): Tapering of textures via lookup table to be maximal at the bottom and 0 at the top.
|
||||
// path = [
|
||||
// [20, 30], [20, 20],
|
||||
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
|
||||
// [20,-20], [20,-30],
|
||||
// ];
|
||||
// rotate_sweep(
|
||||
// path, caps=true,
|
||||
// texture="trunc_pyramids",
|
||||
// tex_size=[5,5], tex_depth=1,
|
||||
// tex_taper=20,
|
||||
// tex_taper=[[0,1], [1,0]],
|
||||
// style="convex",
|
||||
// convexity=10);
|
||||
// Example: Tapering of textures via lookup table.
|
||||
// path = [
|
||||
// [20, 30], [20, 20],
|
||||
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
|
||||
// [20,-20], [20,-30],
|
||||
// ];
|
||||
// rotate_sweep(
|
||||
// path, closed=false,
|
||||
// texture="trunc_pyramids",
|
||||
// tex_size=[5,5], tex_depth=1,
|
||||
// tex_taper=[[0,0], [10,0], [10.1,1], [100,1]],
|
||||
// style="convex",
|
||||
// convexity=10);
|
||||
// Example(3D,NoAxes,Med,VPT=[-2.92656,1.26781,0.102897],VPR=[62.7,0,222.4],VPD=216.381): This VNF tile makes a closed shape and the actual main extrusion is not created.
|
||||
// Example(3D,NoAxes,VPR=[106.10,0.00,158.30],VPD=155.56,VPT=[-2.68,-0.92,1.07]): Here we use a cosine function (lifted so it stays nonnegative) to scale the texture. Since the taper function rises as high as 2 the effective texture depth is 4 at the peaks.
|
||||
// rotate_sweep([[20,-20],[20,20]],texture="trunc_diamonds",
|
||||
// caps=true, tex_reps=[20,16], tex_depth=2,
|
||||
// tex_taper=function(x) 1-cos(360*3*x));
|
||||
// Example(3D,NoAxes,VPR=[83.70,0.00,195.40],VPD=82.67,VPT=[-1.69,4.43,0.46]): Here we use a sine function that goes below zero in the top half of the object. This inverts the texture and the result is that the inverted texture bulges outward with the change in the texture depth that the taper applies. In the bottom section, the scaling applies directly.
|
||||
// rotate_sweep([[10,-12],[10,12]], caps=true, tex_reps=[16,6],
|
||||
// tex_taper=function(x) sin(360*x),tex_depth=2,
|
||||
// texture="dots");
|
||||
// Example(3D,NoAxes,VPR=[83.70,0.00,195.40],VPD=82.67,VPT=[-1.69,4.43,0.46]): We adjust the VNF texture from the previous example so its "zero" level is at 1/2. This makes the result symmetric between the positive and negative taperings.
|
||||
// tex = up(1/2,zscale(1/2,texture("dots")));
|
||||
// rotate_sweep([[10,-12],[10,12]], caps=true, tex_reps=[16,6],
|
||||
// tex_taper=function(x) sin(360*x),tex_depth=2,
|
||||
// texture=tex);
|
||||
// Example(3D,NoAxes,VPR=[72.50,0.00,119.10],VPD=155.56,VPT=[7.95,8.65,3.01]): Here we create a texture effect entirely with tapering using a constant "texture" of 3/4. The inverted texture is 1-3/4 = 1/4, so the negative regions of the function create shallower bands.
|
||||
// rotate_sweep([[20,-20],[20,20]], caps=true, tex_reps=[30,45], texture=[[3/4]],
|
||||
// tex_taper=function(x) sin(2.5*360*x),tex_depth=4);
|
||||
// Example(3D,NoAxes,Med,VPT=[-2.92656,1.26781,0.102897],VPR=[62.7,0,222.4],VPD=216.381): This VNF tile makes a closed shape and the actual main extrusion is not created. We give `caps=true` to prevent the shape from being closed on the outside, but because the VNF tile has no edges, no actual cap is created.
|
||||
// shape = skin([rect(2/5),
|
||||
// rect(2/3),
|
||||
// rect(2/5)],
|
||||
@@ -1034,7 +1096,7 @@ function linear_sweep(
|
||||
// caps=false);
|
||||
// tile = move([0,1/2,2/3],yrot(90,shape));
|
||||
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
|
||||
// rotate_sweep(path, closed=false, texture=tile,
|
||||
// rotate_sweep(path, caps=false, texture=tile,
|
||||
// tex_size=[10,10], tex_depth=5);
|
||||
// Example(3D,Med,VPT=[1.04269,4.35278,-0.716624],VPR=[98.4,0,43.9],VPD=175.268): Adding the angle parameter cuts off the extrusion. Note how each extruded component is capped.
|
||||
// shape = skin([rect(2/5),
|
||||
@@ -1045,7 +1107,7 @@ function linear_sweep(
|
||||
// caps=false);
|
||||
// tile = move([0,1/2,2/3],yrot(90,shape));
|
||||
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
|
||||
// rotate_sweep(path, closed=false, texture=tile,
|
||||
// rotate_sweep(path, caps=true, texture=tile,
|
||||
// tex_size=[10,15], tex_depth=5, angle=215);
|
||||
// Example(3D,NoAxes,Med,VPT=[1.00759,3.89216,-1.27032],VPR=[57.1,0,34.8],VPD=240.423): Turning the texture 90 degrees with `tex_rot` produces a texture that ends at the top and bottom.
|
||||
// shape = skin([rect(2/5),
|
||||
@@ -1056,7 +1118,7 @@ function linear_sweep(
|
||||
// caps=false);
|
||||
// tile = move([0,1/2,2/3],yrot(90,shape));
|
||||
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
|
||||
// rotate_sweep(path, closed=false, texture=tile, tex_rot=90,
|
||||
// rotate_sweep(path, caps=true, texture=tile, tex_rot=90,
|
||||
// tex_size=[12,8], tex_depth=9, angle=360);
|
||||
// Example(3D,Med,NoAxes,VPR=[78.1,0,199.3],VPT=[-4.55445,1.37814,-4.39897],VPD=192.044): A basket weave texture, here only halfway around the circle to avoid clutter.
|
||||
// diag_weave_vnf = [
|
||||
@@ -1077,7 +1139,7 @@ function linear_sweep(
|
||||
// ];
|
||||
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
|
||||
// down(31)linear_extrude(height=1)arc(r=23,angle=[0,180], wedge=true);
|
||||
// rotate_sweep(path, closed=false, texture=diag_weave_vnf, angle=180,
|
||||
// rotate_sweep(path, caps=true, texture=diag_weave_vnf, angle=180,
|
||||
// tex_size=[10,10], convexity=12, tex_depth=2);
|
||||
// Example(3D,VPR=[59.20,0.00,159.20],VPD=74.40,VPT=[7.45,6.83,1.54],NoAxes): Textures can be used to place images onto objects. If you want to place an image onto a cylinder you probably don't want it to cover the whole cylinder, or to create many small copies. To do this you can create a textured cylinder with an angle less than 360 degrees to hold the texture. In this example we calculate the angle so that the output has the same aspect ratio. The default `tex_extra` of zero for a single tile ensures that the image appears without an extra border.
|
||||
// img = [
|
||||
@@ -1097,9 +1159,8 @@ function linear_sweep(
|
||||
// h = 20;
|
||||
// r = 15;
|
||||
// ang = len(img[0])/len(img)*h/(2*PI*r)*360;
|
||||
// rotate_sweep([[15,-10],[15,10]], texture=img,
|
||||
// tex_reps=1,angle=ang, closed=false);
|
||||
//
|
||||
// rotate_sweep([[r,-h/2],[r,h/2]], texture=img,
|
||||
// tex_reps=1,angle=ang, caps=true);
|
||||
// Example(3D,VPR=[80.20,0.00,138.40],VPD=82.67,VPT=[6.88,7.29,1.77],NoAxes): Here we have combined the above model with a suitable cylinder. Note that with a coarse texture like this you need to either match the `$fn` of the cylinder to the texture, or choose a sufficiently fine cylinder to avoid conflicting facets.
|
||||
// img = [
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
@@ -1118,26 +1179,69 @@ function linear_sweep(
|
||||
// h = 20;
|
||||
// r = 15;
|
||||
// ang = len(img[0])/len(img)*h/(2*PI*r)*360;
|
||||
// rotate_sweep([[15,-10],[15,10]], texture=img,
|
||||
// tex_reps=1,angle=ang, closed=false);
|
||||
// rotate_sweep([[r,-h/2],[r,h/2]], texture=img,
|
||||
// tex_reps=1,angle=ang, caps=true);
|
||||
// cyl(r=r,h=27,$fn=128);
|
||||
// Example(3D,VPR=[68.30,0.00,148.90],VPD=91.85,VPT=[-0.56,5.78,-0.90],NoAxes): Above we explicitly calculated the required angle to produce the correct aspect ratio. Here we use `pixel_aspect` which produces an output whose average width has the desired aspect ratio.
|
||||
// img = [
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// ];
|
||||
// rotate_sweep([[15,-10],[5,10]], texture=img,
|
||||
// tex_reps=[1,1], caps=true, pixel_aspect=1);
|
||||
// cyl(r1=16,r2=4,h=24,$fn=128);
|
||||
// Example(3D,VPR=[96.30,0.00,133.50],VPD=54.24,VPT=[1.94,2.85,-0.47]): Here we apply the texture to a sphere using the automatic `pixel_aspect` to determine the angle. Note that using {{spheroid()}} with the circum option eliminates artifacts arising due to mimatched faceting.
|
||||
// img = [
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// ];
|
||||
// arc = arc(r=10, angle=[-44,44],n=100);
|
||||
// rotate_sweep(arc, texture=img, tex_reps=[1,1],
|
||||
// caps=true, pixel_aspect=1);
|
||||
// spheroid(10,$fn=64,circum=true);
|
||||
|
||||
|
||||
|
||||
function rotate_sweep(
|
||||
shape, angle=360,
|
||||
texture, tex_size=[5,5], tex_counts, tex_reps,
|
||||
tex_inset=false, tex_rot=0,
|
||||
tex_scale, tex_depth, tex_samples,
|
||||
tex_taper, shift=[0,0], closed=true,
|
||||
tex_scale, tex_depth, tex_samples, tex_aspect, pixel_aspect,
|
||||
tex_taper, shift=[0,0], caps, closed,
|
||||
style="min_edge", cp="centroid",
|
||||
atype="hull", anchor="origin",
|
||||
spin=0, orient=UP, start=0,
|
||||
_tex_inhibit_y_slicing=false
|
||||
_tex_inhibit_y_slicing
|
||||
) =
|
||||
assert(num_defined([closed,caps])<2, "In rotate_sweep the `closed` paramter has been replaced by `caps` with the opposite meaning. You cannot give both.")
|
||||
assert(num_defined([tex_reps,tex_counts])<2, "In rotate_sweep() the 'tex_counts' parameters has been replaced by 'tex_reps'. You cannot give both.")
|
||||
assert(num_defined([tex_scale,tex_depth])<2, "In linear_sweep() the 'tex_scale' parameter has been replaced by 'tex_depth'. You cannot give both.")
|
||||
assert(!is_path(shape) || !closed || len(path)>=3, "'shape' is a path and closed=true, but a closed path requires three points")
|
||||
let( tex_reps = is_def(tex_counts)? echo("In rotate_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
|
||||
assert(!is_path(shape) || caps || len(path)>=3, "'shape' is a path and caps=false, but a closed path requires three points")
|
||||
let(
|
||||
caps = is_def(caps) ? caps
|
||||
: is_def(closed) ? !closed
|
||||
: false,
|
||||
tex_reps = is_def(tex_counts)? echo("In rotate_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
|
||||
: tex_reps,
|
||||
tex_depth = is_def(tex_scale)? echo("In rotate_sweep() the 'tex_scale' parameter is deprecated and has been replaced by 'tex_depth'")tex_scale
|
||||
: default(tex_depth,1),
|
||||
@@ -1163,16 +1267,16 @@ function rotate_sweep(
|
||||
rot=tex_rot,
|
||||
samples=tex_samples,
|
||||
inhibit_y_slicing=_tex_inhibit_y_slicing,
|
||||
taper=tex_taper,
|
||||
taper=tex_taper, tex_aspect=tex_aspect, pixel_aspect=pixel_aspect,
|
||||
shift=shift,
|
||||
closed=closed,
|
||||
closed=!caps,
|
||||
angle=angle,
|
||||
style=style,
|
||||
start=start
|
||||
) :
|
||||
let(
|
||||
region = !is_path(shape) || closed ? region
|
||||
: [deduplicate([[0,shape[0].y], each shape, [0,last(shape).y]])],
|
||||
region = is_path(shape) && caps ? [deduplicate([[0,shape[0].y], each shape, [0,last(shape).y]])]
|
||||
: region,
|
||||
steps = ceil(segs(max_x) * angle / 360) + (angle<360? 1 : 0),
|
||||
skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y),
|
||||
transforms = [
|
||||
@@ -1197,7 +1301,7 @@ module rotate_sweep(
|
||||
tex_scale, tex_depth, tex_samples,
|
||||
tex_taper, shift=[0,0],
|
||||
style="min_edge",
|
||||
closed=true, tex_extra,
|
||||
caps, closed, tex_extra, tex_aspect, pixel_aspect,
|
||||
cp="centroid",
|
||||
convexity=10,
|
||||
atype="hull",
|
||||
@@ -1207,9 +1311,13 @@ module rotate_sweep(
|
||||
_tex_inhibit_y_slicing=false
|
||||
) {
|
||||
dummy =
|
||||
assert(num_defined([closed,caps])<2, "In rotate_sweep the `closed` paramter has been replaced by `caps` with the opposite meaning. You cannot give both.")
|
||||
assert(num_defined([tex_reps,tex_counts])<2, "In rotate_sweep() the 'tex_counts' parameters has been replaced by 'tex_reps'. You cannot give both.")
|
||||
assert(num_defined([tex_scale,tex_depth])<2, "In rotate_sweep() the 'tex_scale' parameter has been replaced by 'tex_depth'. You cannot give both.")
|
||||
assert(!is_path(shape) || !closed || len(shape)>=3, "'shape' is a path and closed=true, but a closed path requires three points");
|
||||
assert(!is_path(shape) || caps || len(shape)>=3, "'shape' is a path and caps=false, but a closed path requires three points");
|
||||
caps = is_def(caps) ? caps
|
||||
: is_def(closed) ? !closed
|
||||
: false;
|
||||
tex_reps = is_def(tex_counts)? echo("In rotate_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
|
||||
: tex_reps;
|
||||
tex_depth = is_def(tex_scale)? echo("In rotate_sweep() the 'tex_scale' parameter is deprecated and has been replaced by 'tex_depth'")tex_scale
|
||||
@@ -1234,17 +1342,17 @@ module rotate_sweep(
|
||||
rot=tex_rot,
|
||||
samples=tex_samples,
|
||||
taper=tex_taper,
|
||||
shift=shift,tex_extra=tex_extra,
|
||||
closed=closed,
|
||||
shift=shift,tex_extra=tex_extra,tex_aspect=tex_aspect, pixel_aspect=pixel_aspect,
|
||||
closed=!caps,
|
||||
inhibit_y_slicing=_tex_inhibit_y_slicing,
|
||||
angle=angle,
|
||||
style=style,
|
||||
atype=atype, anchor=anchor,
|
||||
atype=atype, anchor=anchor,
|
||||
spin=spin, orient=orient, start=start
|
||||
) children();
|
||||
} else {
|
||||
region = !is_path(shape) || closed ? region
|
||||
: [deduplicate([[0,shape[0].y], each shape, [0,last(shape).y]])];
|
||||
region = is_path(shape) && caps ? [deduplicate([[0,shape[0].y], each shape, [0,last(shape).y]])]
|
||||
: region;
|
||||
steps = ceil(segs(max_x) * angle / 360) + (angle<360? 1 : 0);
|
||||
skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y);
|
||||
transforms = [
|
||||
@@ -2562,7 +2670,8 @@ module sweep(shape, transforms, closed=false, caps, style="min_edge", convexity=
|
||||
// Note that {{path_sweep2d()}} does not support `sweep_attach()` because it doesn't compute the transform list, which is
|
||||
// the input used to calculate the attachment transform.
|
||||
// Arguments:
|
||||
// anchor = 2d anchor to the shape used in the path_sweep parent
|
||||
// parent = 2d anchor to the shape used in the path_sweep parent
|
||||
// child = optional 3d anchor for anchoring the child to the parent
|
||||
// frac = position along the path_sweep path as a fraction of total length
|
||||
// ---
|
||||
// idx = index into the path_sweep path (use instead of frac)
|
||||
@@ -4448,7 +4557,7 @@ function _tile_edge_path_list(vnf, axis, maxopen=1) =
|
||||
/// Arguments:
|
||||
/// shape = The path or region to sweep/extrude.
|
||||
/// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to the revolution surface. See {{texture()}} for what named textures are supported.
|
||||
/// tex_size = An optional 2D target size for the textures. Actual texture sizes are scaled somewhat to evenly fit the available surface. Default: `[5,5]`
|
||||
/// tex_size = An optional 2D target size for the textures. Actual texture sizes are scaled somewhat to evenly fit the available surface.
|
||||
/// tex_scale = Scaling multiplier for the texture depth.
|
||||
/// ---
|
||||
/// inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied. If `true`, insets by exactly `1`. Default: `false`
|
||||
@@ -4472,11 +4581,11 @@ function _textured_revolution(
|
||||
shape, texture, tex_size, tex_scale=1,
|
||||
inset=false, rot=false, shift=[0,0],
|
||||
taper, closed=true, angle=360,
|
||||
inhibit_y_slicing=false,
|
||||
inhibit_y_slicing,tex_aspect, pixel_aspect,
|
||||
counts, samples, start=0,tex_extra,
|
||||
style="min_edge", atype="intersect",
|
||||
anchor=CENTER, spin=0, orient=UP
|
||||
) =
|
||||
) =
|
||||
assert(angle>0 && angle<=360)
|
||||
assert(is_path(shape,[2]) || is_region(shape))
|
||||
assert(is_undef(samples) || is_int(samples))
|
||||
@@ -4484,11 +4593,13 @@ function _textured_revolution(
|
||||
assert(counts==undef || is_int(counts) || (all_integer(counts) && len(counts)==2), "tex_reps must be an integer or list of two integers")
|
||||
assert(tex_size==undef || is_vector(tex_size,2) || is_finite(tex_size))
|
||||
assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
|
||||
let( taper_is_ok = is_undef(taper) || (is_finite(taper) && taper>=0 && taper<50) || is_path(taper,2) )
|
||||
assert(taper_is_ok, "Bad taper= value.")
|
||||
assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
|
||||
assert(is_undef(tex_extra) || is_finite(tex_extra) || is_vector(tex_extra,2), "tex_extra must be a number of 2-vector")
|
||||
assert(num_defined([tex_aspect, pixel_aspect])<=1, "Cannot give both tex_aspect and pixel_aspect")
|
||||
assert(is_undef(taper) || !closed, "Cannot give tex_taper if caps=false")
|
||||
//assert(num_defined([tex_aspect, pixel_aspect])==0 || is_undef(angle), "Cannot give tex_aspect or pixel_aspect if you give angle")
|
||||
let(
|
||||
inhibit_y_slicing = default(inhibit_y_slicing, is_path(shape) && len(shape)==2 ? true : false),
|
||||
regions = !is_path(shape,2)? region_parts(shape)
|
||||
: closed? region_parts([shape])
|
||||
: let(
|
||||
@@ -4498,24 +4609,28 @@ function _textured_revolution(
|
||||
checks = [
|
||||
for (rgn=regions, path=rgn)
|
||||
assert(all(path, function(pt) pt.x>=0),"All points in the shape must have non-negative x value"),
|
||||
for(reg=regions, path=reg, edge=pair(path,wrap=closed))
|
||||
assert(edge[0].x>0 || edge[1].x>0,
|
||||
str("The shape cannot have any edges on the axis of rotation",closed?" (including the segment that closes the shape)":""))
|
||||
//for(reg=regions, path=reg, edge=pair(path,wrap=closed))
|
||||
// assert(edge[0].x>0 || edge[1].x>0,
|
||||
// str("The shape cannot have any edges on the axis of rotation",closed?" (including the segment that closes the shape)":""))
|
||||
]
|
||||
)
|
||||
assert(closed || is_path(shape,2), "closed=false is only allowed with paths")
|
||||
assert(closed || is_path(shape,2), "caps=true is only allowed with paths")
|
||||
let(
|
||||
counts = is_undef(counts) ? undef : force_list(counts,2),
|
||||
tex_size = force_list(tex_size,2),
|
||||
texture = _get_texture(texture, rot),
|
||||
tex_extra = is_vnf(texture) ? [1,1]
|
||||
: is_def(tex_extra) ? force_list(tex_extra,2)
|
||||
: counts==[1,1] ? [0,0]
|
||||
: [1,1],
|
||||
dummy = assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
|
||||
tex_extra_try = is_vnf(texture) ? [1,1]
|
||||
: is_def(tex_extra) ? force_list(tex_extra,2)
|
||||
: counts==[1,1] ? [0,0]
|
||||
: [1,1],
|
||||
tex_extra = angle==360 ? [1,tex_extra_try.y] : tex_extra_try,
|
||||
dummy = assert(is_def(counts) || num_defined([pixel_aspect,tex_aspect])==0, "Must specify tex_counts (not tex_size) when using pixel_aspect or tex_aspect")
|
||||
assert(is_undef(pixel_aspect) || !is_vnf(texture), "Cannot give pixel_aspect with a VNF texture")
|
||||
assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
|
||||
inset = is_num(inset)? inset : inset? 1 : 0,
|
||||
samples = !is_vnf(texture)? len(texture) :
|
||||
is_num(samples)? samples : 8,
|
||||
samples = !is_vnf(texture)? len(texture)
|
||||
: is_num(samples)? samples
|
||||
: 8,
|
||||
bounds = pointlist_bounds(flatten(flatten(regions))),
|
||||
maxx = bounds[1].x,
|
||||
miny = bounds[0].y,
|
||||
@@ -4523,6 +4638,20 @@ function _textured_revolution(
|
||||
h = maxy - miny,
|
||||
circumf = 2 * PI * maxx,
|
||||
texcnt = is_vnf(texture) ? undef : [len(texture[0]), len(texture)],
|
||||
angle = num_defined([tex_aspect,pixel_aspect])==0 ? angle
|
||||
: let(
|
||||
paths = flatten(regions),
|
||||
lengths = [for(path=paths) path_length(path,closed=closed)],
|
||||
ind = max_index(lengths),
|
||||
rpath = resample_path(paths[ind], n=counts.y * samples + (closed?0:tex_extra.y), closed=closed),
|
||||
h = path_length(rpath),
|
||||
r = mean(column(rpath,0)),
|
||||
width = counts.x/counts.y * (is_def(pixel_aspect) ? (texcnt.x+tex_extra.x-1)/(texcnt.y+tex_extra.y-1) : tex_aspect) * h + (is_def(pixel_aspect)?1:0),
|
||||
ang = 360 * width / (2*PI*r)
|
||||
)
|
||||
assert(ang<=360, str("Angle required for requested tile counts and aspect is ",ang, " which exceeds 360 degrees."))
|
||||
360 * width / (2*PI*r),
|
||||
|
||||
tile = !is_vnf(texture) || samples==1 ? texture
|
||||
:
|
||||
let(
|
||||
@@ -4541,17 +4670,39 @@ function _textured_revolution(
|
||||
counts_x = is_def(counts)? counts.x : max(1,round(angle/360*circumf/tex_size.x)),
|
||||
adj_angle = is_vnf(texture)?angle
|
||||
: angle*(1-(tex_extra.x-1)/(texcnt.x*counts_x+tex_extra.x-1)), // adjusted angle for strip positions taking tex_extra into account
|
||||
taper_lup = closed || is_undef(taper)? [[-1,1],[2,1]] :
|
||||
is_num(taper)? [[-1,0], [0,0], [taper/100+EPSILON,1], [1-taper/100-EPSILON,1], [1,0], [2,0]] :
|
||||
is_path(taper,2)? let(
|
||||
retaper = [
|
||||
for (t=taper)
|
||||
assert(t[0]>=0 && t[0]<=100, "taper lookup indices must be between 0 and 100 inclusive.")
|
||||
[t[0]/100, t[1]]
|
||||
],
|
||||
taperout = [[-1,retaper[0][1]], each retaper, [2,last(retaper)[1]]]
|
||||
) taperout :
|
||||
assert(false, "Bad taper= argument value."),
|
||||
taperfunc = closed || is_undef(taper)? function (x) 1
|
||||
: is_finite(taper)?
|
||||
let(
|
||||
taper = taper<=1 ? taper
|
||||
: echo("The tex_taper now uses a value from 0-1. Your entry was larger than 1 and has been scaled by 1/100.")
|
||||
taper/100
|
||||
)
|
||||
assert(taper>=0 && taper<=0.5, str("tex_taper must be between 0 and 0.5 but was ",taper))
|
||||
function (x) lookup(x, [[0,0],
|
||||
if (taper==0.5) [taper,1]
|
||||
else each [[taper+EPSILON,1],[1-taper-EPSILON,1]],
|
||||
[1,0]])
|
||||
: is_path(taper,2) ?
|
||||
let(
|
||||
taper = max(column(taper,0)) <= 1 ? taper
|
||||
: assert("The tex_taper table now uses values from 0-1. Your entry was larger than 1 and has been scaled by 1/100.")
|
||||
xscale(1/100,taper)
|
||||
)
|
||||
function(x) lookup(x,taper)
|
||||
: is_function(taper) ? taper
|
||||
: assert(false,"tex_taper must be a function, scalar or list of pairs"),
|
||||
// Checks a path to see if it has segments on the Y axis. More than 1 is an error. If no segments return
|
||||
// path unchanged with closed=true. If there is 1 segment, delete that segment (by rotating the path so it's
|
||||
// at the end) and return closed=false. This prevents textures from continuing into the inside of a shape.
|
||||
open_axis_paths = function(path,closed)
|
||||
!closed ? [path,closed]
|
||||
: let(
|
||||
axind = [for(i=[0:1:len(path)-1]) if (approx(path[i].x,0) && approx(select(path,i+1).x,0)) i],
|
||||
dummy = assert(len(axind)<=1, "Found path with more than 1 segment on the Y axis, which is not supported with texturing.")
|
||||
)
|
||||
len(axind)==0 ? [path,true]
|
||||
:
|
||||
[list_rotate(path, (axind[0]+1)%len(path)), false],
|
||||
transform_point = function(tileind, tilez, counts_y, bases, norms)
|
||||
let(
|
||||
part = tileind * samples,
|
||||
@@ -4559,7 +4710,7 @@ function _textured_revolution(
|
||||
frac = part - ind,
|
||||
base = lerp(select(bases,ind), select(bases,ind+1), frac),
|
||||
norm = unit(lerp(select(norms,ind), select(norms,ind+1), frac)),
|
||||
scale = tex_scale * lookup(tileind/counts_y, taper_lup) * base.x/maxx,
|
||||
scale = tex_scale * taperfunc(1-tileind/counts_y) * base.x/maxx,
|
||||
texh = scale<0 ? -(1-tilez - inset) * scale
|
||||
: (tilez - inset) * scale
|
||||
)
|
||||
@@ -4568,6 +4719,9 @@ function _textured_revolution(
|
||||
for (rgn = regions) let(
|
||||
rgn_wall_vnf = vnf_join(
|
||||
[for (path = rgn) let(
|
||||
path_closed = open_axis_paths(path,closed),
|
||||
path = path_closed[0],
|
||||
closed = path_closed[1],
|
||||
plen = path_length(path, closed=closed),
|
||||
counts_y = is_def(counts) ? counts.y : max(1,round(plen/tex_size.y)),
|
||||
obases = resample_path(path, n=counts_y * samples + (closed?0:tex_extra.y), closed=closed),
|
||||
@@ -4625,6 +4779,9 @@ function _textured_revolution(
|
||||
cap_rgn = side_open_path == [] ? []
|
||||
: [ for (path = rgn)
|
||||
let(
|
||||
path_closed = open_axis_paths(path,closed),
|
||||
path = path_closed[0],
|
||||
closed = path_closed[1],
|
||||
plen = path_length(path, closed=closed),
|
||||
counts_y = is_def(counts) ? counts.y : max(1,round(plen/tex_size.y)),
|
||||
bases = resample_path(path, n=counts_y * samples + (closed?0:tex_extra.y), closed=closed),
|
||||
@@ -4658,6 +4815,10 @@ function _textured_revolution(
|
||||
: [for(i=[0,1]) vnf_from_region(column(cap_rgn,i), rot([90,0,i*angle]), reverse=i==1)],
|
||||
extra_paths = side_closed_paths==[] ? []
|
||||
: [for (path = rgn) let(
|
||||
path_closed = open_axis_paths(path,closed),
|
||||
path = path_closed[0],
|
||||
closed = path_closed[1],
|
||||
|
||||
plen = path_length(path, closed=closed),
|
||||
counts_y = is_def(counts) ? counts.y : max(1,round(plen/tex_size.y)),
|
||||
bases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
|
||||
@@ -4676,9 +4837,12 @@ function _textured_revolution(
|
||||
) vnf_join(concat(cap_vnfs, extra_vnfs)),
|
||||
endcaps_vnf = closed? EMPTY_VNF :
|
||||
let(
|
||||
plen = path_length(rgn[0], closed=closed),
|
||||
path_closed = open_axis_paths(rgn[0],closed),
|
||||
path = path_closed[0],
|
||||
closed = path_closed[1],
|
||||
plen = path_length(path, closed=closed),
|
||||
counts_y = is_def(counts) ? counts.y : max(1,round(plen/tex_size.y)),
|
||||
obases = resample_path(rgn[0], n=counts_y * samples + (closed?0:1), closed=closed),
|
||||
obases = resample_path(path, n=counts_y * samples + (closed?0:tex_extra.y), closed=closed),
|
||||
onorms = path_normals(obases, closed=closed),
|
||||
bases = xrot(90, p=path3d(obases)),
|
||||
norms = xrot(90, p=path3d(onorms)),
|
||||
@@ -4696,7 +4860,7 @@ function _textured_revolution(
|
||||
ppath = [
|
||||
for (vert = epath) let(
|
||||
uang = vert.x / counts_x,
|
||||
tex_scale = tex_scale * lookup(j+1, taper_lup),
|
||||
tex_scale = tex_scale * taperfunc(j+1),
|
||||
texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
|
||||
: (vert.z - inset) * tex_scale * (base.x / maxx),
|
||||
xyz = base - norm * texh
|
||||
@@ -4714,7 +4878,7 @@ function _textured_revolution(
|
||||
ppath = [
|
||||
for (vert = bpath[j+1]) let(
|
||||
uang = vert.x / counts_x,
|
||||
tex_scale = tex_scale * lookup(j+1, taper_lup),
|
||||
tex_scale = tex_scale * taperfunc(j+1),
|
||||
texh = tex_scale<0 ? -(1-vert.y - inset) * tex_scale * (base.x / maxx)
|
||||
: (vert.y - inset) * tex_scale * (base.x / maxx),
|
||||
xyz = base - norm * texh
|
||||
@@ -4764,7 +4928,7 @@ module _textured_revolution(
|
||||
shape, texture, tex_size, tex_scale=1,
|
||||
inset=false, rot=false, shift=[0,0],
|
||||
taper, closed=true, angle=360,
|
||||
style="min_edge", atype="intersect",
|
||||
style="min_edge", atype="intersect",tex_aspect, pixel_aspect,
|
||||
inhibit_y_slicing=false,tex_extra,
|
||||
convexity=10, counts, samples, start=0,
|
||||
anchor=CENTER, spin=0, orient=UP
|
||||
@@ -4773,7 +4937,7 @@ module _textured_revolution(
|
||||
vnf = _textured_revolution(
|
||||
shape, texture, tex_size=tex_size,
|
||||
tex_scale=tex_scale, inset=inset, rot=rot,
|
||||
taper=taper, closed=closed, style=style,
|
||||
taper=taper, closed=closed, style=style,tex_aspect=tex_aspect, pixel_aspect=pixel_aspect,
|
||||
shift=shift, angle=angle,tex_extra=tex_extra,
|
||||
samples=samples, counts=counts, start=start,
|
||||
inhibit_y_slicing=inhibit_y_slicing
|
||||
@@ -4792,7 +4956,7 @@ function _textured_point_array(points, texture, tex_reps, tex_size, tex_samples,
|
||||
col_wrap=false, tex_depth=1, row_wrap=false, caps, cap1, cap2, reverse=false, style="min_edge", tex_extra, tex_skip, sidecaps,sidecap1,sidecap2,normals) =
|
||||
assert(tex_reps==undef || is_int(tex_reps) || (all_integer(tex_reps) && len(tex_reps)==2), "tex_reps must be an integer or list of two integers")
|
||||
assert(tex_size==undef || is_num(tex_size) || is_vector(tex_size,2), "tex_size must be a scalar or 2-vector")
|
||||
assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps")
|
||||
assert(num_defined([tex_size, tex_reps])==1, "Must give exactly one of tex_size and tex_reps")
|
||||
assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"]))
|
||||
assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d")
|
||||
assert(is_consistent(points), "Non-rectangular or invalid point array")
|
||||
@@ -4808,7 +4972,7 @@ function _textured_point_array(points, texture, tex_reps, tex_size, tex_samples,
|
||||
ptsize=[len(points[0]), len(points)],
|
||||
tex_reps = is_def(tex_reps) ? force_list(tex_reps,2)
|
||||
: let(
|
||||
tex_size = is_undef(tex_size) ? [5,5] : force_list(tex_size,2),
|
||||
tex_size = force_list(tex_size,2),
|
||||
xsize = norm(points[0][0]-points[0][1])*(ptsize.x+(col_wrap?1:0)),
|
||||
ysize = norm(points[0][0]-points[1][0])*(ptsize.y+(row_wrap?1:0))
|
||||
)
|
||||
|
130
strings.scad
130
strings.scad
@@ -28,11 +28,11 @@ function _is_liststr(s) = is_list(s) || is_str(s);
|
||||
// pos = starting index of substring, or vector of first and last position. Default: 0
|
||||
// len = length of substring, or omit it to get the rest of the string. If len is zero or less then the emptry string is returned.
|
||||
// Example:
|
||||
// substr("abcdefg",3,3); // Returns "def"
|
||||
// substr("abcdefg",2); // Returns "cdefg"
|
||||
// substr("abcdefg",len=3); // Returns "abc"
|
||||
// substr("abcdefg",[2,4]); // Returns "cde"
|
||||
// substr("abcdefg",len=-2); // Returns ""
|
||||
// s1=substr("abcdefg",3,3); // Returns "def"
|
||||
// s2=substr("abcdefg",2); // Returns "cdefg"
|
||||
// s3=substr("abcdefg",len=3); // Returns "abc"
|
||||
// s4=substr("abcdefg",[2,4]); // Returns "cde"
|
||||
// s5=substr("abcdefg",len=-2); // Returns ""
|
||||
function substr(str, pos=0, len=undef) =
|
||||
assert(is_string(str))
|
||||
is_list(pos) ? _substr(str, pos[0], pos[1]-pos[0]+1) :
|
||||
@@ -84,20 +84,20 @@ function suffix(str,len) =
|
||||
// all = set to true to return all matches as a list. Overrides last. Default: false
|
||||
// start = index where the search starts
|
||||
// Example:
|
||||
// str_find("abc123def123abc","123"); // Returns 3
|
||||
// str_find("abc123def123abc","b"); // Returns 1
|
||||
// str_find("abc123def123abc","1234"); // Returns undef
|
||||
// str_find("abc",""); // Returns 0
|
||||
// str_find("abc123def123", "123", start=4); // Returns 9
|
||||
// str_find("abc123def123abc","123",last=true); // Returns 9
|
||||
// str_find("abc123def123abc","b",last=true); // Returns 13
|
||||
// str_find("abc123def123abc","1234",last=true); // Returns undef
|
||||
// str_find("abc","",last=true); // Returns 3
|
||||
// str_find("abc123def123", "123", start=8, last=true)); // Returns 3
|
||||
// str_find("abc123def123abc","123",all=true); // Returns [3,9]
|
||||
// str_find("abc123def123abc","b",all=true); // Returns [1,13]
|
||||
// str_find("abc123def123abc","1234",all=true); // Returns []
|
||||
// str_find("abc","",all=true); // Returns [0,1,2]
|
||||
// a=str_find("abc123def123abc","123"); // Returns 3
|
||||
// b=str_find("abc123def123abc","b"); // Returns 1
|
||||
// c=str_find("abc123def123abc","1234"); // Returns undef
|
||||
// d=str_find("abc",""); // Returns 0
|
||||
// e=str_find("abc123def123", "123", start=4); // Returns 9
|
||||
// f=str_find("abc123def123abc","123",last=true); // Returns 9
|
||||
// g=str_find("abc123def123abc","b",last=true); // Returns 13
|
||||
// h=str_find("abc123def123abc","1234",last=true); // Returns undef
|
||||
// i=str_find("abc","",last=true); // Returns 3
|
||||
// j=str_find("abc123def123", "123", start=8, last=true)); // Returns 3
|
||||
// k=str_find("abc123def123abc","123",all=true); // Returns [3,9]
|
||||
// l=str_find("abc123def123abc","b",all=true); // Returns [1,13]
|
||||
// m=str_find("abc123def123abc","1234",all=true); // Returns []
|
||||
// n=str_find("abc","",all=true); // Returns [0,1,2]
|
||||
function str_find(str,pattern,start=undef,last=false,all=false) =
|
||||
assert(_is_liststr(str), "str must be a string or list")
|
||||
assert(_is_liststr(pattern), "pattern must be a string or list")
|
||||
@@ -136,13 +136,13 @@ function _str_find_all(str,pattern) =
|
||||
// str = String to search
|
||||
// start = Starting index for search in str
|
||||
// pattern = String pattern to search for
|
||||
// Examples:
|
||||
// substr_match("abcde",2,"cd"); // Returns true
|
||||
// substr_match("abcde",2,"cx"); // Returns false
|
||||
// substr_match("abcde",2,"cdef"); // Returns false
|
||||
// substr_match("abcde",-2,"cd"); // Returns false
|
||||
// substr_match("abcde",19,"cd"); // Returns false
|
||||
// substr_match("abc",1,""); // Returns true
|
||||
// Example:
|
||||
// a=substr_match("abcde",2,"cd"); // Returns true
|
||||
// b=substr_match("abcde",2,"cx"); // Returns false
|
||||
// c=substr_match("abcde",2,"cdef"); // Returns false
|
||||
// d=substr_match("abcde",-2,"cd"); // Returns false
|
||||
// e=substr_match("abcde",19,"cd"); // Returns false
|
||||
// f=substr_match("abc",1,""); // Returns true
|
||||
|
||||
//
|
||||
// This is carefully optimized for speed. Precomputing the length
|
||||
@@ -173,9 +173,9 @@ function _substr_match_recurse(str,sindex,pattern,plen,pindex=0,) =
|
||||
// str = String to search.
|
||||
// pattern = String pattern to search for.
|
||||
// Example:
|
||||
// starts_with("abcdef","abc"); // Returns true
|
||||
// starts_with("abcdef","def"); // Returns false
|
||||
// starts_with("abcdef",""); // Returns true
|
||||
// b1=starts_with("abcdef","abc"); // Returns true
|
||||
// b2=starts_with("abcdef","def"); // Returns false
|
||||
// b3=starts_with("abcdef",""); // Returns true
|
||||
function starts_with(str,pattern) = _is_liststr(str) && substr_match(str,0,pattern);
|
||||
|
||||
|
||||
@@ -192,9 +192,9 @@ function starts_with(str,pattern) = _is_liststr(str) && substr_match(str,0,patte
|
||||
// str = String to search.
|
||||
// pattern = String pattern to search for.
|
||||
// Example:
|
||||
// ends_with("abcdef","def"); // Returns true
|
||||
// ends_with("abcdef","de"); // Returns false
|
||||
// ends_with("abcdef",""); // Returns true
|
||||
// b1=ends_with("abcdef","def"); // Returns true
|
||||
// b2=ends_with("abcdef","de"); // Returns false
|
||||
// b3=ends_with("abcdef",""); // Returns true
|
||||
function ends_with(str,pattern) = _is_liststr(str) && substr_match(str,len(str)-len(pattern),pattern);
|
||||
|
||||
|
||||
@@ -220,12 +220,12 @@ function ends_with(str,pattern) = _is_liststr(str) && substr_match(str,len(str)-
|
||||
// sep = a string or list of strings to use for the separator
|
||||
// keep_nulls = boolean value indicating whether to keep null strings in the output list. Default: true
|
||||
// Example:
|
||||
// str_split("abc+def-qrs*iop","*-+"); // Returns ["abc", "def", "qrs", "iop"]
|
||||
// str_split("abc+*def---qrs**iop+","*-+");// Returns ["abc", "", "def", "", "", "qrs", "", "iop", ""]
|
||||
// str_split("abc def"," "); // Returns ["abc", "", "", "", "", "", "def"]
|
||||
// str_split("abc def"," ",keep_nulls=false); // Returns ["abc", "def"]
|
||||
// str_split("abc+def-qrs*iop",["+","-","*"]); // Returns ["abc", "def", "qrs", "iop"]
|
||||
// str_split("abc+def-qrs*iop",["-","+","*"]); // Returns ["abc+def", "qrs*iop", "", ""]
|
||||
// s1=str_split("abc+def-qrs*iop","*-+"); // Returns ["abc", "def", "qrs", "iop"]
|
||||
// s2=str_split("abc+*def---qrs**iop+","*-+");// Returns ["abc", "", "def", "", "", "qrs", "", "iop", ""]
|
||||
// s3=str_split("abc def"," "); // Returns ["abc", "", "", "", "", "", "def"]
|
||||
// s4=str_split("abc def"," ",keep_nulls=false); // Returns ["abc", "def"]
|
||||
// s5=str_split("abc+def-qrs*iop",["+","-","*"]); // Returns ["abc", "def", "qrs", "iop"]
|
||||
// s6=str_split("abc+def-qrs*iop",["-","+","*"]); // Returns ["abc+def", "qrs*iop", "", ""]
|
||||
function str_split(str,sep,keep_nulls=true) =
|
||||
!keep_nulls ? _remove_empty_strs(str_split(str,sep,keep_nulls=true)) :
|
||||
is_list(sep) ? _str_split_recurse(str,sep,i=0,result=[]) :
|
||||
@@ -265,8 +265,8 @@ function _remove_empty_strs(list) =
|
||||
// list = list of strings to concatenate
|
||||
// sep = separator string to insert. Default: ""
|
||||
// Example:
|
||||
// str_join(["abc","def","ghi"]); // Returns "abcdefghi"
|
||||
// str_join(["abc","def","ghi"], " + "); // Returns "abc + def + ghi"
|
||||
// s1=str_join(["abc","def","ghi"]); // Returns "abcdefghi"
|
||||
// s2=str_join(["abc","def","ghi"], " + "); // Returns "abc + def + ghi"
|
||||
function str_join(list,sep="",_i=0, _result="") =
|
||||
assert(is_list(list))
|
||||
_i >= len(list)-1 ? (_i==len(list) ? _result : str(_result,list[_i])) :
|
||||
@@ -285,22 +285,22 @@ function str_join(list,sep="",_i=0, _result="") =
|
||||
// Takes a string `s` and strips off all leading and/or trailing characters that exist in string `c`.
|
||||
// By default strips both leading and trailing characters. If you set start or end to true then
|
||||
// it will strip only the leading or trailing characters respectively. If you set start
|
||||
// or end to false then it will strip only lthe trailing or leading characters.
|
||||
// or end to false then it will strip only the trailing or leading characters.
|
||||
// Arguments:
|
||||
// s = The string to strip leading or trailing characters from.
|
||||
// c = The string of characters to strip.
|
||||
// start = if true then strip leading characters
|
||||
// end = if true then strip trailing characters
|
||||
// Example:
|
||||
// str_strip("--##--123--##--","#-"); // Returns: "123"
|
||||
// str_strip("--##--123--##--","-"); // Returns: "##--123--##"
|
||||
// str_strip("--##--123--##--","#"); // Returns: "--##--123--##--"
|
||||
// str_strip("--##--123--##--","#-",end=true); // Returns: "--##--123"
|
||||
// str_strip("--##--123--##--","-",end=true); // Returns: "--##--123--##"
|
||||
// str_strip("--##--123--##--","#",end=true); // Returns: "--##--123--##--"
|
||||
// str_strip("--##--123--##--","#-",start=true); // Returns: "123--##--"
|
||||
// str_strip("--##--123--##--","-",start=true); // Returns: "##--123--##--"
|
||||
// str_strip("--##--123--##--","#",start=true); // Returns: "--##--123--##--"
|
||||
// s1=str_strip("--##--123--##--","#-"); // Returns: "123"
|
||||
// s2=str_strip("--##--123--##--","-"); // Returns: "##--123--##"
|
||||
// s3=str_strip("--##--123--##--","#"); // Returns: "--##--123--##--"
|
||||
// s4=str_strip("--##--123--##--","#-",end=true); // Returns: "--##--123"
|
||||
// s5=str_strip("--##--123--##--","-",end=true); // Returns: "--##--123--##"
|
||||
// s6=str_strip("--##--123--##--","#",end=true); // Returns: "--##--123--##--"
|
||||
// s7=str_strip("--##--123--##--","#-",start=true); // Returns: "123--##--"
|
||||
// s8=str_strip("--##--123--##--","-",start=true); // Returns: "##--123--##--"
|
||||
// s9=str_strip("--##--123--##--","#",start=true); // Returns: "--##--123--##--"
|
||||
|
||||
function _str_count_leading(s,c,_i=0) =
|
||||
(_i>=len(s)||!in_list(s[_i],[each c]))? _i :
|
||||
@@ -337,6 +337,10 @@ function str_strip(s,c,start,end) =
|
||||
// length = length to pad to
|
||||
// char = character to pad with. Default: " " (space)
|
||||
// left = if true, pad on the left side. Default: false
|
||||
// Example:
|
||||
// s1=str_pad("hello", 10, "*"); // Returns: "hello*****"
|
||||
// s2=str_pad("hello", 10, "*", left=true); // Returns: "*****hello"
|
||||
|
||||
function str_pad(str,length,char=" ",left=false) =
|
||||
assert(is_str(str))
|
||||
assert(is_str(char) && len(char)==1, "char must be a single character string")
|
||||
@@ -349,14 +353,22 @@ function str_pad(str,length,char=" ",left=false) =
|
||||
|
||||
|
||||
// Function: str_replace_char()
|
||||
// Synopsis: Replace given chars in a string with another substring.
|
||||
// Synopsis: Replace specified character in a string with a string.
|
||||
// Topics: Strings
|
||||
// See Also: suffix(), str_find(), substr_match(), starts_with(), ends_with(), str_split(), str_join(), str_strip()
|
||||
// Usage:
|
||||
// newstr = str_replace_char(str, char, replace);
|
||||
// Description:
|
||||
// Replace every occurence of `char` in the input string with the string `replace` which
|
||||
// can be any string.
|
||||
// Replace every occurence of `char` (a single character string) in the input string
|
||||
// with the string `replace` which can be any string.
|
||||
// Arguments:
|
||||
// str = string to process
|
||||
// char = single character string to search for
|
||||
// replace = string that replaces all copies of `char`
|
||||
// Example:
|
||||
// s1 = str_replace_char("abcdcba","c","_123_"); // Returns: "ab123d123ba"
|
||||
// s2 = str_replace_char(" s t r i n g ", " ", ""); // Returns: "string"
|
||||
|
||||
function str_replace_char(str,char,replace) =
|
||||
assert(is_str(str))
|
||||
assert(is_str(char) && len(char)==1, "Search pattern 'char' must be a single character string")
|
||||
@@ -376,7 +388,7 @@ function str_replace_char(str,char,replace) =
|
||||
// Arguments:
|
||||
// str = String to convert.
|
||||
// Example:
|
||||
// downcase("ABCdef"); // Returns "abcdef"
|
||||
// s=downcase("ABCdef"); // Returns "abcdef"
|
||||
function downcase(str) =
|
||||
assert(is_string(str))
|
||||
str_join([for(char=str) let(code=ord(char)) code>=65 && code<=90 ? chr(code+32) : char]);
|
||||
@@ -394,7 +406,7 @@ function downcase(str) =
|
||||
// Arguments:
|
||||
// str = String to convert.
|
||||
// Example:
|
||||
// upcase("ABCdef"); // Returns "ABCDEF"
|
||||
// s=upcase("ABCdef"); // Returns "ABCDEF"
|
||||
function upcase(str) =
|
||||
assert(is_string(str))
|
||||
str_join([for(char=str) let(code=ord(char)) code>=97 && code<=122 ? chr(code-32) : char]);
|
||||
@@ -412,8 +424,12 @@ function upcase(str) =
|
||||
// Produce a random string of length `n`. If you give a string `charset` then the
|
||||
// characters of the random string are drawn from that list, weighted by the number
|
||||
// of times each character appears in the list. If you do not give a character set
|
||||
// then the string is generated with characters ranging from 0 to z (based on
|
||||
// then the string is generated with characters ranging from "0" to "z" (based on
|
||||
// character code).
|
||||
// Arguments:
|
||||
// n = number of characters to produce
|
||||
// charset = string to draw the characters from. Default: characters from "0" to "z".
|
||||
// seed = random number seed
|
||||
function rand_str(n, charset, seed) =
|
||||
is_undef(charset)? str_join([for(c=rand_int(48,122,n,seed)) chr(c)])
|
||||
: str_join([for(i=rand_int(0,len(charset)-1,n,seed)) charset[i]]);
|
||||
@@ -560,6 +576,8 @@ function parse_frac(str,mixed=true,improper=true,signed=true) =
|
||||
// Description:
|
||||
// Converts a string to a number. The string can be either a fraction (two integers separated by a "/") or a floating point number.
|
||||
// Returns NaN if the conversion fails.
|
||||
// Arguments:
|
||||
// str = string to process
|
||||
// Example:
|
||||
// parse_num("3/4"); // Returns 0.75
|
||||
// parse_num("3.4e-2"); // Returns 0.034
|
||||
|
271
vectors.scad
271
vectors.scad
@@ -58,7 +58,7 @@ function is_vector(v, length, zero, all_nonzero=false, eps=EPSILON) =
|
||||
// Function: add_scalar()
|
||||
// Synopsis: Adds a scalar value to every item in a vector.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: add_scalar(), v_mul(), v_div()
|
||||
// See Also: v_mul(), v_div()
|
||||
// Usage:
|
||||
// v_new = add_scalar(v, s);
|
||||
// Description:
|
||||
@@ -69,35 +69,35 @@ function is_vector(v, length, zero, all_nonzero=false, eps=EPSILON) =
|
||||
// Example:
|
||||
// a = add_scalar([1,2,3],3); // Returns: [4,5,6]
|
||||
function add_scalar(v,s) =
|
||||
assert(is_vector(v), "Input v must be a vector")
|
||||
assert(is_finite(s), "Input s must be a finite scalar")
|
||||
assert(is_vector(v), "\nInput v must be a vector.")
|
||||
assert(is_finite(s), "\nInput s must be a finite scalar.")
|
||||
[for(entry=v) entry+s];
|
||||
|
||||
|
||||
// Function: v_mul()
|
||||
// Synopsis: Returns the element-wise multiplication of two equal-length vectors.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: add_scalar(), v_mul(), v_div()
|
||||
// See Also: add_scalar(), v_div()
|
||||
// Usage:
|
||||
// v3 = v_mul(v1, v2);
|
||||
// Description:
|
||||
// Element-wise multiplication. Multiplies each element of `v1` by the corresponding element of `v2`.
|
||||
// Both `v1` and `v2` must be the same length. Returns a vector of the products. Note that
|
||||
// the items in `v1` and `v2` can be anything that OpenSCAD will multiply.
|
||||
// Both `v1` and `v2` must be the same length. Returns a vector of the products.
|
||||
// The items in `v1` and `v2` can be anything that OpenSCAD can multiply together.
|
||||
// Arguments:
|
||||
// v1 = The first vector.
|
||||
// v2 = The second vector.
|
||||
// Example:
|
||||
// v_mul([3,4,5], [8,7,6]); // Returns [24, 28, 30]
|
||||
function v_mul(v1, v2) =
|
||||
assert( is_list(v1) && is_list(v2) && len(v1)==len(v2), "Incompatible input")
|
||||
assert( is_list(v1) && is_list(v2) && len(v1)==len(v2), "\nIncompatible input.")
|
||||
[for (i = [0:1:len(v1)-1]) v1[i]*v2[i]];
|
||||
|
||||
|
||||
// Function: v_div()
|
||||
// Synopsis: Returns the element-wise division of two equal-length vectors.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: add_scalar(), v_mul(), v_div()
|
||||
// See Also: add_scalar(), v_mul()
|
||||
// Usage:
|
||||
// v3 = v_div(v1, v2);
|
||||
// Description:
|
||||
@@ -109,14 +109,14 @@ function v_mul(v1, v2) =
|
||||
// Example:
|
||||
// v_div([24,28,30], [8,7,6]); // Returns [3, 4, 5]
|
||||
function v_div(v1, v2) =
|
||||
assert( is_vector(v1) && is_vector(v2,len(v1)), "Incompatible vectors")
|
||||
assert( is_vector(v1) && is_vector(v2,len(v1)), "\nIncompatible vectors.")
|
||||
[for (i = [0:1:len(v1)-1]) v1[i]/v2[i]];
|
||||
|
||||
|
||||
// Function: v_abs()
|
||||
// Synopsis: Returns the absolute values of the given vector.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// See Also: v_ceil(), v_floor(), v_round()
|
||||
// Usage:
|
||||
// v2 = v_abs(v);
|
||||
// Description: Returns a vector of the absolute value of each element of vector `v`.
|
||||
@@ -125,40 +125,53 @@ function v_div(v1, v2) =
|
||||
// Example:
|
||||
// v_abs([-1,3,-9]); // Returns: [1,3,9]
|
||||
function v_abs(v) =
|
||||
assert( is_vector(v), "Invalid vector" )
|
||||
assert( is_vector(v), "\nInvalid vector." )
|
||||
[for (x=v) abs(x)];
|
||||
|
||||
|
||||
// Function: v_floor()
|
||||
// Synopsis: Returns the values of the given vector, rounded down.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// Usage:
|
||||
// v2 = v_floor(v);
|
||||
// Description:
|
||||
// Returns the given vector after performing a `floor()` on all items.
|
||||
function v_floor(v) =
|
||||
assert( is_vector(v), "Invalid vector" )
|
||||
[for (x=v) floor(x)];
|
||||
|
||||
|
||||
// Function: v_ceil()
|
||||
// Synopsis: Returns the values of the given vector, rounded up.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// See Also: v_abs(), v_floor(), v_round()
|
||||
// Usage:
|
||||
// v2 = v_ceil(v);
|
||||
// Description:
|
||||
// Returns the given vector after performing a `ceil()` on all items.
|
||||
function v_ceil(v) =
|
||||
assert( is_vector(v), "Invalid vector" )
|
||||
assert(is_vector(v), "\nInvalid vector." )
|
||||
[for (x=v) ceil(x)];
|
||||
|
||||
|
||||
// Function: v_floor()
|
||||
// Synopsis: Returns the values of the given vector, rounded down.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_ceil(), v_round()
|
||||
// Usage:
|
||||
// v2 = v_floor(v);
|
||||
// Description:
|
||||
// Returns the given vector after performing a `floor()` on all items.
|
||||
function v_floor(v) =
|
||||
assert(is_vector(v), "\nInvalid vector." )
|
||||
[for (x=v) floor(x)];
|
||||
|
||||
|
||||
// Function: v_round()
|
||||
// Synopsis: Returns the values of the given vector, rounded to the nearest whole number.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// Usage:
|
||||
// v2 = v_round(v);
|
||||
// Description:
|
||||
// Returns the given vector after performing a `round()` on all items.
|
||||
function v_round(v) =
|
||||
assert(is_vector(v), "\nInvalid vector." )
|
||||
[for (x=v) round(x)];
|
||||
|
||||
|
||||
// Function: v_lookup()
|
||||
// Synopsis: Like `lookup()`, but it can interpolate between vector results.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// See Also: v_abs(), v_floor(), v_ceil(), v_round()
|
||||
// Usage:
|
||||
// v2 = v_lookup(x, v);
|
||||
// Description:
|
||||
@@ -178,8 +191,8 @@ function v_lookup(x, v) =
|
||||
hi = vhi[1]
|
||||
)
|
||||
assert(is_vector(lo) && is_vector(hi),
|
||||
"Result values must all be numbers, or all be vectors.")
|
||||
assert(len(lo) == len(hi), "Vector result values must be the same length")
|
||||
"\nResult values must all be numbers, or all be vectors.")
|
||||
assert(len(lo) == len(hi), "\nVector result values must be the same length.")
|
||||
vlo.x == vhi.x? vlo[1] :
|
||||
let( u = (x - vlo.x) / (vhi.x - vlo.x) )
|
||||
lerp(lo,hi,u);
|
||||
@@ -191,7 +204,7 @@ function v_lookup(x, v) =
|
||||
// Function: unit()
|
||||
// Synopsis: Returns the unit length of a given vector.
|
||||
// Topics: Vectors, Math
|
||||
// See Also: v_abs(), v_floor(), v_ceil()
|
||||
// See Also: v_abs(), v_floor(), v_ceil(), v_round()
|
||||
// Usage:
|
||||
// v = unit(v, [error]);
|
||||
// Description:
|
||||
@@ -208,8 +221,8 @@ function v_lookup(x, v) =
|
||||
// v5 = unit([0,0,0],[1,2,3]); // Returns: [1,2,3]
|
||||
// v6 = unit([0,0,0]); // Asserts an error.
|
||||
function unit(v, error=[[["ASSERT"]]]) =
|
||||
assert(is_vector(v), "Invalid vector")
|
||||
norm(v)<EPSILON? (error==[[["ASSERT"]]]? assert(norm(v)>=EPSILON,"Cannot normalize a zero vector") : error) :
|
||||
assert(is_vector(v), "\nInvalid vector.")
|
||||
norm(v)<EPSILON? (error==[[["ASSERT"]]]? assert(norm(v)>=EPSILON,"\nCannot normalize a zero vector.") : error) :
|
||||
v/norm(v);
|
||||
|
||||
|
||||
@@ -222,7 +235,7 @@ function unit(v, error=[[["ASSERT"]]]) =
|
||||
// Description:
|
||||
// Given a vector, returns the angle in degrees counter-clockwise from X+ on the XY plane.
|
||||
function v_theta(v) =
|
||||
assert( is_vector(v,2) || is_vector(v,3) , "Invalid vector")
|
||||
assert( is_vector(v,2) || is_vector(v,3) , "\nInvalid vector.")
|
||||
atan2(v.y,v.x);
|
||||
|
||||
|
||||
@@ -255,19 +268,19 @@ function v_theta(v) =
|
||||
function vector_angle(v1,v2,v3) =
|
||||
assert( ( is_undef(v3) && ( is_undef(v2) || same_shape(v1,v2) ) )
|
||||
|| is_consistent([v1,v2,v3]) ,
|
||||
"Bad arguments.")
|
||||
assert( is_vector(v1) || is_consistent(v1), "Bad arguments.")
|
||||
"\nBad arguments.")
|
||||
assert( is_vector(v1) || is_consistent(v1), "\nBad arguments.")
|
||||
let( vecs = ! is_undef(v3) ? [v1-v2,v3-v2] :
|
||||
! is_undef(v2) ? [v1,v2] :
|
||||
len(v1) == 3 ? [v1[0]-v1[1], v1[2]-v1[1]]
|
||||
: v1
|
||||
)
|
||||
assert(is_vector(vecs[0],2) || is_vector(vecs[0],3), "Bad arguments.")
|
||||
assert(is_vector(vecs[0],2) || is_vector(vecs[0],3), "\nBad arguments.")
|
||||
let(
|
||||
norm0 = norm(vecs[0]),
|
||||
norm1 = norm(vecs[1])
|
||||
)
|
||||
assert(norm0>0 && norm1>0, "Zero length vector.")
|
||||
assert(norm0>0 && norm1>0, "\nZero length vector.")
|
||||
// NOTE: constrain() corrects crazy FP rounding errors that exceed acos()'s domain.
|
||||
acos(constrain((vecs[0]*vecs[1])/(norm0*norm1), -1, 1));
|
||||
|
||||
@@ -299,16 +312,16 @@ function vector_angle(v1,v2,v3) =
|
||||
// axis6 = vector_axis([[10,0,10], [0,0,0], [-10,10,0]]); // Returns: [-0.57735, -0.57735, 0.57735]
|
||||
function vector_axis(v1,v2=undef,v3=undef) =
|
||||
is_vector(v3)
|
||||
? assert(is_consistent([v3,v2,v1]), "Bad arguments.")
|
||||
? assert(is_consistent([v3,v2,v1]), "\nBad arguments.")
|
||||
vector_axis(v1-v2, v3-v2)
|
||||
: assert( is_undef(v3), "Bad arguments.")
|
||||
: assert( is_undef(v3), "\nBad arguments.")
|
||||
is_undef(v2)
|
||||
? assert( is_list(v1), "Bad arguments.")
|
||||
? assert( is_list(v1), "\nBad arguments.")
|
||||
len(v1) == 2
|
||||
? vector_axis(v1[0],v1[1])
|
||||
: vector_axis(v1[0],v1[1],v1[2])
|
||||
: assert( is_vector(v1,zero=false) && is_vector(v2,zero=false) && is_consistent([v1,v2])
|
||||
, "Bad arguments.")
|
||||
, "\nBad arguments.")
|
||||
let(
|
||||
eps = 1e-6,
|
||||
w1 = point3d(v1/norm(v1)),
|
||||
@@ -331,9 +344,9 @@ function vector_axis(v1,v2=undef,v3=undef) =
|
||||
function vector_bisect(v1,v2) =
|
||||
assert(is_vector(v1))
|
||||
assert(is_vector(v2))
|
||||
assert(!approx(norm(v1),0), "Zero length vector.")
|
||||
assert(!approx(norm(v2),0), "Zero length vector.")
|
||||
assert(len(v1)==len(v2), "Vectors are of different sizes.")
|
||||
assert(!approx(norm(v1),0), "\nZero length vector.")
|
||||
assert(!approx(norm(v2),0), "\nZero length vector.")
|
||||
assert(len(v1)==len(v2), "\nVectors are of different sizes.")
|
||||
let( v1 = unit(v1), v2 = unit(v2) )
|
||||
approx(v1,-v2)? undef :
|
||||
let(
|
||||
@@ -360,42 +373,17 @@ function vector_bisect(v1,v2) =
|
||||
// stroke([[0,0],w],endcap2="arrow2",color="red");
|
||||
// stroke([[0,0],vector_perp(v,w)], endcap2="arrow2", color="blue");
|
||||
function vector_perp(v,w) =
|
||||
assert(is_vector(v) && is_vector(w) && len(v)==len(w), "Invalid or mismatched inputs")
|
||||
assert(is_vector(v) && is_vector(w) && len(v)==len(w), "\nInvalid or mismatched inputs")
|
||||
w - w*v*v/(v*v);
|
||||
|
||||
|
||||
// Section: Vector Searching
|
||||
|
||||
|
||||
// Function: pointlist_bounds()
|
||||
// Synopsis: Returns the min and max bounding coordinates for the given list of points.
|
||||
// Topics: Geometry, Bounding Boxes, Bounds
|
||||
// See Also: closest_point(), vnf_bounds()
|
||||
// Usage:
|
||||
// pt_pair = pointlist_bounds(pts);
|
||||
// Description:
|
||||
// Finds the bounds containing all the points in `pts` which can be a list of points in any dimension.
|
||||
// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with
|
||||
// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]`
|
||||
// Arguments:
|
||||
// pts = List of points.
|
||||
function pointlist_bounds(pts) =
|
||||
assert(is_path(pts,dim=undef,fast=true) , "Invalid pointlist." )
|
||||
let(
|
||||
select = ident(len(pts[0])),
|
||||
spread = [
|
||||
for(i=[0:len(pts[0])-1])
|
||||
let( spreadi = pts*select[i] )
|
||||
[ min(spreadi), max(spreadi) ]
|
||||
]
|
||||
) transpose(spread);
|
||||
|
||||
|
||||
|
||||
// Function: closest_point()
|
||||
// Synopsis: Finds the closest point in a list of points.
|
||||
// Topics: Geometry, Points, Distance
|
||||
// See Also: pointlist_bounds(), furthest_point(), closest_point()
|
||||
// See Also: pointlist_bounds(), furthest_point()
|
||||
// Usage:
|
||||
// index = closest_point(pt, points);
|
||||
// Description:
|
||||
@@ -404,15 +392,15 @@ function pointlist_bounds(pts) =
|
||||
// pt = The point to find the closest point to.
|
||||
// points = The list of points to search.
|
||||
function closest_point(pt, points) =
|
||||
assert( is_vector(pt), "Invalid point." )
|
||||
assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." )
|
||||
assert(is_vector(pt), "\nInvalid point." )
|
||||
assert(is_path(points,dim=len(pt)), "\nInvalid pointlist or incompatible dimensions." )
|
||||
min_index([for (p=points) norm(p-pt)]);
|
||||
|
||||
|
||||
// Function: furthest_point()
|
||||
// Synopsis: Finds the furthest point in a list of points.
|
||||
// Topics: Geometry, Points, Distance
|
||||
// See Also: pointlist_bounds(), furthest_point(), closest_point()
|
||||
// See Also: pointlist_bounds(), closest_point()
|
||||
// Usage:
|
||||
// index = furthest_point(pt, points);
|
||||
// Description:
|
||||
@@ -421,8 +409,8 @@ function closest_point(pt, points) =
|
||||
// pt = The point to find the farthest point from.
|
||||
// points = The list of points to search.
|
||||
function furthest_point(pt, points) =
|
||||
assert( is_vector(pt), "Invalid point." )
|
||||
assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." )
|
||||
assert( is_vector(pt), "\nInvalid point." )
|
||||
assert(is_path(points,dim=len(pt)), "\nInvalid pointlist or incompatible dimensions." )
|
||||
max_index([for (p=points) norm(p-pt)]);
|
||||
|
||||
|
||||
@@ -436,7 +424,7 @@ function furthest_point(pt, points) =
|
||||
// Given a list of query points `query` and a `target` to search,
|
||||
// finds the points in `target` that match each query point. A match holds when the
|
||||
// distance between a point in `target` and a query point is less than or equal to `r`.
|
||||
// The returned list will have a list for each query point containing, in arbitrary
|
||||
// The returned list contains a list for each query point containing, in arbitrary
|
||||
// order, the indices of all points that match that query point.
|
||||
// The `target` may be a simple list of points or a search tree.
|
||||
// When `target` is a large list of points, a search tree is constructed to
|
||||
@@ -444,8 +432,8 @@ function furthest_point(pt, points) =
|
||||
// For small point lists, a direct search is done dispensing a tree construction.
|
||||
// Alternatively, `target` may be a search tree built with `vector_search_tree()`.
|
||||
// In that case, that tree is parsed looking for matches.
|
||||
// An empty list of query points will return a empty output list.
|
||||
// An empty list of target points will return a output list with an empty list for each query point.
|
||||
// An empty list of query points returns a empty output list.
|
||||
// An empty list of target points returns a output list with an empty list for each query point.
|
||||
// Arguments:
|
||||
// query = list of points to find matches for.
|
||||
// r = the search radius.
|
||||
@@ -485,7 +473,7 @@ function vector_search(query, r, target) =
|
||||
query==[] ? [] :
|
||||
is_list(query) && target==[] ? is_vector(query) ? [] : [for(q=query) [] ] :
|
||||
assert( is_finite(r) && r>=0,
|
||||
"The query radius should be a positive number." )
|
||||
"\nThe query radius should be a positive number." )
|
||||
let(
|
||||
tgpts = is_matrix(target), // target is a point list
|
||||
tgtree = is_list(target) // target is a tree
|
||||
@@ -495,13 +483,13 @@ function vector_search(query, r, target) =
|
||||
&& (len(target[1])==4 || (len(target[1])==1 && is_list(target[1][0])) )
|
||||
)
|
||||
assert( tgpts || tgtree,
|
||||
"The target should be a list of points or a search tree compatible with the query." )
|
||||
"\nThe target should be a list of points or a search tree compatible with the query." )
|
||||
let(
|
||||
dim = tgpts ? len(target[0]) : len(target[0][0]),
|
||||
simple = is_vector(query, dim)
|
||||
)
|
||||
assert( simple || is_matrix(query,undef,dim),
|
||||
"The query points should be a list of points compatible with the target point list.")
|
||||
"\nThe query points should be a list of points compatible with the target point list.")
|
||||
tgpts
|
||||
? len(target)<=400
|
||||
? simple ? [for(i=idx(target)) if(norm(target[i]-query)<=r) i ] :
|
||||
@@ -518,9 +506,9 @@ function _bt_search(query, r, points, tree) =
|
||||
assert( is_list(tree)
|
||||
&& ( ( len(tree)==1 && is_list(tree[0]) )
|
||||
|| ( len(tree)==4 && is_num(tree[0]) && is_num(tree[1]) ) ),
|
||||
"The tree is invalid.")
|
||||
"\nThe tree is invalid.")
|
||||
len(tree)==1
|
||||
? assert( tree[0]==[] || is_vector(tree[0]), "The tree is invalid." )
|
||||
? assert( tree[0]==[] || is_vector(tree[0]), "\nThe tree is invalid." )
|
||||
[for(i=tree[0]) if(norm(points[i]-query)<=r) i ]
|
||||
: norm(query-points[tree[0]]) > r+tree[1] ? [] :
|
||||
concat(
|
||||
@@ -541,14 +529,14 @@ function _bt_search(query, r, points, tree) =
|
||||
// search process. The tree construction stops branching when
|
||||
// a tree node represents a number of points less or equal to `leafsize`.
|
||||
// Search trees are ball trees. Constructing the
|
||||
// tree should be O(n log n) and searches should be O(log n), though real life
|
||||
// performance depends on how the data is distributed, and it will deteriorate
|
||||
// for high data dimensions. This data structure is useful when you will be
|
||||
// tree should be O(n log n) and searches should be O(log n), although real life
|
||||
// performance depends on how the data is distributed, and it deteriorates
|
||||
// for high data dimensions. This data structure is useful when you are
|
||||
// performing many searches of the same data, so that the cost of constructing
|
||||
// the tree is justified. (See https://en.wikipedia.org/wiki/Ball_tree)
|
||||
// For a small lists of points, the search with a tree may be more expensive
|
||||
// than direct comparisons. The argument `treemin` sets the minimum length of
|
||||
// point set for which a tree search will be done by `vector_search`.
|
||||
// the point set for which a tree search will be done by `vector_search`.
|
||||
// For an empty list of points it returns an empty list.
|
||||
// Arguments:
|
||||
// points = list of points to store in the search tree.
|
||||
@@ -568,9 +556,9 @@ function _bt_search(query, r, points, tree) =
|
||||
// }
|
||||
function vector_search_tree(points, leafsize=25, treemin=400) =
|
||||
points==[] ? [] :
|
||||
assert( is_matrix(points), "The input list entries should be points." )
|
||||
assert( is_matrix(points), "\nThe input list entries should be points." )
|
||||
assert( is_int(leafsize) && leafsize>=1,
|
||||
"The tree leaf size should be an integer greater than zero.")
|
||||
"\nThe tree leaf size should be an integer greater than zero.")
|
||||
len(points)<treemin ? points :
|
||||
[ points, _bt_tree(points, count(len(points)), leafsize) ];
|
||||
|
||||
@@ -621,7 +609,7 @@ function _bt_tree(points, ind, leafsize=25) =
|
||||
// }
|
||||
function vector_nearest(query, k, target) =
|
||||
assert(is_int(k) && k>0)
|
||||
assert(is_vector(query), "Query must be a vector.")
|
||||
assert(is_vector(query), "\nQuery must be a vector.")
|
||||
let(
|
||||
tgpts = is_matrix(target,undef,len(query)), // target is a point list
|
||||
tgtree = is_list(target) // target is a tree
|
||||
@@ -630,9 +618,9 @@ function vector_nearest(query, k, target) =
|
||||
&& (len(target[1])==4 || (len(target[1])==1 && is_list(target[1][0])) )
|
||||
)
|
||||
assert( tgpts || tgtree,
|
||||
"The target should be a list of points or a search tree compatible with the query." )
|
||||
"\nThe target should be a list of points or a search tree compatible with the query." )
|
||||
assert((tgpts && (k<=len(target))) || (tgtree && (k<=len(target[0]))),
|
||||
"More results are requested than the number of points.")
|
||||
"\nMore results are requested than the number of points.")
|
||||
tgpts
|
||||
? let( tree = _bt_tree(target, count(len(target))) )
|
||||
column(_bt_nearest( query, k, target, tree),0)
|
||||
@@ -644,7 +632,7 @@ function _bt_nearest(p, k, points, tree, answers=[]) =
|
||||
assert( is_list(tree)
|
||||
&& ( ( len(tree)==1 && is_list(tree[0]) )
|
||||
|| ( len(tree)==4 && is_num(tree[0]) && is_num(tree[1]) ) ),
|
||||
"The tree is invalid.")
|
||||
"\nThe tree is invalid.")
|
||||
len(tree)==1
|
||||
? _insert_many(answers, k, [for(entry=tree[0]) [entry, norm(points[entry]-p)]])
|
||||
: let( d = norm(p-points[tree[0]]) )
|
||||
@@ -669,9 +657,102 @@ function _insert_sorted(list, k, new) =
|
||||
function _insert_many(list, k, newlist,i=0) =
|
||||
i==len(newlist)
|
||||
? list
|
||||
: assert(is_vector(newlist[i],2), "The tree is invalid.")
|
||||
: assert(is_vector(newlist[i],2), "\nThe tree is invalid.")
|
||||
_insert_many(_insert_sorted(list,k,newlist[i]),k,newlist,i+1);
|
||||
|
||||
|
||||
|
||||
// Section: Bounds
|
||||
|
||||
|
||||
// Function: pointlist_bounds()
|
||||
// Synopsis: Returns the min and max bounding coordinates for the given list of points.
|
||||
// Topics: Geometry, Bounding Boxes, Bounds, Scaling
|
||||
// See Also: closest_point(), furthest_point(), vnf_bounds()
|
||||
// Usage:
|
||||
// pt_pair = pointlist_bounds(pts);
|
||||
// Description:
|
||||
// Finds the bounds containing all the points in `pts`, which can be a list of points in any dimension.
|
||||
// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with
|
||||
// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]`
|
||||
// Arguments:
|
||||
// pts = List of points.
|
||||
function pointlist_bounds(pts) =
|
||||
assert(is_path(pts,dim=undef,fast=true) , "\nInvalid pointlist." )
|
||||
let(
|
||||
select = ident(len(pts[0])),
|
||||
spread = [
|
||||
for(i=[0:len(pts[0])-1])
|
||||
let( spreadi = pts*select[i] )
|
||||
[ min(spreadi), max(spreadi) ]
|
||||
]
|
||||
) transpose(spread);
|
||||
|
||||
|
||||
// Function: fit_to_box()
|
||||
// Synopsis: Scale the x, y, and/or z coordinantes of a list of points to span a range.
|
||||
// Topics: Geometry, Bounding Boxes, Bounds, VNF Manipulation
|
||||
// See Also: fit_to_range()
|
||||
// Usage:
|
||||
// new_pts = fit_to_box(pts, [x=], [y=], [z=]);
|
||||
// new_vnf = fit_to_box(vnf, [x=], [y=], [z=]);
|
||||
// Description:
|
||||
// Given a list of 2D or 3D points, or a VNF structure, rescale and position one or more of the coordinates
|
||||
// to fit within specified ranges. At least one range (`x`, `y`, or `z`) must be specified. A normal use case
|
||||
// for this function is to rescale a VNF texture to fit within `0 <= z <= 1`.
|
||||
// .
|
||||
// While a range is typically `[min_value,max_value]`, the minimum and maximum values can be reversed,
|
||||
// resulting in new coordinates being a rescaled mirror image of the original coordinates.
|
||||
// Arguments:
|
||||
// pts = List of points, or a VNF structure.
|
||||
// x = `[min,max]` of rescaled x coordinates. Default: undef
|
||||
// y = `[min,max]` of rescaled y coordinates. Default: undef
|
||||
// z = `[min,max]` of rescaled z coordinates. Default: undef
|
||||
// Example(2D): A 2D bezier path (red) rescaled (blue) to fit in a square box centered on the origin.
|
||||
// bez = [
|
||||
// [10,60], [-5,30],
|
||||
// [20,60], [50,50], [100,30],
|
||||
// [50,30], [70,20]
|
||||
// ];
|
||||
// path = bezpath_curve(bez);
|
||||
// newpath = fit_to_box(path, x=[0,40], y=[0,40]);
|
||||
// stroke(path, width=2, color="red");
|
||||
// stroke(square(40), width=1, closed=true);
|
||||
// stroke(newpath, width=2, color="blue");
|
||||
// Example(3D): A prismoid (left) is rescaled to fit new x and z bounds. The z bounds minimum and maximum values are reversed, resulting in the new object on the right having inverted z coordinates.
|
||||
// vnf = prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]);
|
||||
// vnf_boxed = fit_to_box(vnf, x=[30,55], z=[5,-15]);
|
||||
// vnf_polyhedron(vnf);
|
||||
// vnf_polyhedron(vnf_boxed);
|
||||
function fit_to_box(pts, x, y, z) =
|
||||
assert(is_path(pts) || is_vnf(pts), "\npts must be a valid 2D or 3D path, or a VNF structure.")
|
||||
assert(any_defined([x,y,z]), "\nAt least one [min,max] range x, y, or z must be defined.")
|
||||
assert(is_undef(x) || is_vector(x,2), "\nx must be a 2-vector [min,max].")
|
||||
assert(is_undef(y) || is_vector(y,2), "\nx must be a 2-vector [min,max].")
|
||||
assert(is_undef(z) || is_vector(z,2), "\nx must be a 2-vector [min,max].")
|
||||
let(
|
||||
isvnf = is_vnf(pts),
|
||||
p = isvnf ? pts[0] : pts,
|
||||
bounds = isvnf ? vnf_bounds(pts) : pointlist_bounds(pts),
|
||||
dim = len(bounds[0]),
|
||||
err = assert(is_undef(z) || (dim>2 && is_def(z)), "\n2D data detected with z range specified."),
|
||||
whichdim = [is_def(x), is_def(y), is_def(z)],
|
||||
xmin = bounds[0][0],
|
||||
ymin = bounds[0][1],
|
||||
zmin = dim>2 ? bounds[0][2] : 0,
|
||||
// new scales
|
||||
xscale = whichdim.x ? (x[1]-x[0]) / (bounds[1][0]-xmin) : 1,
|
||||
yscale = whichdim.y ? (y[1]-y[0]) / (bounds[1][1]-ymin) : 1,
|
||||
zscale = whichdim.z ? (z[1]-z[0]) / (bounds[1][2]-zmin) : 1,
|
||||
// new offsets
|
||||
xo = whichdim.x ? x[0] : 0,
|
||||
yo = whichdim.y ? y[0] : 0,
|
||||
zo = whichdim.z ? z[0] : 0,
|
||||
// shift original min to 0, rescale to new scale, shift back to new min
|
||||
newpts = move(dim>2 ? [xo,yo,zo] : [xo,yo],
|
||||
scale(dim>2 ? [xscale,yscale,zscale] : [xscale,yscale],
|
||||
move(dim>2 ? -[xmin,ymin,zmin] : -[xmin,ymin], pts)))
|
||||
) isvnf ? [newpts[0], pts[1]] : newpts;
|
||||
|
||||
|
||||
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
|
||||
|
182
vnf.scad
182
vnf.scad
@@ -86,8 +86,8 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
|
||||
// triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false
|
||||
// convexity = (module) Max number of times a line could intersect a wall of the shape.
|
||||
// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported.
|
||||
// tex_size = An optional 2D target size (scalar or 2-vector) for the textures at `points[0][0]`. This size is approximate; the actual texture sizes are scaled as needed for whole tiles to fit the available surface. Default: `[5,5]`
|
||||
// tex_reps = If given instead of tex_size, an integer scalar or 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
|
||||
// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes are scaled somewhat to evenly fit the available surface.
|
||||
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
|
||||
// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false`
|
||||
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
|
||||
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
|
||||
@@ -211,7 +211,56 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
|
||||
// apply(m, [ [rgroove[0].x,0,-z], each rgroove, [last(rgroove).x,0,-z] ])
|
||||
// ], caps=true, col_wrap=true, reverse=true);
|
||||
// vnf_polyhedron(vnf, convexity=8);
|
||||
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): When applying a texture to a vertex array, remember that the density of the texture follows the density of the vertex array grid. Here is a surface with a wrinkle in both x and y directions, using location data generated by {{smooth_path()}}. The bezier curves have non-uniformly distributed spline points, indicated by the red dots along each edge. This results in a non-uniform distribution of the texture tiling.
|
||||
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): This vase shape cannot be constructed with rotational or linear sweeps. Using a vertex array to create a stack of polygons is the most practical way to make this and many other shapes. The cross-section is a rounded 9-pointed star that changes size and rotates back and forth as it rises in the z direction.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// vprofile =
|
||||
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
|
||||
// relsize=1, method="corners");
|
||||
// ridgepd = 20; // z period of star point wiggle
|
||||
// ridgeamp = 5; // amplitude of star point wiggle
|
||||
// polystack = [
|
||||
// for(p=vprofile) let(r=p.x, z=p.y)
|
||||
// path3d(
|
||||
// smooth_path(
|
||||
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
|
||||
// relsize=0.6, splinesteps=5, method="corners", closed=true),
|
||||
// z)
|
||||
// ];
|
||||
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true));
|
||||
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): The previous vase shape with a pebbly texture, simply by adding `texture="dots"` to the `vnf_vertex_array()` call. Because textures are spread over grid units and not measurement units, the data points in the polygon stack should be uniformly spaced.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// vprofile = resample_path(
|
||||
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
|
||||
// relsize=1, method="corners"),
|
||||
// 81, closed=false);
|
||||
// ridgepd = 20; // z period of star point wiggle
|
||||
// ridgeamp = 5; // amplitude of star point wiggle
|
||||
// polystack = [
|
||||
// for(p=vprofile) let(r=p.x, z=p.y)
|
||||
// path3d(
|
||||
// smooth_path(
|
||||
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
|
||||
// relsize=0.6, splinesteps=5, method="corners", closed=true),
|
||||
// z)
|
||||
// ];
|
||||
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true,
|
||||
// texture="dots", tex_samples=1, tex_size=5));
|
||||
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): This point array defines a simple square, but with a non-uniform grid.
|
||||
// pts = [for(x=[-1:.1:1])
|
||||
// [for(y=[-1:.1:1])
|
||||
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
|
||||
// 20*[x,y,0])]];
|
||||
// vnf=vnf_vertex_array(pts);
|
||||
// color("blue") vnf_wireframe(vnf,width=.2);
|
||||
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): The non-uniform grid gives rise to a non-uniform texturing, showing the effect of the uniformity and distribution of the points when creating a texture.
|
||||
// pts = [for(x=[-1:.1:1])
|
||||
// [for(y=[-1:.1:1])
|
||||
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
|
||||
// 20*[x,y,0])]];
|
||||
// vnf_vertex_array(pts,texture="dots",tex_reps=15);
|
||||
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): Here is another example showing the effect of nonuniform sampling. Here is a surface with a wrinkle in both x and y directions, using location data generated by {{smooth_path()}}, which uses beziers. Bezier curves have non-uniformly distributed points, indicated by the red dots along each edge, which results in a non-uniform texture tiling.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// xprofile = smooth_path([[0,0,0], [25,0,0], [49,0,-10], [51,0,10], [75,0,0], [100,0,0]],
|
||||
@@ -228,7 +277,7 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
|
||||
// for(p=xprofile) translate(p-[0,4,0]) sphere(1.5);
|
||||
// for(p=yprofile) translate(p-[4,0,0]) sphere(1.5);
|
||||
// }
|
||||
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): By passing the spline curves into {{resample_path()}}, we can get a uniform distribution of the same number of x and y profile points, as shown by the red dots. This results in a uniform distribution of the texture tiling.
|
||||
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): By passing the spline curves into {{resample_path()}}, we can get a uniform distribution of the x and y profile points, as shown by the red dots, which results in a uniform texture tiling.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// xprof = smooth_path([[0,0,0], [25,0,0], [49,0,-10], [51,0,10], [75,0,0], [100,0,0]],
|
||||
@@ -247,55 +296,6 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
|
||||
// for(p=xprofile) translate(p-[0,4,0]) sphere(1.5);
|
||||
// for(p=yprofile) translate(p-[4,0,0]) sphere(1.5);
|
||||
// }
|
||||
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): This is a vase shape that cannot be constructed with rotational or linear sweeps. Using a vertex array to create a stack of polygons is the most practical way to make this and many other shapes. The cross-section is a rounded 9-pointed star that changes size and rotates back and forth as it rises in the z direction.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// vprofile =
|
||||
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
|
||||
// relsize=1, method="corners");
|
||||
// ridgepd = 20; // z period of star point wiggle
|
||||
// ridgeamp = 5; // amplitude of star point wiggle
|
||||
// polystack = [
|
||||
// for(p=vprofile) let(r=p.x, z=p.y)
|
||||
// path3d(
|
||||
// smooth_path(
|
||||
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
|
||||
// relsize=0.6, splinesteps=5, method="corners", closed=true),
|
||||
// z)
|
||||
// ];
|
||||
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true));
|
||||
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): The previous vase shape with a pebbly texture, simply by adding `texture="dots"` to the `vnf_vertex_array()` call. Because textures are spread over grid units and not measurement units, the data points in the polygon stack should be more or less uniformly spaced. Each star-shaped cross-section has uniformly-spaced points, but the vertical profile `vprofile` in the previous example isn't uniform because the control points aren't evenly spaced. We fix this by passing this profile into `resample_path()`, which results in a uniform texture density.
|
||||
// include <BOSL2/rounding.scad>
|
||||
//
|
||||
// vprofile = resample_path(
|
||||
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
|
||||
// relsize=1, method="corners"),
|
||||
// 81, closed=false);
|
||||
// ridgepd = 20; // z period of star point wiggle
|
||||
// ridgeamp = 5; // amplitude of star point wiggle
|
||||
// polystack = [
|
||||
// for(p=vprofile) let(r=p.x, z=p.y)
|
||||
// path3d(
|
||||
// smooth_path(
|
||||
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
|
||||
// relsize=0.6, splinesteps=5, method="corners", closed=true),
|
||||
// z)
|
||||
// ];
|
||||
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true,
|
||||
// texture="dots", tex_samples=1));
|
||||
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): This point array defines a square region but with a non-uniform grid.
|
||||
// pts = [for(x=[-1:.1:1])
|
||||
// [for(y=[-1:.1:1])
|
||||
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
|
||||
// 20*[x,y,0])]];
|
||||
// vnf=vnf_vertex_array(pts);
|
||||
// color("blue") vnf_wireframe(vnf,width=.2);
|
||||
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): The non-uniform grid gives rise to a non-uniform texturing.
|
||||
// pts = [for(x=[-1:.1:1])
|
||||
// [for(y=[-1:.1:1])
|
||||
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
|
||||
// 20*[x,y,0])]];
|
||||
// vnf_vertex_array(pts,texture="dots",tex_reps=15);
|
||||
|
||||
|
||||
module vnf_vertex_array(
|
||||
@@ -310,7 +310,7 @@ module vnf_vertex_array(
|
||||
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, tex_scaling="default",
|
||||
convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull")
|
||||
{
|
||||
vnf = vnf_vertex_array(points=points, caps=caps, cap1=cap2, cap2=cap2,
|
||||
vnf = vnf_vertex_array(points=points, caps=caps, cap1=cap1, cap2=cap2,
|
||||
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate, tex_scaling=tex_scaling,
|
||||
texture=texture, tex_reps=tex_reps, tex_size=tex_size, tex_samples=tex_samples, tex_inset=tex_inset, tex_rot=tex_rot,
|
||||
tex_depth=tex_depth, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps,sidecap1=sidecap1,sidecap2=sidecap2
|
||||
@@ -339,7 +339,7 @@ function vnf_vertex_array(
|
||||
tex_inset=tex_inset, tex_samples=tex_samples, tex_rot=tex_rot, tex_scaling=tex_scaling,
|
||||
col_wrap=col_wrap, row_wrap=row_wrap, tex_depth=tex_depth, caps=caps, cap1=cap1, cap2=cap2, reverse=reverse,
|
||||
style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,normals=normals,triangulate=triangulate)
|
||||
:
|
||||
:
|
||||
assert(!(any([caps,cap1,cap2]) && !col_wrap), "\ncol_wrap must be true if caps are requested (without texture).")
|
||||
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap (without texture).")
|
||||
let(
|
||||
@@ -435,20 +435,39 @@ function vnf_vertex_array(
|
||||
) triangulate? vnf_triangulate(vnf) : vnf;
|
||||
|
||||
|
||||
|
||||
// Function&Module: vnf_tri_array()
|
||||
// Synopsis: Returns a VNF from an array of points. The array need not be rectangular.
|
||||
// SynTags: VNF
|
||||
// Topics: VNF Generators, Lists
|
||||
// See Also: vnf_vertex_array(), vnf_join(), vnf_from_polygons(), vnf_merge_points()
|
||||
// Usage:
|
||||
// vnf = vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=])
|
||||
// vnf = vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=])
|
||||
// vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=],...) [ATTACHMENTS];
|
||||
// Description:
|
||||
// Produces a VNF from an array of points where each row length can differ from the adjacent rows by any amount. This enables the construction of triangular or even irregular VNF patches. The resulting VNF can be wrapped along the rows by setting `row_wrap` to true, and wrapped along columns by setting `col_wrap` to true. It is possible to do both at once.
|
||||
// Produces a VNF from an array of points where each row length can differ from the adjacent rows by
|
||||
// any amount. This enables the construction of triangular or even irregular VNF patches. The
|
||||
// resulting VNF can be wrapped along the rows by setting `row_wrap` to true, and wrapped along
|
||||
// columns by setting `col_wrap` to true. It is possible to do both at once.
|
||||
// If `row_wrap` is false or not provided, end caps can be generated across the top and/or bottom rows.
|
||||
// .
|
||||
// The algorithm starts with the first point on each row and recursively walks around finding the minimum-length edge to make each new triangle face. This may result in several triangles being connected to one vertex. When triangulating two rows that happen to be equal length, the result is equivalent to {{vnf_vertex_array()}} using the "min_edge" style. If you already have a rectangular vertex list (equal length rows), you should use `vnf_vertex_array()` if you need a different triangulation style.
|
||||
// The algorithm starts with the first point on each row and recursively walks around finding the
|
||||
// minimum-length edge to make each new triangle face. This may result in several triangles being
|
||||
// connected to one vertex. When triangulating two rows that happen to be equal length, the result is
|
||||
// equivalent to {{vnf_vertex_array()}} using the "min_edge" style. If you already have a rectangular
|
||||
// vertex list (equal length rows), you should use `vnf_vertex_array()` if you need a different
|
||||
// triangulation style.
|
||||
// .
|
||||
// If you need to merge two VNF arrays that share edges using `vnf_join()` you can remove the duplicated vertices using `vnf_merge_points()`.
|
||||
// Because the algorithm seeks the minimum-length new edge to generate triangles between two
|
||||
// unequal-lengthy rows of vertices, there are cases where this can causing bunching of several
|
||||
// triangles sharing a single vertex, if several successive points of one row are closest to a single
|
||||
// point on the other row. Example 6 demonstrates this. If the two rows are equal in length, this
|
||||
// doesn't happen. The `limit_bunching` parameter, by default, limits the number of *additional*
|
||||
// triangles that would normally be generated to the difference between the row lengths. Example 6
|
||||
// demonstrates the effect of disabling this limit.
|
||||
// .
|
||||
// If you need to merge two VNF arrays that share edges using `vnf_join()` you can remove the
|
||||
// duplicated vertices using `vnf_merge_points()`.
|
||||
// Arguments:
|
||||
// points = List of point lists for each row.
|
||||
// ---
|
||||
@@ -458,6 +477,7 @@ function vnf_vertex_array(
|
||||
// col_wrap = If true, add faces to connect the last column to the first.
|
||||
// row_wrap = If true, add faces to connect the last row to the first.
|
||||
// reverse = If true, reverse all face normals.
|
||||
// limit_buncthing = If true, when triangulating between two rows of unequal length, then limit the number of additional triangles that would normally share a vertex. Ignored when the two row lengths are equal. If false, a vertex can be shared by unlimited triangles. Default: true
|
||||
// convexity = (module) Max number of times a line could intersect a wall of the shape.
|
||||
// cp = (module) Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid"
|
||||
// anchor = (module) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
|
||||
@@ -499,6 +519,13 @@ function vnf_vertex_array(
|
||||
// vnf = vnf_tri_array(pts);
|
||||
// vnf_wireframe(vnf,width=0.1);
|
||||
// color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9);
|
||||
// Example(3D,Med,NoAxes,Edges,VPR=[29,0,341],VPD=45,VPT=[11,5,2]): The default parameter `limit_bunching=true` prevents too many triangles from sharing a single vertex in one row, if several points of one row happen to be closest to a single point on another row. In the left figure, `limit_bunching=false`, causing an endpoint on each row to get many triangles from the other row, because the algorithm seeks the shortest triangle leg distance once the first two points of each row are connected. This doesn't happen if both rows are the same length. The figure on the right uses the default `limit_bunching=true`, forcing the triangulation to stop adding too many triangles to the same vertex.
|
||||
// pts = [
|
||||
// [[5,0,0], [4,0,1.4], [3,0,2], [2,0,1.4], [1,0,0]],
|
||||
// [[14,10,0], [12,9,5], [9,8,7], [6,7,7], [3,6,5], [0,5,0]]
|
||||
// ];
|
||||
// vnf_tri_array(pts, limit_bunching=false);
|
||||
// right(10) vnf_tri_array(pts);
|
||||
// Example(3D,NoAxes,Edges,VPR=[65,0,25],VPD=380,Med): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices.
|
||||
// bez = [
|
||||
// [[0,26], [35,26], [29,0], [80,16], [102,0]], //top
|
||||
@@ -524,11 +551,12 @@ module vnf_tri_array(
|
||||
col_wrap=false,
|
||||
row_wrap=false,
|
||||
reverse=false,
|
||||
limit_bunching=true,
|
||||
convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull")
|
||||
{
|
||||
vnf = vnf_tri_array(points=points, caps=caps, cap1=cap2, cap2=cap2,
|
||||
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse
|
||||
);
|
||||
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse,
|
||||
limit_bunching = limit_bunching);
|
||||
vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children();
|
||||
}
|
||||
|
||||
@@ -537,7 +565,8 @@ function vnf_tri_array(
|
||||
caps, cap1, cap2,
|
||||
col_wrap=false,
|
||||
row_wrap=false,
|
||||
reverse=false
|
||||
reverse=false,
|
||||
limit_bunching=true
|
||||
) =
|
||||
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap.")
|
||||
let(
|
||||
@@ -560,8 +589,11 @@ function vnf_tri_array(
|
||||
if (reverse) [[ for(i=[0:rowstarts[0]-1-addcol]) i ]]
|
||||
else [[ for(i=[rowstarts[0]-1-addcol:-1:0]) i ]],
|
||||
// triangulate between the two polygons
|
||||
for(i = [0:plen-2+(row_wrap?1:0)]) let(j = (i+1)%plen)
|
||||
_lofttri(st[i], st[j], pcumlen[i], pcumlen[j], rowstarts[i], rowstarts[j], reverse),
|
||||
for(i = [0:plen-2+(row_wrap?1:0)])
|
||||
let(
|
||||
j = (i+1)%plen,
|
||||
max_extra_edges = limit_bunching ? max(1, abs(len(st[i])-len(st[j]))) : INF
|
||||
) _lofttri(st[i], st[j], pcumlen[i], pcumlen[j], rowstarts[i], rowstarts[j], reverse, trimax=max_extra_edges),
|
||||
// close up the last end
|
||||
if (caplast)
|
||||
if (reverse) [[ for(i=[pcumlen[plen]-1-addcol:-1:pcumlen[plen-1]]) i ]]
|
||||
@@ -594,24 +626,35 @@ Other parameters are for internal use:
|
||||
trilist[] = array of triangles to return
|
||||
i1 = vertex index on p1 of the next triangle
|
||||
i2 = vertex index on p2 of the next triangle
|
||||
tricount1 = number extra triangles generated on vertex i2 to row p1
|
||||
tricount2 = number extra triangles generated on vertex i1 to row p2
|
||||
trimax = max number of extra triangles that can be created on any point in a row
|
||||
(next triangle vertex found can be on either p1 or p2, depending
|
||||
on which triangle is smaller.)
|
||||
|
||||
Returns an array of triangles using vertex indices offset by
|
||||
i1offset and i2offset
|
||||
*/
|
||||
function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], i1=0, i2=0) = n1!=n2 ?
|
||||
function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], i1=0, i2=0, tricount1=0, tricount2=0, trimax=INF) = n1!=n2 ?
|
||||
// unequal row lengths
|
||||
let(
|
||||
t1 = i1 < n1 ? i1+1 : n1, // test point 1
|
||||
t2 = i2 < n2 ? i2+1 : n2, // test point 2
|
||||
//dum=echo(str("i1=",i1," i2=",i2," t1=",t1," t2=",t2," n1=",n1," n2=",n2, " p1[t1]=",p1[t1]," p2[i2]=",p2[i2])),
|
||||
d12 = t2>=n2 ? 9e+9 : norm(p2[t2]-p1[i1]), // distance from i1 to t2
|
||||
d21 = t1>=n1 ? 9e+9 : norm(p1[t1]-p2[i2]), // distance from i2 to t1
|
||||
//dum2=echo(str(" d12=",d12," d21=",d21," tricounts=",tricount1,",",tricount2)),
|
||||
userow = d12<d21 ? (tricount1<trimax ? 2 : 1) : (tricount2<trimax ? 1 : 2),
|
||||
newt = userow==1 ? (t1<n1?t1:i1) : (t2<n2?t2:i2),
|
||||
newofft = userow==2 ? i2offset+newt : i1offset+newt,
|
||||
tc1 = d12<d21 && tricount1<trimax ? tricount1+1 : 0,
|
||||
tc2 = d21<d12 && tricount2<trimax ? tricount2+1 : 0,
|
||||
triangle = reverse ?
|
||||
[i1offset+i1, i2offset+i2, d12<d21 ? i2offset+t2 : i1offset+t1] :
|
||||
[i2offset+i2, i1offset+i1, d12<d21 ? i2offset+t2 : i1offset+t1]
|
||||
[i1offset+i1, i2offset+i2, newofft] :
|
||||
[i2offset+i2, i1offset+i1, newofft]
|
||||
) t1>=n1 && t2>=n2 ? trilist :
|
||||
_lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]), d12<d21 ? i1 : t1, d12<d21 ? t2 : i2)
|
||||
_lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]),
|
||||
userow==1 ? (t1>=n1?i1:t1) : i1, userow==2 ? (t2>=n2?i2:t2) : i2, tc1, tc2, trimax)
|
||||
|
||||
: // equal row lengths
|
||||
let(n=n1, i=i1,
|
||||
@@ -625,7 +668,8 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[],
|
||||
[i2offset+t, i1offset+t, d12<d21 ? i1offset+i : i2offset+i] :
|
||||
[i1offset+t, i2offset+t, d12<d21 ? i1offset+i : i2offset+i]
|
||||
) t>=n ? trilist :
|
||||
_lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t);
|
||||
_lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t, 0,0,trimax);
|
||||
|
||||
|
||||
|
||||
// Function: vnf_join()
|
||||
|
Reference in New Issue
Block a user