diff --git a/.github/workflows/docsgen.yml b/.github/workflows/docsgen.yml index 911fac27..bb854ca8 100644 --- a/.github/workflows/docsgen.yml +++ b/.github/workflows/docsgen.yml @@ -1,57 +1,54 @@ -# This is a basic workflow to help you get started with Actions - name: CI - -# Controls when the action will run. Triggers the workflow on push events but only for the master branch on: push: branches: - master -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: GenerateDocs: - # The type of runner that the job will run on runs-on: macos-10.15 steps: - name: Checkout uses: actions/checkout@v2 - - name: Wiki Checkout - env: - GH_PAT: ${{ secrets.GH_PAT }} - run: | - cd $GITHUB_WORKSPACE - git clone https://${GH_PAT}@github.com/revarbat/BOSL2.wiki.git - cd BOSL2.wiki - git config user.name "revarbat" - git config user.email "revarbat@users.noreply.github.com" + - name: Checkout Wiki + uses: actions/checkout@v2 + with: + repository: ${{github.repository}}.wiki + path: BOSL2.wiki + + - name: Install gifsicle + run: brew install gifsicle - name: Install Pillow run: sudo pip3 install Pillow + - name: Install Docsgen + run: sudo pip3 install openscad_docsgen + - name: Install OpenSCAD run: | - curl -L -o OpenSCAD.dmg https://files.openscad.org/OpenSCAD-2019.05.dmg + curl -L -o OpenSCAD.dmg https://files.openscad.org/OpenSCAD-2021.01.dmg hdiutil attach OpenSCAD.dmg cp -a /Volumes/OpenSCAD/OpenSCAD.app /Applications/ - - name: Generate Index - run: | - cd $GITHUB_WORKSPACE - ./scripts/genindex.sh - - - name: Generate Cheat Sheet - run: | - cd $GITHUB_WORKSPACE - ./scripts/gencheat.sh - - name: Generating Docs - env: - GH_PAT: ${{ secrets.GH_PAT }} run: | cd $GITHUB_WORKSPACE export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE) - ./scripts/make_all_docs.sh -i && cd BOSL2.wiki && git add --all && ( git commit -m "Wiki docs auto-regen." && git push --set-upstream https://${GH_PAT}@github.com/revarbat/BOSL2.wiki.git master || true ) + openscad-docsgen -m -i -t -c *.scad + cd BOSL2.wiki + git config user.name github-actions + git config user.email github-actions@github.com + git add --all + git commit -m "Wiki docs auto-regen." && git push || true + - name: Bump Release Version + run: | + cd $GITHUB_WORKSPACE + ./scripts/increment_version.sh + git config user.name github-actions + git config user.email github-actions@github.com + git add version.scad + git commit -m "Bump release version." && git push || true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 13c3151b..88f0279f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,30 +1,23 @@ -# This is a basic workflow to help you get started with Actions - name: CI - -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch on: [pull_request] -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: Regressions: - # The type of runner that the job will run on runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - - name: Wiki Checkout + - name: Clone Wiki run: | cd $GITHUB_WORKSPACE git clone https://github.com/revarbat/BOSL2.wiki.git - - name: Get OpenSCAD Appimage + - name: Install OpenSCAD run: | cd $GITHUB_WORKSPACE - wget https://files.openscad.org/OpenSCAD-2019.05-x86_64.AppImage - sudo mv OpenSCAD-2019.05-x86_64.AppImage /usr/local/bin/openscad + wget https://files.openscad.org/OpenSCAD-2021.01-x86_64.AppImage + sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad sudo chmod +x /usr/local/bin/openscad - name: Run Regression Tests @@ -34,47 +27,35 @@ jobs: ./scripts/run_tests.sh CheckDocs: - # The type of runner that the job will run on runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout uses: actions/checkout@v2 - - name: Wiki Checkout + - name: Clone Wiki run: | cd $GITHUB_WORKSPACE git clone https://github.com/revarbat/BOSL2.wiki.git - - name: Install Python pip - run: sudo apt-get install python3-pip + - name: Apt Update + run: sudo apt update - name: Install Python dev - run: sudo apt-get install python3-dev python3-setuptools + run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil - - name: Install Pillow Dependencies - run: sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev - - - name: Install Pillow - run: sudo pip3 install Pillow + - name: Install OpenSCAD-DocsGen package. + run: sudo pip3 install openscad-docsgen - name: Install OpenSCAD run: | cd $GITHUB_WORKSPACE - wget https://files.openscad.org/OpenSCAD-2019.05-x86_64.AppImage - sudo mv OpenSCAD-2019.05-x86_64.AppImage /usr/local/bin/openscad + wget https://files.openscad.org/OpenSCAD-2021.01-x86_64.AppImage + sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad sudo chmod +x /usr/local/bin/openscad - - name: Generate Index - run: | - cd $GITHUB_WORKSPACE - ./scripts/genindex.sh - - name: Generating Docs run: | cd $GITHUB_WORKSPACE export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE) - ./scripts/make_all_docs.sh -t -i + openscad-docsgen -Tm *.scad diff --git a/.nodocsfiles b/.nodocsfiles deleted file mode 100644 index dd599e4c..00000000 --- a/.nodocsfiles +++ /dev/null @@ -1,3 +0,0 @@ -bosl1compat.scad -std.scad -version.scad diff --git a/.openscad_gendocs_rc b/.openscad_gendocs_rc new file mode 100644 index 00000000..0d6030c9 --- /dev/null +++ b/.openscad_gendocs_rc @@ -0,0 +1,49 @@ +DocsDirectory: BOSL2.wiki/ +IgnoreFiles: + foo.scad + std.scad + bosl1compat.scad + version.scad + tmp_*.scad +PrioritizeFiles: + constants.scad + transforms.scad + distributors.scad + mutators.scad + attachments.scad + primitives.scad + shapes.scad + shapes2d.scad + masks.scad + math.scad + vectors.scad + arrays.scad + quaternions.scad + affine.scad + coords.scad + geometry.scad + edges.scad + vnf.scad + paths.scad + regions.scad + debug.scad + common.scad + strings.scad + errors.scad + beziers.scad + threading.scad + rounding.scad + knurling.scad + partitions.scad + rounding.scad + skin.scad + hull.scad + triangulation.scad + turtle3d.scad + stacks.scad + queues.scad + structs.scad +DefineHeader(BulletList): Side Effects +DefineHeader(Table:Anchor Name|Position): Extra Anchors +DefineHeader(Table:Name|Definition): Terminology + diff --git a/README.md b/README.md index 23177f49..2955c743 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # BOSL2 +![BOSL2 Logo](images/BOSL2logo.png) **The Belfry OpenScad Library, v2** @@ -123,7 +124,7 @@ The library files are as follows: - [`polyhedra.scad`](polyhedra.scad): Modules to create various regular and stellated polyhedra. - [`walls.scad`](walls.scad): Modules to create walls and structural elements for 3D printing. - [`cubetruss.scad`](cubetruss.scad): Modules to create modular open-framed trusses and joiners. - - [`involute_gears.scad`](involute_gears.scad): Modules and functions to make involute gears and racks. + - [`gears.scad`](gears.scad): Modules and functions to make gears, racks, worm, and worm gears. - [`joiners.scad`](joiners.scad): Modules to make joiner shapes for connecting separately printed objects. - [`sliders.scad`](sliders.scad): Modules for creating simple sliders and rails. - [`metric_screws.scad`](metric_screws.scad): Functions and modules to make metric screws, nuts, and screwholes. @@ -134,7 +135,7 @@ The library files are as follows: - [`wiring.scad`](wiring.scad): Modules to render routed bundles of wires. - [`hingesnaps.scad`](hingesnaps.scad): Modules to make foldable, snap-locking parts. - [`bottlecaps.scad`](bottlecaps.scad): Modules to make standard beverage bottle caps and necks. - + - [`modular_hose.scad`](modular_hose.scad): Modules to make module hose segments and ends. ## Documentation The full library docs can be found at https://github.com/revarbat/BOSL2/wiki diff --git a/WRITING_DOCS.md b/WRITING_DOCS.md index 19ee8e1f..438124db 100644 --- a/WRITING_DOCS.md +++ b/WRITING_DOCS.md @@ -1,155 +1,850 @@ -# Formatting Comments for Docs +Documenting OpenSCAD Code +------------------------------------------------- -Documentation and example images are generated automatically from source code comments by the `scripts/docs_gen.py` script. Not all comments are added to the wiki. Just those comment blocks starting with certain keywords: +Documentation comment blocks are all based around a single simple syntax: -- `// LibFile: NAME` -- `// Section: NAME` -- `// Constant: NAME` -- `// Function: NAME` -- `// Module: NAME` -- `// Function&Module: NAME` + // Block Name(Metadata): TitleText + // Body line 1 + // Body line 2 + // Body line 3 -## LibFile: +- The Block Name is one or two words, both starting with a capital letter. +- The Metadata is in parentheses. It is optional, and can contain fairly arbitrary text, as long as it doesn't include newlines or parentheses. If the Metadata part is not given, the parentheses are optional. +- A colon `:` will always follow after the Block Name and optional Metadata. +- The TitleText will be preceded by a space ` `, and can contain arbitrary text, as long as it contains no newlines. The TitleText part is also optional for some header blocks. +- The body will contain zero or more lines of text indented by three spaces after the comment markers. Each line can contain arbitrary text. -LibFile blocks can be followed by multiple lines that can be added as markdown text after the header. Indentation is important, as it denotes the end of block. +So, for example, a Figure block to show a 640x480 animated GIF of a spinning shape may look like: -``` -// LibFile: foo.scad -// You can have several lines of markdown formatted text here. -// You just need to make sure that each line is indented, with -// at least three spaces after the comment marker. You can -// denote a paragraph break with a comment line with three -// trailing spaces, or just a period. -// . -// The end of the block is denoted by a line without a comment. -``` + // Figure(Spin,Size=640x480,VPD=444): A Cube and Cylinder. + // cube(80, center=true); + // cylinder(h=100,d=60,center=true); -## Section: +Various block types don't need all of those parts, so they may look simpler: -Section blocks can be followed by multiple lines that can be added as markdown text after the header. Indentation is important, as it denotes the end of block. + // Topics: Mask, Cylindrical, Attachable + +Or: + + // Description: + // This is a description. + // It can be multiple lines in length. + +Or: + + // Usage: Typical Usage + // x = foo(a, b, c); + // x = foo([a, b, c, ...]); + +Comments blocks that don't start with a known block header are ignored and not added to output documentation. This lets you have normal comments in your code that are not used for documentation. If you must start a comment block with one of the known headers, then adding a single extra `/` or space after the comment marker, will make it be treated as a regular comment: + + /// File: Foobar.scad + + +Block Headers +======================= + +File/LibFile Blocks +------------------- + +All files must have either a `// File:` block or a `// LibFile:` block at the start. This is the place to put in the canonical filename, and a description of what the file is for. These blocks can be used interchangably, but you can only have one per file. `// File:` or `// LibFile:` blocks can be followed by a multiple line body that are added as markdown text after the header: + + // LibFile: foo.scad + // You can have several lines of markdown formatted text here. + // You just need to make sure that each line is indented, with + // at least three spaces after the comment marker. You can + // denote a paragraph break with a comment line with three + // trailing spaces, or just a period. + // . + // You can have links in this text to functions, modules, or + // constants in other files by putting the name in double- + // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to + // link to another file, or section in another file you can use + // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). + // The end of the block is denoted by a line without a comment. + +Which outputs Markdown code that renders like: + +> ## LibFile: foo.scad +> You can have several lines of markdown formatted text here. +> You just need to make sure that each line is indented, with +> at least three spaces after the comment marker. You can +> denote a paragraph break with a comment line with three +> trailing spaces, or just a period. +> +> You can have links in this text to functions, modules, or +> constants in other files by putting the name in double- +> braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to +> link to another file, or section in another file you can use +> a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). +> The end of the block is denoted by a line without a comment. + +You can use `// File:` instead of `// LibFile:`, if it seems more apropriate for +your particular context:: + + // File: Foobar.scad + // This file contains a collection of metasyntactical nonsense. + +Which outputs Markdown code that renders like: + +> # File: Foobar.scad +> This file contains a collection of metasyntactical nonsense. + + +Includes Block +-------------- + +To declare what code the user needs to add to their code to include or use this library file, you can use the `// Includes:` block. You should put this right after the `// File:` or `// LibFile:` block. This code block will also be prepended to all Example and Figure code blocks before they are evaluated: + + // Includes: + // include + // include + +Which outputs Markdown code that renders like: + +> **Includes:** +> +> To use, add the following lines to the beginning of your file: +> +> ```openscad +> include +> include +> ``` + +CommonCode Block +---------------- + +If you have a block of code you plan to use throughout the file's Figure or Example blocks, and you don't actually want it displayed, you can use a `// CommonCode:` block like thus: + + // CommonCode: + // module text3d(text, h=0.01, size=3) { + // linear_extrude(height=h, convexity=10) { + // text(text=text, size=size, valign="center", halign="center"); + // } + // } + +This doesn't have immediately visible markdown output, but you *can* use that code in later examples: + + // Example: + // text3d("Foobar"); + + +Section Block +------------- +Section blocks take a title, and an optional body that will be shown as the description of the Section. If a body line if just a `.` (dot, period), then that line is treated as a blank line in the output: + + // Section: Foobar + // You can have several lines of markdown formatted text here. + // You just need to make sure that each line is indented, with + // at least three spaces after the comment marker. You can + // denote a paragraph break with a comment line with three + // trailing spaces, or just a period. + // . + // You can have links in this text to functions, modules, or + // constants in other files by putting the name in double- + // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to + // link to another file, or section in another file you can use + // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). + // . + // The end of the block is denoted by a line without a comment. + // or a line that is unindented after the comment. + +Which outputs Markdown code that renders like: + +> ## Section: Foobar +> You can have several lines of markdown formatted text here. +> You just need to make sure that each line is indented, with +> at least three spaces after the comment marker. You can +> denote a paragraph break with a comment line with three +> trailing spaces, or just a period. +> +> You can have links in this text to functions, modules, or +> constants in other files by putting the name in double- +> braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to +> link to another file, or section in another file you can use +> a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). +> +> The end of the block is denoted by a line without a comment. +> or a line that is unindented after the comment. Sections can also include Figures; images generated from code that is not shown in a code block. -``` -// Section: Foobar -// You can have several lines of markdown formatted text here. -// You just need to make sure that each line is indented, with -// at least three spaces after the comment marker. You can -// denote a paragraph break with a comment line with three -// trailing spaces, or just a period. -// . -// The end of the block is denoted by a line without a comment. -// or a line that is unindented after the comment. -// Figure: Figure description -// cylinder(h=100, d1=75, d2=50); -// up(100) cylinder(h=100, d1=50, d2=75); -// Figure(Spin): Animated figure that spins to show all faces. -// cube([10,100,50], center=true); -// cube([100,10,30], center=true); -``` -## CommonCode: +Item Blocks +----------- -CommonCode blocks can be used to denote code that can be shared between all of the Figure and Example blocks in the file, without being shown itself. Indentation is important. Less than three spaces indent denotes the end of the block +Item blocks headers come in four varieties: `Constant`, `Function`, `Module`, and `Function&Module`. -``` -// CommonCode: -// module text3d(text, h=0.01, size=3) { -// linear_extrude(height=h, convexity=10) { -// text(text=text, size=size, valign="center", halign="center"); -// } -// } -``` +The `Constant` header is used to document a code constant. It should have a Description sub-block, and Example sub-blocks are recommended: -## Module:/Function:/Function&Module:/Constant: + // Constant: PHI + // Description: The golden ratio phi. + PHI = (1+sqrt(5))/2; -Module, Function, and Constant docs blocks all have a similar specific format. Most sub-blocks are optional, except the Module/Function/Constant line, and the Description block. +Which outputs Markdown code that renders like: -Valid sub-blocks are: +> ### Constant: PHI +> **Description:** +> The golden ration phi. -- `Status: DEPRECATED, use blah instead.` - Optional, used to denote deprecation. -- `Usage: Optional Usage Title` - Optional. Multiple allowed. Followed by an indented block of usage patterns. Optional arguments should be in braces like `[opt]`. Alternate args should be separated by a vertical bar like `r|d`. -- `Description:` - Can be single-line or a multi-line block of the description. -- `Figure: Optional Figure Title` - Optional. Multiple allowed. Followed by a multi-line code block used to create a figure image. The code will not be shown. All figures will follow the Description block. -- `Arguments:` - Denotes start of an indented block of argument descriptions. Each line has the argument name, a space, an equals, another space, then the description for the argument all on one line. Like `arg = The argument description`. If you really need to explain an argument in longer form, explain it in the Description. -- `Side Effects:` - Denotes the start of a block describing the side effects, such as `$special_var`s that are set. -- `Extra Anchors:` - Denotes the start of an indented block of available non-standard named anchors for a part. -- `Example:` - Denotes the beginning of a multi-line example code block. -- `Examples:` - Denotes the beginning of a block of examples, where each line will be shows as a separate example with a separate image if needed. -Modules blocks will generate images for each example or figure block. Function and Constant blocks will only generate images for example blocks if they have `2D` or `3D` tags. Example and figure blocks can have tags added by putting then inside parentheses before the colon. Ie: `Examples(BigFlatSpin):` or `Figure(2D):`. +The `Module` header is used to document a module. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks: -The full set of optional example tags are: + // Module: cross() + // Usage: + // cross(size); + // Description: + // Creates a 2D cross/plus shape. + // Arguments: + // size = The scalar size of the cross. + // Example(2D): + // cross(size=100); + module cross(size=1) { + square([size, size/3], center=true); + square([size/3, size], center=true); + } +Which outputs Markdown code that renders like: + +> ### Module: cross() +> **Usage:** +> - cross(size); +> +> **Description:** +> Creates a 2D cross/plus shape. +> +> **Arguments:** +> Positional Arg | What it does +> -------------------- | ------------------- +> size | The scalar size of the cross. +> +> **Example:** +> ```openscad +> cross(size=100); +> ``` +> GENERATED IMAGE GOES HERE + + +The `Function` header is used to document a function. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks. By default, Examples will not generate images for function blocks: + + // Function: vector_angle() + // Usage: + // ang = vector_angle(v1, v2); + // Description: + // Calculates the angle between two vectors in degrees. + // Arguments: + // v1 = The first vector. + // v2 = The second vector. + // Example: + // v1 = [1,1,0]; + // v2 = [1,0,0]; + // angle = vector_angle(v1, v2); + // // Returns: 45 + function vector_angle(v1,v2) = + acos(max(-1,min(1,(vecs[0]*vecs[1])/(norm0*norm1)))); + +Which outputs Markdown code that renders like: + +> ### Function: vector_angle() +> **Usage:** +> - ang = vector_angle(v1, v2); +> +> **Description:** +> Calculates the angle between two vectors in degrees. +> +> **Arguments:** +> Positional Arg | What it does +> -------------------- | ------------------- +> `v1` | The first vector. +> `v2` | The second vector. +> +> **Example:** +> ```openscad +> v1 = [1,1,0]; +> v2 = [1,0,0]; +> angle = vector_angle(v1, v2); +> // Returns: 45 +> ``` + +The `Function&Module` header is used to document a function which has a related module of the same name. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks. You should have Usage blocks for both calling as a function, and calling as a +module: + + // Function&Module: oval() + // Topics: 2D Shapes, Geometry + // Usage: As a Module + // oval(rx,ry); + // Usage: As a Function + // path = oval(rx,ry); + // Description: + // When called as a function, returns the perimeter path of the oval. + // When called as a module, creates a 2D oval shape. + // Arguments: + // rx = X axis radius. + // ry = Y axis radius. + // Example(2D): Called as a Function + // path = oval(100,60); + // polygon(path); + // Example(2D): Called as a Module + // oval(80,60); + module oval(rx,ry) { + polygon(oval(rx,ry)); + } + function oval(rx,ry) = + [for (a=[360:-360/$fn:0.0001]) [rx*cos(a),ry*sin(a)]; + +Which outputs Markdown code that renders like: + +> ### Function&Module: oval() +> **Topics:** 2D Shapes, Geometry +> +> **Usage:** As a Module +> +> - oval(rx,ry); +> +> **Usage:** As a Function +> +> - path = oval(rx,ry); +> +> **Description:** +> When called as a function, returns the perimeter path of the oval. +> When called as a module, creates a 2D oval shape. +> +> **Arguments:** +> Positional Arg | What it does +> -------------------- | ------------------- +> rx | X axis radius. +> ry | Y axis radius. +> +> **Example:** Called as a Function +> +> ```openscad +> path = oval(100,60); +> polygon(path); +> ``` +> GENERATED IMAGE SHOWN HERE +> +> **Example:** Called as a Module +> +> ```openscad +> oval(80,60); +> ``` +> GENERATED IMAGE SHOWN HERE + +These Type blocks can have a number of sub-blocks. Most sub-blocks are optional, The available standard sub-blocks are: +- `// Aliases: alternatename(), anothername()` +- `// Status: DEPRECATED` +- `// Topics: Comma, Delimited, Topic, List` +- `// Usage:` +- `// Description:` +- `// Arguments:` +- `// Figure:` or `// Figures` +- `// See Also: otherfunc(), othermod(), OTHERCONST` +- `// Example:` or `// Examples:` + + +Aliases Block +------------- + +The Aliases block is used to give alternate names for a function, module, or +constant. This is reflected in the indexes generated. It looks like: + + // Aliases: secondname(), thirdname() + +Which outputs Markdown code that renders like: + +> **Aliases:** secondname(), thirdname() + + +Status Block +------------ + +The Status block is used to mark a function, module, or constant as deprecated: + + // Status: DEPRECATED, use foo() instead + +Which outputs Markdown code that renders like: + +> **Status:** DEPRECATED, use foo() instead + + +Topics Block +------------ + +The Topics block can associate various topics with the current function or module. This can be used to make an index of Topics: + + // Topics: 2D Shapes, Geometry, Masks + +Which outputs Markdown code that renders like: + +> **Topics:** 2D Shapes, Geometry, Masks + + +Usage Block +----------- + +The Usage block describes the various ways that the current function or module can be called, with the names of the arguments. By convention, the first few arguments that can be called positionally just have their name shown. The remaining arguments that should be passed by name, will have the name followed by an `=` (equal sign). Arguments that are optional in the given Usage context are shown in `<` and `>` angle brackets: + + // Usage: As a Module + // oval(rx, ry, ); + // Usage: As a Function + // path = oval(rx, ry, ); + +Which outputs Markdown code that renders like: + +> **Usage:** As a Module +> - oval(rx, ry, ); +> +> **Usage:** As a Function +> +> - path = oval(rx, ry, ); + + +Description Block +----------------- +The Description block just describes the currect function, module, or constant: + + // Descripton: This is the description for this function or module. + // It can be multiple lines long. Markdown syntax code will be used + // verbatim in the output markdown file, with the exception of `_`, + // which will traslate to `\_`, so that underscores in function/module + // names don't get butchered. A line with just a period (`.`) will be + // treated as a blank line. + // . + // You can have links in this text to functions, modules, or + // constants in other files by putting the name in double- + // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to + // link to another file, or section in another file you can use + // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). + +Which outputs Markdown code that renders like: + +> **Description:** +> It can be multiple lines long. Markdown syntax code will be used +> verbatim in the output markdown file, with the exception of `_`, +> which will traslate to `\_`, so that underscores in function/module +> names don't get butchered. A line with just a period (`.`) will be +> treated as a blank line. +> +> You can have links in this text to functions, modules, or +> constants in other files by putting the name in double- +> braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to +> link to another file, or section in another file you can use +> a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). + + +Arguments Block +--------------- +The Arguments block creates a table that describes the positional arguments for a function or module, and optionally a second table that describes named arguments: + + // Arguments: + // v1 = This supplies the first vector. + // v2 = This supplies the second vector. + // --- + // fast = Use fast, but less comprehensive calculation method. + // bar = Takes an optional `bar` struct. See {{bar()}}. + // dflt = Default value. + +Which outputs Markdown code that renders like: + +> **Arguments:** +> Positional Arg | What it Does +> -------------- | --------------------------------- +> `v1` | This supplies the first vector. +> `v2` | The supplies the second vector. +> +> Named Arg | What it Does +> -------------- | --------------------------------- +> `fast` | If true, use fast, but less accurate calculation method. +> `bar` | Takes an optional `bar` struct. See [bar()](foobar.scad#function-bar). +> `dflt` | Default value. + + +See Also Block +-------------- + +The See Also block is used to give links to related functions, modules, or +constants. It looks like: + + // See Also: relatedfunc(), similarmodule() + +Which outputs Markdown code that renders like: + +> **See Also:** [relatedfunc()](otherfile.scad#relatedfunc), [similarmodule()](otherfile.scad#similarmodule) + + +Figure Block +-------------- + +A Figure block generates and shows an image from a script in the multi-line body, by running it in OpenSCAD. A Figures block (plural) does the same, but treats each line of the body as a separate Figure block: + + // Figure: Figure description + // cylinder(h=100, d1=75, d2=50); + // up(100) cylinder(h=100, d1=50, d2=75); + // Figure(Spin,VPD=444): Animated figure that spins to show all faces. + // cube([10,100,50], center=true); + // cube([100,10,30], center=true); + // Figures: + // cube(100); + // cylinder(h=100,d=50); + // sphere(d=100); + +Which outputs Markdown code that renders like: + +> **Figure 1:** Figure description +> GENERATED IMAGE SHOWN HERE +> +> **Figure 2:** Animated figure that spins to show all faces. +> GENERATED IMAGE SHOWN HERE +> +> **Figure 3:** +> GENERATED IMAGE OF CUBE SHOWN HERE +> +> **Figure 4:** +> GENERATED IMAGE OF CYLINDER SHOWN HERE +> +> **Figure 5:** +> GENERATED IMAGE OF SPHERE SHOWN HERE + +The metadata of the Figure block can contain various directives to alter how +the image will be generated. These can be comma separated to give multiple +metadata directives: + +- `NORENDER`: Don't generate an image for this example, but show the example text. +- `Hide`: Generate, but don't show script or image. This can be used to generate images to be manually displayed in markdown text blocks. - `2D`: Orient camera in a top-down view for showing 2D objects. -- `3D`: Orient camera in an oblique view for showing 3D objects. Used to force an Example sub-block to generate an image in Function and Constant blocks. -- `NORENDER`: Don't generate an image for this example. -- `Small`: Make the image small sized. (The default) +- `3D`: Orient camera in an oblique view for showing 3D objects. +- `VPD=440`: Force viewpoint distance `$vpd` to 440. +- `VPT=[10,20,30]` Force the viewpoint translation `$vpt` to `[10,20,30]`. +- `VPR=[55,0,600]` Force the viewpoint rotation `$vpr` to `[55,0,60]`. +- `Spin`: Animate camera orbit around the `[0,1,1]` axis to display all sides of an object. +- `FlatSpin`: Animate camera orbit around the Z axis, above the XY plane. +- `Anim`: Make an animation where `$t` varies from `0.0` to almost `1.0`. +- `Frames=36`: Number of animation frames to make. +- `FrameMS=250`: Sets the number of milliseconds per frame for spins and animation. +- `Small`: Make the image small sized. - `Med`: Make the image medium sized. - `Big`: Make the image big sized. - `Huge`: Make the image huge sized. +- `Size=880x640`: Make the image 880 by 640 pixels in size. +- `Render`: Force full rendering from OpenSCAD, instead of the normal preview. +- `Edges`: Highlight face edges. +- `NoAxes`: Hides the axes and scales. +- `ScriptUnder`: Display script text under image, rather than beside it. + + +Example Block +------------- + +An Example block shows a script, and possibly shows an image generated from it. +The script is in the multi-line body. The `Examples` (plural) block does +the same, but it treats eash body line as a separate Example bloc to show. +Any images, if generated, will be created by running it in OpenSCAD: + + // Example: Example description + // cylinder(h=100, d1=75, d2=50); + // up(100) cylinder(h=100, d1=50, d2=75); + // Example(Spin,VPD=444): Animated shape that spins to show all faces. + // cube([10,100,50], center=true); + // cube([100,10,30], center=true); + // Examples: + // cube(100); + // cylinder(h=100,d=50); + // sphere(d=100); + +Which outputs Markdown code that renders like: + +> **Example 1:** Example description +> ```openscad +> cylinder(h=100, d1=75, d2=50); +> up(100) cylinder(h=100, d1=50, d2=75); +> ``` +> GENERATED IMAGE SHOWN HERE +> +> **Example 2:** Animated shape that spins to show all faces. +> ```openscad +> cube([10,100,50], center=true); +> cube([100,10,30], center=true); +> ``` +> GENERATED IMAGE SHOWN HERE +> +> **Example 3:** +> ```openscad +> cube(100); +> ``` +> GENERATED IMAGE OF CUBE SHOWN HERE +> +> **Example 4:** +> ```openscad +> cylinder(h=100,d=50); +> ``` +> GENERATED IMAGE OF CYLINDER SHOWN HERE +> +> **Example 5:** +> ```openscad +> sphere(d=100); +> ``` +> GENERATED IMAGE OF SPHERE SHOWN HERE + +The metadata of the Example block can contain various directives to alter how +the image will be generated. These can be comma separated to give multiple +metadata directives: + +- `NORENDER`: Don't generate an image for this example, but show the example text. +- `Hide`: Generate, but don't show script or image. This can be used to generate images to be manually displayed in markdown text blocks. +- `2D`: Orient camera in a top-down view for showing 2D objects. +- `3D`: Orient camera in an oblique view for showing 3D objects. Often used to force an Example sub-block to generate an image in Function and Constant blocks. +- `VPD=440`: Force viewpoint distance `$vpd` to 440. +- `VPT=[10,20,30]` Force the viewpoint translation `$vpt` to `[10,20,30]`. +- `VPR=[55,0,600]` Force the viewpoint rotation `$vpr` to `[55,0,60]`. - `Spin`: Animate camera orbit around the `[0,1,1]` axis to display all sides of an object. - `FlatSpin`: Animate camera orbit around the Z axis, above the XY plane. -- `FR`: Force full rendering from OpenSCAD, instead of the normal preview. +- `Anim`: Make an animation where `$t` varies from `0.0` to almost `1.0`. +- `FrameMS=250`: Sets the number of milliseconds per frame for spins and animation. +- `Frames=36`: Number of animation frames to make. +- `Small`: Make the image small sized. +- `Med`: Make the image medium sized. +- `Big`: Make the image big sized. +- `Huge`: Make the image huge sized. +- `Size=880x640`: Make the image 880 by 640 pixels in size. +- `Render`: Force full rendering from OpenSCAD, instead of the normal preview. - `Edges`: Highlight face edges. +- `NoAxes`: Hides the axes and scales. +- `ScriptUnder`: Display script text under image, rather than beside it. -Indentation is important, as it denotes the end of sub-block. +Modules will default to generating and displaying the image as if the `3D` +directive is given. Functions and constants will default to not generating +an image unless `3D`, `Spin`, `FlatSpin` or `Anim` is explicitly given. -``` -// Module: foo() -// Status: DEPRECATED, use BLAH instead. -// Usage: Optional Usage Description -// foo(foo, bar, [qux]); -// foo(bar, baz, [qux]); -// Usage: Another Optional Usage Description -// foo(foo, flee, flie, [qux]) -// Description: Short description. -// Description: -// A longer, multi-line description. -// All description blocks are added together. -// You _can_ use *markdown* notation as well. -// You can have paragraph breaks by having a -// line with just a period, like this: -// . -// You can end multi-line blocks by un-indenting the next -// line, or by using a comment with no spaces like this: -// -// Figure: Figure description -// cylinder(h=100, d1=75, d2=50); -// up(100) cylinder(h=100, d1=50, d2=75); -// Figure(Spin): Animated figure that spins to show all faces. -// cube([10,100,50], center=true); -// cube([100,10,30], center=true); -// -// Arguments: -// foo = This is the description of the foo argument. All on one line. -// bar = This is the description of the bar argument. All on one line. -// baz = This is the description of the baz argument. All on one line. -// qux = This is the description of the qux argument. All on one line. -// flee = This is the description of the flee argument. All on one line. -// flie = This is the description of the flie argument. All on one line. -// Side Effects: -// `$floo` gets set to the floo value. -// Extra Anchors: -// "blawb" = An anchor at the blawb point of the part, oriented upwards. -// "fewble" = An anchor at the fewble connector of the part, oriented back yowards Y+. -// Examples: Each line below gets its own example block and image. -// foo(foo="a", bar="b"); -// foo(foo="b", baz="c"); -// Example: Multi-line example. -// lst = [ -// "multi-line examples", -// "are shown in one block", -// "with a single image.", -// ]; -// foo(lst, 23, "blah"); -// Example(2D): Example to show as 2D top-down rendering. -// foo(foo="b", baz="c", qux=true); -// Example(Spin): Example orbiting the [0,1,1] axis. -// foo(foo="b", baz="c", qux="full"); -// Example(FlatSpin): Example orbiting the Z axis from above. -// foo(foo="b", baz="c", qux="full2"); -``` +If any lines of the Example script begin with `--`, then they are not shown in +the example script output to the documentation, but they *are* included in the +script used to generate the example image, without the `--`, of course: + + // Example: Multi-line example. + // --$fn = 72; // Lines starting with -- aren't shown in docs example text. + // lst = [ + // "multi-line examples", + // "are shown in one block", + // "with a single image.", + // ]; + // foo(lst, 23, "blah"); + + +Creating Custom Block Headers +============================= + +If you have need of a non-standard documentation block in your docs, you can declare the new block type using `DefineHeader:`. This has the syntax: + + // DefineHeader(TYPE): NEWBLOCKNAME + +Where NEWBLOCKNAME is the name of the new block header, and TYPE defines the behavior of the new block. TYPE can be one of: + +- `Generic`: Show both the TitleText and body. +- `Text`: Show the TitleText as the first line of the body. +- `Label`: Show only the TitleText and no body. +- `NumList`: Shows TitleText, and the body lines in a numbered list. +- `BulletListList`: Shows TitleText, and the body lines in a bullet list. +- `Table`: Shows TitleText, and body lines in a definition table. +- `Figure`: Shows TitleText, and an image rendered from the script in the Body. +- `Example`: Like Figure, but also shows the body as an example script. + + +Generic Block Type +------------------ + +The Generic block header type takes both title and body lines and generates a markdown block that has the block header, title, and a following body: + + // DefineHeader(Generic): Result + // Result: For Typical Cases + // Does typical things. + // Or something like that. + // Refer to {{stuff()}} for more info. + // Result: For Atypical Cases + // Performs an atypical thing. + +Which outputs Markdown code that renders like: + +> **Result:** For Typical Cases +> +> Does typical things. +> Or something like that. +> Refer to [stuff()](foobar.scad#function-stuff) for more info. +> +> **Result:** For Atypical Cases +> +> Performs an atypical thing. +> + + +Text Block Type +--------------- + +The Text block header type is similar to the Generic type, except it merges +the title into the body. This is useful for allowing single-line or multi- +line blocks: + + // DefineHeader(Text): Reason + // Reason: This is a simple reason. + // Reason: This is a complex reason. + // It is a multi-line explanation + // about why this does what it does. + // Refer to {{nonsense()}} for more info. + +Which outputs Markdown code that renders like: + +> **Reason:** +> +> This is a simple reason. +> +> **Reason:** +> +> This is a complex reason. +> It is a multi-line explanation +> about why this does what it does. +> Refer to [nonsense()](foobar.scad#function-nonsense) for more info. + + +Label Block Type +---------------- + +The Label block header type takes just the title, and shows it with the header: + + // DefineHeader(Label): Regions + // Regions: Antarctica, New Zealand + // Regions: Europe, Australia + +Which outputs Markdown code that renders like: + +> **Regions:** Antarctica, New Zealand +> **Regions:** Europe, Australia + + +NumList Block Type +------------------ + +The NumList block header type takes both title and body lines, and outputs a +numbered list block: + + // DefineHeader(NumList): Steps + // Steps: How to handle being on fire. + // Stop running around and panicing. + // Drop to the ground. Refer to {{drop()}}. + // Roll on the ground to smother the flames. + +Which outputs Markdown code that renders like: + +> **Steps:** How to handle being on fire. +> +> 1. Stop running around and panicing. +> 2. Drop to the ground. Refer to [drop()](foobar.scad#function-drop). +> 3. Roll on the ground to smother the flames. +> + + +BulletList Block Type +--------------------- + +The BulletList block header type takes both title and body lines: + + // DefineHeader(BulletList): Side Effects + // Side Effects: For Typical Uses + // The variable {{$foo}} gets set. + // The default for subsequent calls is updated. + +Which outputs Markdown code that renders like: + +> **Side Effects:** For Typical Uses +> +> - The variable [$foo](foobar.scad#function-foo) gets set. +> - The default for subsequent calls is updated. +> + + +Table Block Type +------------------ + +The Table block header type outputs a header block with the title, followed by +one or more tables. This is genertally meant for definition lists. The header +names are given in the DefineHeader metadata. Header names are separated by +`|` (vertical bar, or pipe) characters, and sets of headers (for multiple +tables) are separated by `||` (two vertical bars). A header that starts with +the `^` (hat, or circumflex) character, will cause the items in that column +to be surrounded by \`foo\` literal markers. Cells in the body content are +separated by `=` (equals signs): + + // DefineHeader(Table:^Link Name|Description): Anchors + // Anchors: by Name + // "link1" = Anchor for the joiner Located at the {{BACK}} side of the shape. + // "a"/"b" = Anchor for the joiner Located at the {{FRONT}} side of the shape. + +Which outputs Markdown code that renders like: + +> **Anchors:** by Name +> +> Link Name | Description +> -------------- | -------------------- +> `"link1"` | Anchor for the joiner at the [BACK](constants.scad#constant-back) side of the shape. +> `"a"` / `"b"` | Anchor for the joiner at the [FRONT](constants.scad#constant-front) side of the shape. +> + +You can have multiple subtables, separated by a line with only three dashes: `---`: + + // DefineHeader(Table:^Pos Arg|What it Does||^Names Arg|What it Does): Args + // Args: + // foo = The foo argument. + // bar = The bar argument. + // --- + // baz = The baz argument. + // qux = The baz argument. + +Which outputs Markdown code that renders like: + +> **Args:** +> +> Pos Arg | What it Does +> ----------- | -------------------- +> `foo` | The foo argument. +> `bar` | The bar argument. +> +> Named Arg | What it Does +> ----------- | -------------------- +> `baz` | The baz argument. +> `qux` | The qux argument. +> + +Defaults Configuration +====================== + +The `openscad_decsgen` script looks for an `.openscad_docsgen_rc` file in +the source code directory it is run in. In that file, you can give a few +defaults for what files will be processed, and where to save the generated +markdown documentation. + +To ignore specific files, to prevent generating documentation for them, you +can use the IgnoreFiles block. Note that the commentline prefix is not +needed in the configuration file: + + IgnoreFiles: + ignored1.scad + ignored2.scad + +To prioritize the ordering of files when generating the Table of Contents +and other indices, you can use the PrioritizeFiles block: + + PrioritizeFiles: + file1.scad + file2.scad + +To specify what directory to write the markdown output documentation to, you +can use the DocsDirectory block: + + DocsDirectory: wiki_dir + +You can also use the DefineHeader block in the config file to make custom +block headers: + + DefineHeader(Text): Returns + DefineHeader(BulletList): Side Effects + DefineHeader(Table:^Anchor Name|Position): Extra Anchors diff --git a/affine.scad b/affine.scad index dbedf1f5..55c3ea35 100644 --- a/affine.scad +++ b/affine.scad @@ -1,33 +1,237 @@ ////////////////////////////////////////////////////////////////////// // LibFile: affine.scad // Matrix math and affine transformation matrices. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// // Section: Matrix Manipulation // Function: ident() -// Description: Create an `n` by `n` identity matrix. +// Usage: +// mat = ident(n); +// Topics: Affine, Matrices +// Description: +// Create an `n` by `n` square identity matrix. // Arguments: // n = The size of the identity matrix square, `n` by `n`. -function ident(n) = [for (i = [0:1:n-1]) [for (j = [0:1:n-1]) (i==j)?1:0]]; +// Example: +// mat = ident(3); +// // Returns: +// // [ +// // [1, 0, 0], +// // [0, 1, 0], +// // [0, 0, 1] +// // ] +// Example: +// mat = ident(4); +// // Returns: +// // [ +// // [1, 0, 0, 0], +// // [0, 1, 0, 0], +// // [0, 0, 1, 0], +// // [0, 0, 0, 1] +// // ] +function ident(n) = [ + for (i = [0:1:n-1]) [ + for (j = [0:1:n-1]) (i==j)? 1 : 0 + ] +]; + + +// Function: is_affine() +// Usage: +// bool = is_affine(x,); +// Topics: Affine, Matrices, Transforms, Type Checking +// See Also: is_matrix() +// Description: +// Tests if the given value is an affine matrix, possibly also checking it's dimenstion. +// Arguments: +// x = The value to test for being an affine matrix. +// dim = The number of dimensions the given affine is required to be for. Generally 2 for 2D or 3 for 3D. If given as a list of integers, allows any of the given dimensions. Default: `[2,3]` +// Examples: +// bool = is_affine(affine2d_scale([2,3])); // Returns true +// bool = is_affine(affine3d_scale([2,3,4])); // Returns true +// bool = is_affine(affine3d_scale([2,3,4]),2); // Returns false +// bool = is_affine(affine3d_scale([2,3]),2); // Returns true +// bool = is_affine(affine3d_scale([2,3,4]),3); // Returns true +// bool = is_affine(affine3d_scale([2,3]),3); // Returns false +function is_affine(x,dim=[2,3]) = + is_finite(dim)? is_affine(x,[dim]) : + let( ll = len(x) ) + is_list(x) && in_list(ll-1,dim) && + [for (r=x) if(!is_list(r) || len(r)!=ll) 1] == []; + + +// Function: is_2d_transform() +// Usage: +// x = is_2d_transform(t); +// Topics: Affine, Matrices, Transforms, Type Checking +// See Also: is_affine(), is_matrix() +// Description: +// Checks if the input is a 3D transform that does not act on the z coordinate, except possibly +// for a simple scaling of z. Note that an input which is only a zscale returns false. +// Arguments: +// t = The transformation matrix to check. +// Examples: +// b = is_2d_transform(zrot(45)); // Returns: true +// b = is_2d_transform(yrot(45)); // Returns: false +// b = is_2d_transform(xrot(45)); // Returns: false +// b = is_2d_transform(move([10,20,0])); // Returns: true +// b = is_2d_transform(move([10,20,30])); // Returns: false +// b = is_2d_transform(scale([2,3,4])); // Returns: true +function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works + t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && + (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() // Function: affine2d_to_3d() -// Description: Takes a 3x3 affine2d matrix and returns its 4x4 affine3d equivalent. -function affine2d_to_3d(m) = concat( - [for (r = [0:2]) - concat( - [for (c = [0:2]) m[r][c]], - [0] - ) - ], - [[0, 0, 0, 1]] -); +// Usage: +// mat = affine2d_to_3d(m); +// Topics: Affine, Matrices, Transforms +// See Also: affine3d_to_2d() +// Description: +// Takes a 3x3 affine2d matrix and returns its 4x4 affine3d equivalent. +// Example: +// mat = affine2d_to_3d(affine2d_translate([10,20])); +// // Returns: +// // [ +// // [1, 0, 0, 10], +// // [0, 1, 0, 20], +// // [0, 0, 1, 0], +// // [0, 0, 0, 1], +// // ] +function affine2d_to_3d(m) = [ + [ m[0][0], m[0][1], 0, m[0][2] ], + [ m[1][0], m[1][1], 0, m[1][2] ], + [ 0, 0, 1, 0 ], + [ m[2][0], m[2][1], 0, m[2][2] ] +]; + + +// Function: affine3d_to_2d() +// Usage: +// mat = affine3d_to_2d(m); +// Topics: Affine, Matrices +// See Also: affine2d_to_3d() +// Description: +// Takes a 4x4 affine3d matrix and returns its 3x3 affine2d equivalent. 3D transforms that would alter the Z coordinate are disallowed. +// Example: +// mat = affine2d_to_3d(affine3d_translate([10,20,0])); +// // Returns: +// // [ +// // [1, 0, 10], +// // [0, 1, 20], +// // [0, 0, 1], +// // ] +function affine3d_to_2d(m) = + assert(is_2d_transform(m)) + [ + for (r=[0:3]) if (r!=2) [ + for (c=[0:3]) if (c!=2) m[r][c] + ] + ]; + + +// Function: apply() +// Usage: +// pts = apply(transform, points); +// Topics: Affine, Matrices, Transforms +// Description: +// Applies the specified transformation matrix to a point, pointlist, bezier patch or VNF. +// Both inputs can be 2D or 3D, and it is also allowed to supply 3D transformations with 2D +// data as long as the the only action on the z coordinate is a simple scaling. +// Arguments: +// transform = The 2D or 3D transformation matrix to apply to the point/points. +// points = The point, pointlist, bezier patch, or VNF to apply the transformation to. +// Example(3D): +// path1 = path3d(circle(r=40)); +// tmat = xrot(45); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +// Example(2D): +// path1 = circle(r=40); +// tmat = translate([10,5]); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +// Example(2D): +// path1 = circle(r=40); +// tmat = rot(30) * back(15) * scale([1.5,0.5,1]); +// path2 = apply(tmat, path1); +// #stroke(path1,closed=true); +// stroke(path2,closed=true); +function apply(transform,points) = + points==[] ? [] : + is_vector(points) + ? /* Point */ apply(transform, [points])[0] : + is_list(points) && len(points)==2 && is_path(points[0],3) && is_list(points[1]) && is_vector(points[1][0]) + ? /* VNF */ [apply(transform, points[0]), points[1]] : + is_list(points) && is_list(points[0]) && is_vector(points[0][0]) + ? /* BezPatch */ [for (x=points) apply(transform,x)] : + let( + tdim = len(transform[0])-1, + datadim = len(points[0]) + ) + tdim == 3 && datadim == 3 ? [for(p=points) point3d(transform*concat(p,[1]))] : + tdim == 2 && datadim == 2 ? [for(p=points) point2d(transform*concat(p,[1]))] : + tdim == 3 && datadim == 2 ? + assert(is_2d_transform(transform), str("Transforms is 3d but points are 2d")) + [for(p=points) point2d(transform*concat(p,[0,1]))] : + assert(false, str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); + + +// Function: rot_decode() +// Usage: +// info = rot_decode(rotation); // Returns: [angle,axis,cp,translation] +// Topics: Affine, Matrices, Transforms +// Description: +// Given an input 3D rigid transformation operator (one composed of just rotations and translations) represented +// as a 4x4 matrix, compute the rotation and translation parameters of the operator. Returns a list of the +// four parameters, the angle, in the interval [0,180], the rotation axis as a unit vector, a centerpoint for +// the rotation, and a translation. If you set `parms = rot_decode(rotation)` then the transformation can be +// reconstructed from parms as `move(parms[3]) * rot(a=parms[0],v=parms[1],cp=parms[2])`. This decomposition +// makes it possible to perform interpolation. If you construct a transformation using `rot` the decoding +// may flip the axis (if you gave an angle outside of [0,180]). The returned axis will be a unit vector, and +// the centerpoint lies on the plane through the origin that is perpendicular to the axis. It may be different +// than the centerpoint you used to construct the transformation. +// Example: +// info = rot_decode(rot(45)); +// // Returns: [45, [0,0,1], [0,0,0], [0,0,0]] +// Example: +// info = rot_decode(rot(a=37, v=[1,2,3], cp=[4,3,-7]))); +// // Returns: [37, [0.26, 0.53, 0.80], [4.8, 4.6, -4.6], [0,0,0]] +// Example: +// info = rot_decode(left(12)*xrot(-33)); +// // Returns: [33, [-1,0,0], [0,0,0], [-12,0,0]] +// Example: +// info = rot_decode(translate([3,4,5])); +// // Returns: [0, [0,0,1], [0,0,0], [3,4,5]] +function rot_decode(M) = + assert(is_matrix(M,4,4) && approx(M[3],[0,0,0,1]), "Input matrix must be a 4x4 matrix representing a 3d transformation") + let(R = submatrix(M,[0:2],[0:2])) + assert(approx(det3(R),1) && approx(norm_fro(R * transpose(R)-ident(3)),0),"Input matrix is not a rotation") + let( + translation = [for(row=[0:2]) M[row][3]], // translation vector + largest = max_index([R[0][0], R[1][1], R[2][2]]), + axis_matrix = R + transpose(R) - (matrix_trace(R)-1)*ident(3), // Each row is on the rotational axis + // Construct quaternion q = c * [x sin(theta/2), y sin(theta/2), z sin(theta/2), cos(theta/2)] + q_im = axis_matrix[largest], + q_re = R[(largest+2)%3][(largest+1)%3] - R[(largest+1)%3][(largest+2)%3], + c_sin = norm(q_im), // c * sin(theta/2) for some c + c_cos = abs(q_re) // c * cos(theta/2) + ) + approx(c_sin,0) ? [0,[0,0,1],[0,0,0],translation] : + let( + angle = 2*atan2(c_sin, c_cos), // This is supposed to be more accurate than acos or asin + axis = (q_re>=0 ? 1:-1)*q_im/c_sin, + tproj = translation - (translation*axis)*axis, // Translation perpendicular to axis determines centerpoint + cp = (tproj + cross(axis,tproj)*c_cos/c_sin)/2 + ) + [angle, axis, cp, (translation*axis)*axis]; + @@ -35,54 +239,135 @@ function affine2d_to_3d(m) = concat( // Function: affine2d_identity() -// Description: Create a 3x3 affine2d identity matrix. +// Usage: +// mat = affine2d_identify(); +// Topics: Affine, Matrices, Transforms +// Description: +// Create a 3x3 affine2d identity matrix. +// Example: +// mat = affine2d_identity(); +// // Returns: +// // [ +// // [1, 0, 0], +// // [0, 1, 0], +// // [0, 0, 1] +// // ] function affine2d_identity() = ident(3); // Function: affine2d_translate() +// Usage: +// mat = affine2d_translate(v); +// Topics: Affine, Matrices, Transforms, Translation +// See Also: move(), affine3d_translate() // Description: // Returns the 3x3 affine2d matrix to perform a 2D translation. // Arguments: // v = 2D Offset to translate by. [X,Y] -function affine2d_translate(v) = [ - [1, 0, v.x], - [0, 1, v.y], - [0 ,0, 1] -]; +// Example: +// mat = affine2d_translate([30,40]); +// // Returns: +// // [ +// // [1, 0, 30], +// // [0, 1, 40], +// // [0, 0, 1] +// // ] +function affine2d_translate(v=[0,0]) = + assert(is_vector(v),2) + [ + [1, 0, v.x], + [0, 1, v.y], + [0 ,0, 1] + ]; // Function: affine2d_scale() +// Usage: +// mat = affine2d_scale(v); +// Topics: Affine, Matrices, Transforms, Scaling +// See Also: scale(), xscale(), yscale(), zscale(), affine3d_scale() // Description: // Returns the 3x3 affine2d matrix to perform a 2D scaling transformation. // Arguments: // v = 2D vector of scaling factors. [X,Y] -function affine2d_scale(v) = [ - [v.x, 0, 0], - [ 0, v.y, 0], - [ 0, 0, 1] -]; +// Example: +// mat = affine2d_scale([3,4]); +// // Returns: +// // [ +// // [3, 0, 0], +// // [0, 4, 0], +// // [0, 0, 1] +// // ] +function affine2d_scale(v=[1,1]) = + assert(is_vector(v,2)) + [ + [v.x, 0, 0], + [ 0, v.y, 0], + [ 0, 0, 1] + ]; // Function: affine2d_zrot() +// Usage: +// mat = affine2d_zrot(ang); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine3d_zrot() // Description: // Returns the 3x3 affine2d matrix to perform a rotation of a 2D vector around the Z axis. // Arguments: // ang = Number of degrees to rotate. -function affine2d_zrot(ang) = [ - [cos(ang), -sin(ang), 0], - [sin(ang), cos(ang), 0], - [ 0, 0, 1] -]; +// Example: +// mat = affine2d_zrot(90); +// // Returns: +// // [ +// // [0,-1, 0], +// // [1, 0, 0], +// // [0, 0, 1] +// // ] +function affine2d_zrot(ang=0) = + assert(is_finite(ang)) + [ + [cos(ang), -sin(ang), 0], + [sin(ang), cos(ang), 0], + [ 0, 0, 1] + ]; // Function: affine2d_mirror() // Usage: // mat = affine2d_mirror(v); +// Topics: Affine, Matrices, Transforms, Reflection, Mirroring +// See Also: mirror(), xflip(), yflip(), zflip(), affine3d_mirror() // Description: // Returns the 3x3 affine2d matrix to perform a reflection of a 2D vector across the line given by its normal vector. // Arguments: // v = The normal vector of the line to reflect across. +// Example: +// mat = affine2d_mirror([0,1]); +// // Returns: +// // [ +// // [ 1, 0, 0], +// // [ 0,-1, 0], +// // [ 0, 0, 1] +// // ] +// Example: +// mat = affine2d_mirror([1,0]); +// // Returns: +// // [ +// // [-1, 0, 0], +// // [ 0, 1, 0], +// // [ 0, 0, 1] +// // ] +// Example: +// mat = affine2d_mirror([1,1]); +// // Returns approximately: +// // [ +// // [ 0,-1, 0], +// // [-1, 0, 0], +// // [ 0, 0, 1] +// // ] function affine2d_mirror(v) = + assert(is_vector(v,2)) let(v=unit(point2d(v)), a=v.x, b=v.y) [ [1-2*a*a, 0-2*a*b, 0], @@ -93,29 +378,32 @@ function affine2d_mirror(v) = // Function: affine2d_skew() // Usage: -// affine2d_skew(xa, ya) +// mat = affine2d_skew(xa); +// mat = affine2d_skew(ya=); +// mat = affine2d_skew(xa, ya); +// Topics: Affine, Matrices, Transforms, Skewing +// See Also: skew(), affine3d_skew() // Description: // Returns the 3x3 affine2d matrix to skew a 2D vector along the XY plane. // Arguments: -// xa = Skew angle, in degrees, in the direction of the X axis. -// ya = Skew angle, in degrees, in the direction of the Y axis. -function affine2d_skew(xa, ya) = [ - [1, tan(xa), 0], - [tan(ya), 1, 0], - [0, 0, 1] -]; - - -// Function: affine2d_chain() -// Usage: -// affine2d_chain(affines) -// Description: -// Returns a 3x3 affine2d transformation matrix which results from applying each matrix in `affines` in order. -// Arguments: -// affines = A list of 3x3 affine2d matrices. -function affine2d_chain(affines, _m=undef, _i=0) = - (_i>=len(affines))? (is_undef(_m)? ident(3) : _m) : - affine2d_chain(affines, _m=(is_undef(_m)? affines[_i] : affines[_i] * _m), _i=_i+1); +// xa = Skew angle, in degrees, in the direction of the X axis. Default: 0 +// ya = Skew angle, in degrees, in the direction of the Y axis. Default: 0 +// Example: +// mat = affine2d_skew(xa=45,ya=-45); +// // Returns approximately: +// // [ +// // [ 1, 1, 0], +// // [-1, 1, 0], +// // [ 0, 0, 1] +// // ] +function affine2d_skew(xa=0, ya=0) = + assert(is_finite(xa)) + assert(is_finite(ya)) + [ + [1, tan(xa), 0], + [tan(ya), 1, 0], + [0, 0, 1] + ]; @@ -123,86 +411,187 @@ function affine2d_chain(affines, _m=undef, _i=0) = // Function: affine3d_identity() -// Description: Create a 4x4 affine3d identity matrix. +// Usage: +// mat = affine3d_identity(); +// Topics: Affine, Matrices, Transforms +// Description: +// Create a 4x4 affine3d identity matrix. +// Example: +// mat = affine2d_identity(); +// // Returns: +// // [ +// // [1, 0, 0, 0], +// // [0, 1, 0, 0], +// // [0, 0, 1, 0], +// // [0, 0, 0, 1] +// // ] function affine3d_identity() = ident(4); // Function: affine3d_translate() +// Usage: +// mat = affine3d_translate(v); +// Topics: Affine, Matrices, Transforms, Translation +// See Also: move(), affine2d_translate() // Description: // Returns the 4x4 affine3d matrix to perform a 3D translation. // Arguments: // v = 3D offset to translate by. [X,Y,Z] -function affine3d_translate(v) = [ - [1, 0, 0, v.x], - [0, 1, 0, v.y], - [0, 0, 1, v.z], - [0 ,0, 0, 1] -]; +// Example: +// mat = affine2d_translate([30,40,50]); +// // Returns: +// // [ +// // [1, 0, 0, 30], +// // [0, 1, 0, 40], +// // [0, 0, 1, 50] +// // [0, 0, 0, 1] +// // ] +function affine3d_translate(v=[0,0,0]) = + assert(is_list(v)) + let( v = [for (i=[0:2]) default(v[i],0)] ) + [ + [1, 0, 0, v.x], + [0, 1, 0, v.y], + [0, 0, 1, v.z], + [0 ,0, 0, 1] + ]; // Function: affine3d_scale() +// Usage: +// mat = affine3d_scale(v); +// Topics: Affine, Matrices, Transforms, Scaling +// See Also: scale(), affine2d_scale() // Description: // Returns the 4x4 affine3d matrix to perform a 3D scaling transformation. // Arguments: // v = 3D vector of scaling factors. [X,Y,Z] -function affine3d_scale(v) = [ - [v.x, 0, 0, 0], - [ 0, v.y, 0, 0], - [ 0, 0, v.z, 0], - [ 0, 0, 0, 1] -]; +// Example: +// mat = affine3d_scale([3,4,5]); +// // Returns: +// // [ +// // [3, 0, 0, 0], +// // [0, 4, 0, 0], +// // [0, 0, 5, 0], +// // [0, 0, 0, 1] +// // ] +function affine3d_scale(v=[1,1,1]) = + assert(is_list(v)) + let( v = [for (i=[0:2]) default(v[i],1)] ) + [ + [v.x, 0, 0, 0], + [ 0, v.y, 0, 0], + [ 0, 0, v.z, 0], + [ 0, 0, 0, 1] + ]; // Function: affine3d_xrot() +// Usage: +// mat = affine3d_xrot(ang); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: // Returns the 4x4 affine3d matrix to perform a rotation of a 3D vector around the X axis. // Arguments: // ang = number of degrees to rotate. -function affine3d_xrot(ang) = [ - [1, 0, 0, 0], - [0, cos(ang), -sin(ang), 0], - [0, sin(ang), cos(ang), 0], - [0, 0, 0, 1] -]; +// Example: +// mat = affine3d_xrot(90); +// // Returns: +// // [ +// // [1, 0, 0, 0], +// // [0, 0,-1, 0], +// // [0, 1, 0, 0], +// // [0, 0, 0, 1] +// // ] +function affine3d_xrot(ang=0) = + assert(is_finite(ang)) + [ + [1, 0, 0, 0], + [0, cos(ang), -sin(ang), 0], + [0, sin(ang), cos(ang), 0], + [0, 0, 0, 1] + ]; // Function: affine3d_yrot() +// Usage: +// mat = affine3d_yrot(ang); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: // Returns the 4x4 affine3d matrix to perform a rotation of a 3D vector around the Y axis. // Arguments: // ang = Number of degrees to rotate. -function affine3d_yrot(ang) = [ - [ cos(ang), 0, sin(ang), 0], - [ 0, 1, 0, 0], - [-sin(ang), 0, cos(ang), 0], - [ 0, 0, 0, 1] -]; +// Example: +// mat = affine3d_yrot(90); +// // Returns: +// // [ +// // [ 0, 0, 1, 0], +// // [ 0, 1, 0, 0], +// // [-1, 0, 0, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_yrot(ang=0) = + assert(is_finite(ang)) + [ + [ cos(ang), 0, sin(ang), 0], + [ 0, 1, 0, 0], + [-sin(ang), 0, cos(ang), 0], + [ 0, 0, 0, 1] + ]; // Function: affine3d_zrot() // Usage: -// affine3d_zrot(ang) +// mat = affine3d_zrot(ang); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: // Returns the 4x4 affine3d matrix to perform a rotation of a 3D vector around the Z axis. // Arguments: // ang = number of degrees to rotate. -function affine3d_zrot(ang) = [ - [cos(ang), -sin(ang), 0, 0], - [sin(ang), cos(ang), 0, 0], - [ 0, 0, 1, 0], - [ 0, 0, 0, 1] -]; +// Example: +// mat = affine3d_zrot(90); +// // Returns: +// // [ +// // [ 0,-1, 0, 0], +// // [ 1, 0, 0, 0], +// // [ 0, 0, 1, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_zrot(ang=0) = + assert(is_finite(ang)) + [ + [cos(ang), -sin(ang), 0, 0], + [sin(ang), cos(ang), 0, 0], + [ 0, 0, 1, 0], + [ 0, 0, 0, 1] + ]; // Function: affine3d_rot_by_axis() // Usage: -// affine3d_rot_by_axis(u, ang); +// mat = affine3d_rot_by_axis(u, ang); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: // Returns the 4x4 affine3d matrix to perform a rotation of a 3D vector around an axis. // Arguments: // u = 3D axis vector to rotate around. // ang = number of degrees to rotate. -function affine3d_rot_by_axis(u, ang) = +// Example: +// mat = affine3d_rot_by_axis([1,1,1], 120); +// // Returns approx: +// // [ +// // [ 0, 0, 1, 0], +// // [ 1, 0, 0, 0], +// // [ 0, 1, 0, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_rot_by_axis(u=UP, ang=0) = + assert(is_finite(ang)) + assert(is_vector(u,3)) approx(ang,0)? affine3d_identity() : let( u = unit(u), @@ -219,13 +608,27 @@ function affine3d_rot_by_axis(u, ang) = // Function: affine3d_rot_from_to() // Usage: -// affine3d_rot_from_to(from, to); +// mat = affine3d_rot_from_to(from, to); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: // Returns the 4x4 affine3d matrix to perform a rotation of a 3D vector from one vector direction to another. // Arguments: // from = 3D axis vector to rotate from. // to = 3D axis vector to rotate to. +// Example: +// mat = affine3d_rot_from_to(UP, RIGHT); +// // Returns: +// // [ +// // [ 0, 0, 1, 0], +// // [ 0, 1, 0, 0], +// // [-1, 0, 0, 0], +// // [ 0, 0, 0, 1] +// // ] function affine3d_rot_from_to(from, to) = + assert(is_vector(from)) + assert(is_vector(to)) + assert(len(from)==len(to)) let( from = unit(point3d(from)), to = unit(point3d(to)) @@ -244,29 +647,35 @@ function affine3d_rot_from_to(from, to) = ]; -// Function: affine_frame_map() +// Function: affine3d_frame_map() // Usage: -// map = affine_frame_map(x=v1,y=v2); -// map = affine_frame_map(x=v1,z=v2); -// map = affine_frame_map(y=v1,y=v2); -// map = affine_frame_map(v1,v2,v3); +// map = affine3d_frame_map(v1, v2, v3, ); +// map = affine3d_frame_map(x=VECTOR1, y=VECTOR2, ); +// map = affine3d_frame_map(x=VECTOR1, z=VECTOR2, ); +// map = affine3d_frame_map(y=VECTOR1, z=VECTOR2, ); +// Topics: Affine, Matrices, Transforms, Rotation +// See Also: rot(), xrot(), yrot(), zrot(), affine2d_zrot() // Description: -// Returns a transformation that maps one coordinate frame to another. You must specify two or three of `x`, `y`, and `z`. The specified -// axes are mapped to the vectors you supplied. If you give two inputs, the third vector is mapped to the appropriate normal to maintain a right hand coordinate system. -// If the vectors you give are orthogonal the result will be a rotation and the `reverse` parameter will supply the inverse map, which enables you -// to map two arbitrary coordinate systems to each other by using the canonical coordinate system as an intermediary. You cannot use the `reverse` option -// with non-orthogonal inputs. +// Returns a transformation that maps one coordinate frame to another. You must specify two or +// three of `x`, `y`, and `z`. The specified axes are mapped to the vectors you supplied. If you +// give two inputs, the third vector is mapped to the appropriate normal to maintain a right hand +// coordinate system. If the vectors you give are orthogonal the result will be a rotation and the +// `reverse` parameter will supply the inverse map, which enables you to map two arbitrary +// coordinate systems to each other by using the canonical coordinate system as an intermediary. +// You cannot use the `reverse` option with non-orthogonal inputs. // Arguments: -// x = Destination vector for x axis -// y = Destination vector for y axis -// z = Destination vector for z axis +// x = Destination 3D vector for x axis. +// y = Destination 3D vector for y axis. +// z = Destination 3D vector for z axis. // reverse = reverse direction of the map for orthogonal inputs. Default: false -// Examples: -// T = affine_frame_map(x=[1,1,0], y=[-1,1,0]); // This map is just a rotation around the z axis -// T = affine_frame_map(x=[1,0,0], y=[1,1,0]); // This map is not a rotation because x and y aren't orthogonal -// // The next map sends [1,1,0] to [0,1,1] and [-1,1,0] to [0,-1,1] -// T = affine_frame_map(x=[0,1,1], y=[0,-1,1]) * affine_frame_map(x=[1,1,0], y=[-1,1,0],reverse=true); -function affine_frame_map(x,y,z, reverse=false) = +// Example: +// T = affine3d_frame_map(x=[1,1,0], y=[-1,1,0]); // This map is just a rotation around the z axis +// Example: +// T = affine3d_frame_map(x=[1,0,0], y=[1,1,0]); // This map is not a rotation because x and y aren't orthogonal +// Example: +// // The next map sends [1,1,0] to [0,1,1] and [-1,1,0] to [0,-1,1] +// T = affine3d_frame_map(x=[0,1,1], y=[0,-1,1]) * affine3d_frame_map(x=[1,1,0], y=[-1,1,0],reverse=true); +function affine3d_frame_map(x,y,z, reverse=false) = assert(num_defined([x,y,z])>=2, "Must define at least two inputs") let( xvalid = is_undef(x) || (is_vector(x) && len(x)==3), @@ -294,19 +703,40 @@ function affine_frame_map(x,y,z, reverse=false) = ) ) assert(ocheck, "Inputs must be orthogonal when reverse==true") - affine2d_to_3d(map) - ) : affine2d_to_3d(transpose(map)); + [for (r=map) [for (c=r) c, 0], [0,0,0,1]] + ) : [for (r=transpose(map)) [for (c=r) c, 0], [0,0,0,1]]; // Function: affine3d_mirror() // Usage: // mat = affine3d_mirror(v); +// Topics: Affine, Matrices, Transforms, Reflection, Mirroring +// See Also: mirror(), xflip(), yflip(), zflip(), affine2d_mirror() // Description: // Returns the 4x4 affine3d matrix to perform a reflection of a 3D vector across the plane given by its normal vector. // Arguments: // v = The normal vector of the plane to reflect across. +// Example: +// mat = affine3d_mirror([1,0,0]); +// // Returns: +// // [ +// // [-1, 0, 0, 0], +// // [ 0, 1, 0, 0], +// // [ 0, 0, 1, 0], +// // [ 0, 0, 0, 1] +// // ] +// Example: +// mat = affine3d_mirror([0,1,0]); +// // Returns: +// // [ +// // [ 1, 0, 0, 0], +// // [ 0,-1, 0, 0], +// // [ 0, 0, 1, 0], +// // [ 0, 0, 0, 1] +// // ] function affine3d_mirror(v) = + assert(is_vector(v)) let( v=unit(point3d(v)), a=v.x, b=v.y, c=v.z @@ -320,7 +750,9 @@ function affine3d_mirror(v) = // Function: affine3d_skew() // Usage: -// mat = affine3d_skew([sxy], [sxz], [syx], [syz], [szx], [szy]); +// mat = affine3d_skew(, , , , , ); +// Topics: Affine, Matrices, Transforms, Skewing +// See Also: skew(), affine3d_skew_xy(), affine3d_skew_xz(), affine3d_skew_yz(), affine2d_skew() // Description: // Returns the 4x4 affine3d matrix to perform a skew transformation. // Arguments: @@ -330,6 +762,15 @@ function affine3d_mirror(v) = // syz = Skew factor multiplier for skewing along the Y axis as you get farther from the Z axis. Default: 0 // szx = Skew factor multiplier for skewing along the Z axis as you get farther from the X axis. Default: 0 // szy = Skew factor multiplier for skewing along the Z axis as you get farther from the Y axis. Default: 0 +// Example: +// mat = affine3d_skew(sxy=2,szx=3); +// // Returns: +// // [ +// // [ 1, 2, 0, 0], +// // [ 0, 1, 0, 0], +// // [ 0, 0, 1, 0], +// // [ 3, 0, 0, 1] +// // ] function affine3d_skew(sxy=0, sxz=0, syx=0, syz=0, szx=0, szy=0) = [ [ 1, sxy, sxz, 0], [syx, 1, syz, 0], @@ -340,174 +781,98 @@ function affine3d_skew(sxy=0, sxz=0, syx=0, syz=0, szx=0, szy=0) = [ // Function: affine3d_skew_xy() // Usage: -// affine3d_skew_xy(xa, ya) +// mat = affine3d_skew_xy(xa); +// mat = affine3d_skew_xy(ya=); +// mat = affine3d_skew_xy(xa, ya); +// Topics: Affine, Matrices, Transforms, Skewing +// See Also: skew(), affine3d_skew(), affine3d_skew_xz(), affine3d_skew_yz(), affine2d_skew() // Description: // Returns the 4x4 affine3d matrix to perform a skew transformation along the XY plane. // Arguments: -// xa = Skew angle, in degrees, in the direction of the X axis. -// ya = Skew angle, in degrees, in the direction of the Y axis. -function affine3d_skew_xy(xa, ya) = [ - [1, 0, tan(xa), 0], - [0, 1, tan(ya), 0], - [0, 0, 1, 0], - [0, 0, 0, 1] -]; +// xa = Skew angle, in degrees, in the direction of the X axis. Default: 0 +// ya = Skew angle, in degrees, in the direction of the Y axis. Default: 0 +// Example: +// mat = affine3d_skew_xy(xa=45,ya=-45); +// // Returns: +// // [ +// // [ 1, 0, 1, 0], +// // [ 0, 1,-1, 0], +// // [ 0, 0, 1, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_skew_xy(xa=0, ya=0) = + assert(is_finite(xa)) + assert(is_finite(ya)) + [ + [1, 0, tan(xa), 0], + [0, 1, tan(ya), 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]; // Function: affine3d_skew_xz() // Usage: -// affine3d_skew_xz(xa, za) +// mat = affine3d_skew_xz(xa); +// mat = affine3d_skew_xz(za=); +// mat = affine3d_skew_xz(xa, za); +// Topics: Affine, Matrices, Transforms, Skewing +// See Also: skew(), affine3d_skew(), affine3d_skew_xy(), affine3d_skew_yz(), affine2d_skew() // Description: // Returns the 4x4 affine3d matrix to perform a skew transformation along the XZ plane. // Arguments: -// xa = Skew angle, in degrees, in the direction of the X axis. -// za = Skew angle, in degrees, in the direction of the Z axis. -function affine3d_skew_xz(xa, za) = [ - [1, tan(xa), 0, 0], - [0, 1, 0, 0], - [0, tan(za), 1, 0], - [0, 0, 0, 1] -]; +// xa = Skew angle, in degrees, in the direction of the X axis. Default: 0 +// za = Skew angle, in degrees, in the direction of the Z axis. Default: 0 +// Example: +// mat = affine3d_skew_xz(xa=45,za=-45); +// // Returns: +// // [ +// // [ 1, 1, 0, 0], +// // [ 0, 1, 0, 0], +// // [ 0,-1, 1, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_skew_xz(xa=0, za=0) = + assert(is_finite(xa)) + assert(is_finite(za)) + [ + [1, tan(xa), 0, 0], + [0, 1, 0, 0], + [0, tan(za), 1, 0], + [0, 0, 0, 1] + ]; // Function: affine3d_skew_yz() // Usage: -// affine3d_skew_yz(ya, za) +// mat = affine3d_skew_yz(ya); +// mat = affine3d_skew_yz(za=); +// mat = affine3d_skew_yz(ya, za); +// Topics: Affine, Matrices, Transforms, Skewing +// See Also: skew(), affine3d_skew(), affine3d_skew_xy(), affine3d_skew_xz(), affine2d_skew() // Description: // Returns the 4x4 affine3d matrix to perform a skew transformation along the YZ plane. // Arguments: -// ya = Skew angle, in degrees, in the direction of the Y axis. -// za = Skew angle, in degrees, in the direction of the Z axis. -function affine3d_skew_yz(ya, za) = [ - [ 1, 0, 0, 0], - [tan(ya), 1, 0, 0], - [tan(za), 0, 1, 0], - [ 0, 0, 0, 1] -]; - - -// Function: affine3d_chain() -// Usage: -// affine3d_chain(affines) -// Description: -// Returns a 4x4 affine3d transformation matrix which results from applying each matrix in `affines` in order. -// Arguments: -// affines = A list of 4x4 affine3d matrices. -function affine3d_chain(affines, _m=undef, _i=0) = - (_i>=len(affines))? (is_undef(_m)? ident(4) : _m) : - affine3d_chain(affines, _m=(is_undef(_m)? affines[_i] : affines[_i] * _m), _i=_i+1); - - -// Function: apply() -// Usage: -// pts = apply(transform, points) -// Description: -// Applies the specified transformation matrix to a point list (or single point). Both inputs can be 2d or 3d, and it is also allowed -// to supply 3d transformations with 2d data as long as the the only action on the z coordinate is a simple scaling. -// Examples: -// transformed = apply(xrot(45), path3d(circle(r=3))); // Rotates 3d circle data around x axis -// transformed = apply(rot(45), circle(r=3)); // Rotates 2d circle data by 45 deg -// transformed = apply(rot(45)*right(4)*scale(3), circle(r=3)); // Scales, translates and rotates 2d circle data -function apply(transform,points) = - points==[] ? [] : - is_vector(points) ? apply(transform, [points])[0] : - let( - tdim = len(transform[0])-1, - datadim = len(points[0]) - ) - tdim == 3 && datadim == 3 ? [for(p=points) point3d(transform*concat(p,[1]))] : - tdim == 2 && datadim == 2 ? [for(p=points) point2d(transform*concat(p,[1]))] : - tdim == 3 && datadim == 2 ? - assert(is_2d_transform(transform),str("Transforms is 3d but points are 2d")) - [for(p=points) point2d(transform*concat(p,[0,1]))] : - assert(false,str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); - - -// Function: apply_list() -// Usage: -// pts = apply_list(points, transform_list) -// Description: -// Transforms the specified point list (or single point) using a list of transformation matrices. Transformations on -// the list are applied in the order they appear in the list (as in right multiplication of matrices). Both inputs can be -// 2d or 3d, and it is also allowed to supply 3d transformations with 2d data as long as the the only action on the z coordinate -// is a simple scaling. All transformations on `transform_list` must have the same dimension: you cannot mix 2d and 3d transformations -// even when acting on 2d data. -// Examples: -// transformed = apply_list(path3d(circle(r=3)),[xrot(45)]); // Rotates 3d circle data around x axis -// transformed = apply_list(circle(r=3), [scale(3), right(4), rot(45)]); // Scales, then translates, and then rotates 2d circle data -function apply_list(points,transform_list) = - transform_list == []? points : - is_vector(points) ? apply_list([points],transform_list)[0] : - let( - tdims = array_dim(transform_list), - datadim = len(points[0]) - ) - assert(len(tdims)==3 || tdims[1]!=tdims[2], "Invalid transformation list") - let( tdim = tdims[1]-1 ) - tdim==2 && datadim == 2 ? apply(affine2d_chain(transform_list), points) : - tdim==3 && datadim == 3 ? apply(affine3d_chain(transform_list), points) : - tdim==3 && datadim == 2 ? - let( - badlist = [for(i=idx(transform_list)) if (!is_2d_transform(transform_list[i])) i] - ) - assert(badlist==[],str("Transforms with indices ",badlist," are 3d but points are 2d")) - apply(affine3d_chain(transform_list), points) : - assert(false,str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); - - -// Function: is_2d_transform() -// Usage: -// is_2d_transform(t) -// Description: -// Checks if the input is a 3d transform that does not act on the z coordinate, except -// possibly for a simple scaling of z. Note that an input which is only a zscale returns false. -function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works - t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && - (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() - - - -// Function: rot_decode() -// Usage: -// [angle,axis,cp,translation] = rot_decode(rotation) -// Description: -// Given an input 3d rigid transformation operator (one composed of just rotations and translations) -// represented as a 4x4 matrix, compute the rotation and translation parameters of the operator. -// Returns a list of the four parameters, the angle, in the interval [0,180], the rotation axis -// as a unit vector, a centerpoint for the rotation, and a translation. If you set `parms=rot_decode(rotation)` -// then the transformation can be reconstructed from parms as `move(parms[3])*rot(a=parms[0],v=parms[1],cp=parms[2])`. -// This decomposition makes it possible to perform interpolation. If you construct a transformation using `rot` -// the decoding may flip the axis (if you gave an angle outside of [0,180]). The returned axis will be a unit vector, -// and the centerpoint lies on the plane through the origin that is perpendicular to the axis. It may be different -// than the centerpoint you used to construct the transformation. +// ya = Skew angle, in degrees, in the direction of the Y axis. Default: 0 +// za = Skew angle, in degrees, in the direction of the Z axis. Default: 0 // Example: -// rot_decode(rot(45)); // Returns [45,[0,0,1], [0,0,0], [0,0,0]] -// rot_decode(rot(a=37, v=[1,2,3], cp=[4,3,-7]))); // Returns [37, [0.26, 0.53, 0.80], [4.8, 4.6, -4.6], [0,0,0]] -// rot_decode(left(12)*xrot(-33)); // Returns [33, [-1,0,0], [0,0,0], [-12,0,0]] -// rot_decode(translate([3,4,5])); // Returns [0, [0,0,1], [0,0,0], [3,4,5]] -function rot_decode(M) = - assert(is_matrix(M,4,4) && approx(M[3],[0,0,0,1]), "Input matrix must be a 4x4 matrix representing a 3d transformation") - let(R = submatrix(M,[0:2],[0:2])) - assert(approx(det3(R),1) && approx(norm_fro(R * transpose(R)-ident(3)),0),"Input matrix is not a rotation") - let( - translation = [for(row=[0:2]) M[row][3]], // translation vector - largest = max_index([R[0][0], R[1][1], R[2][2]]), - axis_matrix = R + transpose(R) - (matrix_trace(R)-1)*ident(3), // Each row is on the rotational axis - // Construct quaternion q = c * [x sin(theta/2), y sin(theta/2), z sin(theta/2), cos(theta/2)] - q_im = axis_matrix[largest], - q_re = R[(largest+2)%3][(largest+1)%3] - R[(largest+1)%3][(largest+2)%3], - c_sin = norm(q_im), // c * sin(theta/2) for some c - c_cos = abs(q_re) // c * cos(theta/2) - ) - approx(c_sin,0) ? [0,[0,0,1],[0,0,0],translation] : - let( - angle = 2*atan2(c_sin, c_cos), // This is supposed to be more accurate than acos or asin - axis = (q_re>=0 ? 1:-1)*q_im/c_sin, - tproj = translation - (translation*axis)*axis, // Translation perpendicular to axis determines centerpoint - cp = (tproj + cross(axis,tproj)*c_cos/c_sin)/2 - ) - [angle, axis, cp, (translation*axis)*axis]; - +// mat = affine3d_skew_yz(ya=45,za=-45); +// // Returns: +// // [ +// // [ 1, 0, 0, 0], +// // [ 1, 1, 0, 0], +// // [-1, 0, 1, 0], +// // [ 0, 0, 0, 1] +// // ] +function affine3d_skew_yz(ya=0, za=0) = + assert(is_finite(ya)) + assert(is_finite(za)) + [ + [ 1, 0, 0, 0], + [tan(ya), 1, 0, 0], + [tan(za), 0, 1, 0], + [ 0, 0, 0, 1] + ]; diff --git a/arrays.scad b/arrays.scad index b0a9a746..3fd8bbcb 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1,44 +1,47 @@ ////////////////////////////////////////////////////////////////////// // LibFile: arrays.scad // List and Array manipulation functions. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// -// Section: Terminology -// - **List**: An ordered collection of zero or more items. ie: `["a", "b", "c"]` -// - **Vector**: A list of numbers. ie: `[4, 5, 6]` -// - **Array**: A nested list of lists, or list of lists of lists, or deeper. ie: `[[2,3], [4,5], [6,7]]` -// - **Dimension**: The depth of nesting of lists in an array. A List is 1D. A list of lists is 2D. etc. -// - **Set**: A list of unique items. +// Terminology: +// **List** = An ordered collection of zero or more items. ie: `["a", "b", "c"]` +// **Vector** = A list of numbers. ie: `[4, 5, 6]` +// **Array** = A nested list of lists, or list of lists of lists, or deeper. ie: `[[2,3], [4,5], [6,7]]` +// **Dimension** = The depth of nesting of lists in an array. A List is 1D. A list of lists is 2D. etc. +// **Set** = A list of unique items. // Section: List Query Operations // Function: is_homogeneous() +// Alias: is_homogenous() // Usage: -// is_homogeneous(list,depth) +// bool = is_homogeneous(list,depth); +// Topics: List Handling, Type Checking +// See Also: is_vector(), is_matrix() // Description: -// Returns true when the list have elements of same type up to the depth `depth`. +// Returns true when the list has elements of same type up to the depth `depth`. // Booleans and numbers are not distinguinshed as of distinct types. // Arguments: -// list = the list to check +// l = the list to check // depth = the lowest level the check is done // Example: -// is_homogeneous( [[1,["a"]], [2,["b"]]] ) // Returns true -// is_homogeneous( [[1,["a"]], [2,[true]]] ) // Returns false -// is_homogeneous( [[1,["a"]], [2,[true]]], 1 ) // Returns true -// is_homogeneous( [[1,["a"]], [2,[true]]], 2 ) // Returns false -// is_homogeneous( [[1,["a"]], [true,["b"]]] ) // Returns true +// a = is_homogeneous([[1,["a"]], [2,["b"]]]); // Returns true +// b = is_homogeneous([[1,["a"]], [2,[true]]]); // Returns false +// c = is_homogeneous([[1,["a"]], [2,[true]]], 1); // Returns true +// d = is_homogeneous([[1,["a"]], [2,[true]]], 2); // Returns false +// e = is_homogeneous([[1,["a"]], [true,["b"]]]); // Returns true function is_homogeneous(l, depth=10) = !is_list(l) || l==[] ? false : let( l0=l[0] ) [] == [for(i=[1:len(l)-1]) if( ! _same_type(l[i],l0, depth+1) ) 0 ]; - + +function is_homogenous(l, depth=10) = is_homogeneous(l, depth); + function _same_type(a,b, depth) = (depth==0) || (is_undef(a) && is_undef(b)) || @@ -47,7 +50,7 @@ function _same_type(a,b, depth) = (is_string(a) && is_string(b)) || (is_list(a) && is_list(b) && len(a)==len(b) && []==[for(i=idx(a)) if( ! _same_type(a[i],b[i],depth-1) ) 0] ); - + // Function: select() // Description: @@ -55,62 +58,49 @@ function _same_type(a,b, depth) = // The first item is index 0. Negative indexes are counted back from the end. // The last item is -1. If only the `start` index is given, returns just the value // at that position. +// Topics: List Handling +// See Also: slice(), subindex(), last() // Usage: -// select(list,start) -// select(list,start,end) +// item = select(list,start); +// list = select(list,start,end); // Arguments: // list = The list to get the portion of. // start = The index of the first item. // end = The index of the last item. // Example: // l = [3,4,5,6,7,8,9]; -// select(l, 5, 6); // Returns [8,9] -// select(l, 5, 8); // Returns [8,9,3,4] -// select(l, 5, 2); // Returns [8,9,3,4,5] -// select(l, -3, -1); // Returns [7,8,9] -// select(l, 3, 3); // Returns [6] -// select(l, 4); // Returns 7 -// select(l, -2); // Returns 8 -// select(l, [1:3]); // Returns [4,5,6] -// select(l, [1,3]); // Returns [4,6] -function select(list, start, end=undef) = +// a = select(l, 5, 6); // Returns [8,9] +// b = select(l, 5, 8); // Returns [8,9,3,4] +// c = select(l, 5, 2); // Returns [8,9,3,4,5] +// d = select(l, -3, -1); // Returns [7,8,9] +// e = select(l, 3, 3); // Returns [6] +// f = select(l, 4); // Returns 7 +// g = select(l, -2); // Returns 8 +// h = select(l, [1:3]); // Returns [4,5,6] +// i = select(l, [1,3]); // Returns [4,6] +function select(list, start, end) = assert( is_list(list) || is_string(list), "Invalid list.") let(l=len(list)) - l==0 ? [] - : end==undef? - is_num(start)? - list[ (start%l+l)%l ] - : assert( is_list(start) || is_range(start), "Invalid start parameter") + l==0 + ? [] + : end==undef + ? is_num(start) + ? list[ (start%l+l)%l ] + : assert( is_list(start) || is_range(start), "Invalid start parameter") [for (i=start) list[ (i%l+l)%l ] ] - : assert(is_finite(start), "Invalid start parameter.") + : assert(is_finite(start), "Invalid start parameter.") assert(is_finite(end), "Invalid end parameter.") let( s = (start%l+l)%l, e = (end%l+l)%l ) - (s <= e)? [for (i = [s:1:e]) list[i]] - : concat([for (i = [s:1:l-1]) list[i]], [for (i = [0:1:e]) list[i]]) ; + (s <= e) + ? [for (i = [s:1:e]) list[i]] + : concat([for (i = [s:1:l-1]) list[i]], [for (i = [0:1:e]) list[i]]) ; -// Function: last() -// Description: -// Returns the last element of a list, or undef if empty. -// Usage: -// last(list) -// Arguments: -// list = The list to get the last element of. -// Example: -// l = [3,4,5,6,7,8,9]; -// last(l); // Returns 9. -function last(list) = list[len(list)-1]; - -// Function: delete_last() -// Description: -// Returns a list of all but the last entry. If input is empty, returns empty list. -// Usage: -// delete_last(list) -function delete_last(list) = - assert(is_list(list)) - list==[] ? [] : slice(list,0,-2); - // Function: slice() +// Usage: +// list = slice(list,start,end); +// Topics: List Handling +// See Also: select(), subindex(), last() // Description: // Returns a slice of a list. The first item is index 0. // Negative indexes are counted back from the end. The last item is -1. @@ -119,33 +109,110 @@ function delete_last(list) = // start = The index of the first item to return. // end = The index after the last item to return, unless negative, in which case the last item to return. // Example: -// slice([3,4,5,6,7,8,9], 3, 5); // Returns [6,7] -// slice([3,4,5,6,7,8,9], 2, -1); // Returns [5,6,7,8,9] -// slice([3,4,5,6,7,8,9], 1, 1); // Returns [] -// slice([3,4,5,6,7,8,9], 6, -1); // Returns [9] -// slice([3,4,5,6,7,8,9], 2, -2); // Returns [5,6,7,8] +// a = slice([3,4,5,6,7,8,9], 3, 5); // Returns [6,7] +// b = slice([3,4,5,6,7,8,9], 2, -1); // Returns [5,6,7,8,9] +// c = slice([3,4,5,6,7,8,9], 1, 1); // Returns [] +// d = slice([3,4,5,6,7,8,9], 6, -1); // Returns [9] +// e = slice([3,4,5,6,7,8,9], 2, -2); // Returns [5,6,7,8] function slice(list,start,end) = assert( is_list(list), "Invalid list" ) assert( is_finite(start) && is_finite(end), "Invalid number(s)" ) let( l = len(list) ) - l==0 ? [] - : let( - s = start<0? (l+start): start, - e = end<0? (l+end+1): end + l==0 + ? [] + : let( + s = start<0? (l+start) : start, + e = end<0? (l+end+1) : end ) [for (i=[s:1:e-1]) if (e>s) list[i]]; +// Function: last() +// Usage: +// item = last(list); +// Topics: List Handling +// See Also: select(), slice(), subindex() +// Description: +// Returns the last element of a list, or undef if empty. +// Arguments: +// list = The list to get the last element of. +// Example: +// l = [3,4,5,6,7,8,9]; +// x = last(l); // Returns 9. +function last(list) = + list[len(list)-1]; + + +// Function: delete_last() +// Usage: +// list = delete_last(list); +// Topics: List Handling +// See Also: select(), slice(), subindex(), last() +// Description: +// Returns a list with all but the last entry from the input list. If input is empty, returns empty list. +// Example: +// nlist = delete_last(["foo", "bar", "baz"]); // Returns: ["foo", "bar"] +function delete_last(list) = + assert(is_list(list)) + list==[] ? [] : slice(list,0,-2); + + +// Function: force_list() +// Usage: +// list = force_list(value, , ); +// Topics: List Handling +// See Also: scalar_vec3() +// Description: +// Coerces non-list values into a list. Makes it easy to treat a scalar input +// consistently as a singleton list, as well as list inputs. +// - If `value` is a list, then that list is returned verbatim. +// - If `value` is not a list, and `fill` is not given, then a list of `n` copies of `value` will be returned. +// - If `value` is not a list, and `fill` is given, then a list `n` items long will be returned where `value` will be the first item, and the rest will contain the value of `fill`. +// Arguments: +// value = The value or list to coerce into a list. +// n = The number of items in the coerced list. Default: 1 +// fill = The value to pad the coerced list with, after the firt value. Default: undef (pad with copies of `value`) +// Examples: +// x = force_list([3,4,5]); // Returns: [3,4,5] +// y = force_list(5); // Returns: [5] +// z = force_list(7, n=3); // Returns: [7,7,7] +// w = force_list(4, n=3, fill=1); // Returns: [4,1,1] +function force_list(value, n=1, fill) = + is_list(value) ? value : + is_undef(fill)? [for (i=[1:1:n]) value] : [value, for (i=[2:1:n]) fill]; + + +// Function: add_scalar() +// Usage: +// v = add_scalar(v,s); +// Topics: List Handling +// Description: +// Given a list and a scalar, returns the list with the scalar added to each item in it. +// If given a list of arrays, recursively adds the scalar to the each array. +// Arguments: +// v = The initial array. +// s = A scalar value to add to every item in the array. +// Example: +// a = add_scalar([1,2,3],3); // Returns: [4,5,6] +// b = add_scalar([[1,2,3],[3,4,5]],3); // Returns: [[4,5,6],[6,7,8]] +function add_scalar(v,s) = + is_finite(s) ? [for (x=v) is_list(x)? add_scalar(x,s) : is_finite(x) ? x+s: x] : v; + + // Function: in_list() -// Description: Returns true if value `val` is in list `list`. When `val==NAN` the answer will be false for any list. +// Usage: +// bool = in_list(val,list, ); +// Topics: List Handling +// Description: +// Returns true if value `val` is in list `list`. When `val==NAN` the answer will be false for any list. // Arguments: // val = The simple value to search for. // list = The list to search. // idx = If given, searches the given subindex for matches for `val`. // Example: -// in_list("bar", ["foo", "bar", "baz"]); // Returns true. -// in_list("bee", ["foo", "bar", "baz"]); // Returns false. -// in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. -function in_list(val,list,idx=undef) = +// a = in_list("bar", ["foo", "bar", "baz"]); // Returns true. +// b = in_list("bee", ["foo", "bar", "baz"]); // Returns false. +// c = in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. +function in_list(val,list,idx) = assert( is_list(list) && (is_undef(idx) || is_finite(idx)), "Invalid input." ) let( s = search([val], list, num_returns_per_match=1, index_col_num=idx)[0] ) @@ -154,9 +221,37 @@ function in_list(val,list,idx=undef) = : val==list[s][idx]; +// Function: find_first_match() +// Topics: List Handling +// See Also: in_list() +// Usage: +// idx = find_first_match(val, list, , ); +// indices = find_first_match(val, list, all=true, , ); +// Description: +// Finds the first item in `list` that matches `val`, returning the index. +// Arguments: +// val = The value to search for. +// list = The list to search through. +// --- +// start = The index to start searching from. +// all = If true, returns a list of all matching item indices. +// eps = The maximum allowed floating point rounding error for numeric comparisons. +function find_first_match(val, list, start=0, all=false, eps=EPSILON) = + all? [for (i=[start:1:len(list)-1]) if(val==list[i] || approx(val, list[i], eps=eps)) i] : + __find_first_match(val, list, eps=eps, i=start); + +function __find_first_match(val, list, eps, i=0) = + i >= len(list)? undef : + approx(val, list[i], eps=eps)? i : + __find_first_match(val, list, eps=eps, i=i+1); + + // Function: min_index() // Usage: -// min_index(vals,[all]); +// idx = min_index(vals); +// idxlist = min_index(vals,all=true); +// Topics: List Handling +// See Also: max_index(), list_increasing(), list_decreasing() // Description: // Returns the index of the first occurrence of the minimum value in the given list. // If `all` is true then returns a list of all indices where the minimum value occurs. @@ -164,8 +259,8 @@ function in_list(val,list,idx=undef) = // vals = vector of values // all = set to true to return indices of all occurences of the minimum. Default: false // Example: -// min_index([5,3,9,6,2,7,8,2,1]); // Returns: 8 -// min_index([5,3,9,6,2,7,8,2,7],all=true); // Returns: [4,7] +// a = min_index([5,3,9,6,2,7,8,2,1]); // Returns: 8 +// b = min_index([5,3,9,6,2,7,8,2,7],all=true); // Returns: [4,7] function min_index(vals, all=false) = assert( is_vector(vals) && len(vals)>0 , "Invalid or empty list of numbers.") all ? search(min(vals),vals,0) : search(min(vals), vals)[0]; @@ -173,7 +268,10 @@ function min_index(vals, all=false) = // Function: max_index() // Usage: -// max_index(vals,[all]); +// idx = max_index(vals); +// idxlist = max_index(vals,all=true); +// Topics: List Handling +// See Also: min_index(), list_increasing(), list_decreasing() // Description: // Returns the index of the first occurrence of the maximum value in the given list. // If `all` is true then returns a list of all indices where the maximum value occurs. @@ -190,13 +288,15 @@ function max_index(vals, all=false) = // Function: list_increasing() // Usage: -// list_increasing(list) +// bool = list_increasing(list); +// Topics: List Handling +// See Also: max_index(), min_index(), list_decreasing() // Description: // Returns true if the list is (non-strictly) increasing // Example: -// list_increasing([1,2,3,4]); // Returns: true -// list_increasing([1,3,2,4]); // Returns: false -// list_increasing([4,3,2,1]); // Returns: false +// a = list_increasing([1,2,3,4]); // Returns: true +// b = list_increasing([1,3,2,4]); // Returns: false +// c = list_increasing([4,3,2,1]); // Returns: false function list_increasing(list) = assert(is_list(list)||is_string(list)) len([for (p=pair(list)) if(p.x>p.y) true])==0; @@ -204,13 +304,15 @@ function list_increasing(list) = // Function: list_decreasing() // Usage: -// list_decreasing(list) +// bool = list_decreasing(list); +// Topics: List Handling +// See Also: max_index(), min_index(), list_increasing() // Description: // Returns true if the list is (non-strictly) decreasing // Example: -// list_decreasing([1,2,3,4]); // Returns: false -// list_decreasing([4,2,3,1]); // Returns: false -// list_decreasing([4,3,2,1]); // Returns: true +// a = list_decreasing([1,2,3,4]); // Returns: false +// b = list_decreasing([4,2,3,1]); // Returns: false +// c = list_decreasing([4,3,2,1]); // Returns: true function list_decreasing(list) = assert(is_list(list)||is_string(list)) len([for (p=pair(list)) if(p.x, ); +// list = list_range(n=, , ); +// list = list_range(e=, ); +// list = list_range(s=, e=, ); +// Topics: List Handling +// See Also: repeat() // Description: // Returns a list, counting up from starting value `s`, by `step` increments, // until either `n` values are in the list, or it reaches the end value `e`. // If both `n` and `e` are given, returns `n` values evenly spread from `s` // to `e`, and `step` is ignored. // Arguments: +// --- // n = Desired number of values in returned list, if given. // s = Starting value. Default: 0 // e = Ending value to stop at, if given. // step = Amount to increment each value. Default: 1 // Example: -// list_range(4); // Returns [0,1,2,3] -// list_range(n=4, step=2); // Returns [0,2,4,6] -// list_range(n=4, s=3, step=3); // Returns [3,6,9,12] -// list_range(n=5, s=0, e=10); // Returns [0, 2.5, 5, 7.5, 10] -// list_range(e=3); // Returns [0,1,2,3] -// list_range(e=7, step=2); // Returns [0,2,4,6] -// list_range(s=3, e=5); // Returns [3,4,5] -// list_range(s=3, e=8, step=2); // Returns [3,5,7] -// list_range(s=4, e=8.3, step=2); // Returns [4,6,8] -// list_range(n=4, s=[3,4], step=[2,3]); // Returns [[3,4], [5,7], [7,10], [9,13]] -function list_range(n=undef, s=0, e=undef, step=undef) = +// a = list_range(4); // Returns [0,1,2,3] +// b = list_range(n=4, step=2); // Returns [0,2,4,6] +// c = list_range(n=4, s=3, step=3); // Returns [3,6,9,12] +// d = list_range(n=5, s=0, e=10); // Returns [0, 2.5, 5, 7.5, 10] +// e = list_range(e=3); // Returns [0,1,2,3] +// f = list_range(e=7, step=2); // Returns [0,2,4,6] +// g = list_range(s=3, e=5); // Returns [3,4,5] +// h = list_range(s=3, e=8, step=2); // Returns [3,5,7] +// i = list_range(s=4, e=8.3, step=2); // Returns [4,6,8] +// j = list_range(n=4, s=[3,4], step=[2,3]); // Returns [[3,4], [5,7], [7,10], [9,13]] +function list_range(n, s=0, e, step) = assert( is_undef(n) || is_finite(n), "Parameter `n` must be a number.") assert( is_undef(n) || is_undef(e) || is_undef(step), "At most 2 of n, e, and step can be given.") let( step = (n!=undef && e!=undef)? (e-s)/(n-1) : default(step,1) ) - is_undef(e) ? - assert( is_consistent([s, step]), "Incompatible data.") + is_undef(e) + ? assert( is_consistent([s, step]), "Incompatible data.") [for (i=[0:1:n-1]) s+step*i ] - : assert( is_vector([s,step,e]), "Start `s`, step `step` and end `e` must be numbers.") + : assert( is_vector([s,step,e]), "Start `s`, step `step` and end `e` must be numbers.") [for (v=[s:step:e]) v] ; @@ -284,7 +391,12 @@ function list_range(n=undef, s=0, e=undef, step=undef) = // Section: List Manipulation // Function: reverse() -// Description: Reverses a list/array or string. +// Usage: +// rlist = reverse(list); +// Topics: List Handling +// See Also: select(), list_rotate() +// Description: +// Reverses a list/array or string. // Arguments: // x = The list or string to reverse. // Example: @@ -297,7 +409,9 @@ function reverse(x) = // Function: list_rotate() // Usage: -// rlist = list_rotate(list,n); +// rlist = list_rotate(list,); +// Topics: List Handling +// See Also: select(), reverse() // Description: // Rotates the contents of a list by `n` positions left. // If `n` is negative, then the rotation is `abs(n)` positions to the right. @@ -318,13 +432,22 @@ function reverse(x) = function list_rotate(list,n=1) = assert(is_list(list)||is_string(list), "Invalid list or string.") assert(is_finite(n), "Invalid number") - let (elems = select(list,n,n+len(list)-1)) + let ( + ll = len(list), + n = ((n % ll) + ll) % ll, + elems = [ + for (i=[n:1:ll-1]) list[i], + for (i=[0:1:n-1]) list[i] + ] + ) is_string(list)? str_join(elems) : elems; // Function: deduplicate() // Usage: -// deduplicate(list,[close],[eps]); +// list = deduplicate(list,,); +// Topics: List Handling +// See Also: deduplicate_indexed() // Description: // Removes consecutive duplicate items in a list. // When `eps` is zero, the comparison between consecutive items is exact. @@ -335,11 +458,11 @@ function list_rotate(list,n=1) = // closed = If true, drops trailing items if they match the first list item. // eps = The maximum tolerance between items. // Examples: -// deduplicate([8,3,4,4,4,8,2,3,3,8,8]); // Returns: [8,3,4,8,2,3,8] -// deduplicate(closed=true, [8,3,4,4,4,8,2,3,3,8,8]); // Returns: [8,3,4,8,2,3] -// deduplicate("Hello"); // Returns: "Helo" -// deduplicate([[3,4],[7,2],[7,1.99],[1,4]],eps=0.1); // Returns: [[3,4],[7,2],[1,4]] -// deduplicate([[7,undef],[7,undef],[1,4],[1,4+1e-12]],eps=0); // Returns: [[7,undef],[1,4],[1,4+1e-12]] +// a = deduplicate([8,3,4,4,4,8,2,3,3,8,8]); // Returns: [8,3,4,8,2,3,8] +// b = deduplicate(closed=true, [8,3,4,4,4,8,2,3,3,8,8]); // Returns: [8,3,4,8,2,3] +// c = deduplicate("Hello"); // Returns: "Helo" +// d = deduplicate([[3,4],[7,2],[7,1.99],[1,4]],eps=0.1); // Returns: [[3,4],[7,2],[1,4]] +// e = deduplicate([[7,undef],[7,undef],[1,4],[1,4+1e-12]],eps=0); // Returns: [[7,undef],[1,4],[1,4+1e-12]] function deduplicate(list, closed=false, eps=EPSILON) = assert(is_list(list)||is_string(list)) let( @@ -353,7 +476,9 @@ function deduplicate(list, closed=false, eps=EPSILON) = // Function: deduplicate_indexed() // Usage: -// new_idxs = deduplicate_indexed(list, indices, [closed], [eps]); +// new_idxs = deduplicate_indexed(list, indices, , ); +// Topics: List Handling +// See Also: deduplicate() // Description: // Given a list, and indices into it, removes consecutive indices that // index to the same values in the list. @@ -363,19 +488,27 @@ function deduplicate(list, closed=false, eps=EPSILON) = // closed = If true, drops trailing indices if what they index matches what the first index indexes. // eps = The maximum difference to allow between numbers or vectors. // Examples: -// deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1]); // Returns: [1,4,3,2,0,1] -// deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1], closed=true); // Returns: [1,4,3,2,0] -// deduplicate_indexed([[7,undef],[7,undef],[1,4],[1,4],[1,4+1e-12]],eps=0); // Returns: [0,2,4] +// a = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1]); // Returns: [1,4,3,2,0,1] +// b = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1], closed=true); // Returns: [1,4,3,2,0] +// c = deduplicate_indexed([[7,undef],[7,undef],[1,4],[1,4],[1,4+1e-12]],eps=0); // Returns: [0,2,4] function deduplicate_indexed(list, indices, closed=false, eps=EPSILON) = assert(is_list(list)||is_string(list), "Improper list or string.") indices==[]? [] : assert(is_vector(indices), "Indices must be a list of numbers.") - let( l = len(indices), - end = l-(closed?0:1) ) - [ for (i = [0:1:l-1]) - let( - a = list[indices[i]], - b = list[indices[(i+1)%l]], + let( + ll = len(list), + l = len(indices), + end = l-(closed?0:1) + ) [ + for (i = [0:1:l-1]) let( + idx1 = indices[i], + idx2 = indices[(i+1)%l], + a = assert(idx1>=0,"Bad index.") + assert(idx1=0,"Bad index.") + assert(idx2); +// Topics: List Handling +// See Also: repeat() // Description: // Takes a list as input and duplicates some of its entries to produce a list // with length `N`. If the requested `N` is not a multiple of the list length then @@ -407,10 +542,10 @@ function deduplicate_indexed(list, indices, closed=false, eps=EPSILON) = // exact = if true return exactly the requested number of points, possibly sacrificing uniformity. If false, return uniform points that may not match the number of points requested. Default: True // Examples: // list = [0,1,2,3]; -// echo(repeat_entries(list, 6)); // Outputs [0,0,1,2,2,3] -// echo(repeat_entries(list, 6, exact=false)); // Outputs [0,0,1,1,2,2,3,3] -// echo(repeat_entries(list, [1,1,2,1], exact=false)); // Outputs [0,1,2,2,3] -function repeat_entries(list, N, exact = true) = +// a = repeat_entries(list, 6); // Returns: [0,0,1,2,2,3] +// b = repeat_entries(list, 6, exact=false); // Returns: [0,0,1,1,2,2,3,3] +// c = repeat_entries(list, [1,1,2,1], exact=false); // Returns: [0,1,2,2,3] +function repeat_entries(list, N, exact=true) = assert(is_list(list) && len(list)>0, "The list cannot be void.") assert((is_finite(N) && N>0) || is_vector(N,len(list)), "Parameter N must be a number greater than zero or vector with the same length of `list`") @@ -426,7 +561,9 @@ function repeat_entries(list, N, exact = true) = // Function: list_set() // Usage: -// list_set(list, indices, values, [dflt], [minlen]) +// list = list_set(list, indices, values, , ); +// Topics: List Handling +// See Also: list_insert(), list_remove(), list_remove_values() // Description: // Takes the input list and returns a new list such that `list[indices[i]] = values[i]` for all of // the (index,value) pairs supplied and unchanged for other indices. If you supply `indices` that are @@ -441,70 +578,85 @@ function repeat_entries(list, N, exact = true) = // dflt = Default value to store in sparse skipped indices. // minlen = Minimum length to expand list to. // Examples: -// list_set([2,3,4,5], 2, 21); // Returns: [2,3,21,5] -// list_set([2,3,4,5], [1,3], [81,47]); // Returns: [2,81,4,47] +// a = list_set([2,3,4,5], 2, 21); // Returns: [2,3,21,5] +// b = list_set([2,3,4,5], [1,3], [81,47]); // Returns: [2,81,4,47] function list_set(list=[],indices,values,dflt=0,minlen=0) = assert(is_list(list)) !is_list(indices)? ( - (is_finite(indices) && indices=len(array) , "Improper index list." ) @@ -566,7 +723,9 @@ function bselect(array,index) = // Function: list_bset() // Usage: -// list_bset(indexset, valuelist,[dflt]) +// arr = list_bset(indexset, valuelist, ); +// Topics: List Handling +// See Also: bselect() // Description: // Opposite of `bselect()`. Returns a list the same length as `indexlist`, where each item will // either be 0 if the corresponding item in `indexset` is false, or the next sequential value @@ -577,8 +736,8 @@ function bselect(array,index) = // valuelist = The list of values to set into the returned list. // dflt = Default value to store when the indexset item is false. // Example: -// list_bset([false,true,false,true,false], [3,4]); // Returns: [0,3,0,4,0] -// list_bset([false,true,false,true,false], [3,4],dflt=1); // Returns: [1,3,1,4,1] +// a = list_bset([false,true,false,true,false], [3,4]); // Returns: [0,3,0,4,0] +// b = list_bset([false,true,false,true,false], [3,4], dflt=1); // Returns: [1,3,1,4,1] function list_bset(indexset, valuelist, dflt=0) = assert(is_list(indexset), "The index set is not a list." ) assert(is_list(valuelist), "The `valuelist` is not a list." ) @@ -594,56 +753,92 @@ function list_bset(indexset, valuelist, dflt=0) = // Section: List Length Manipulation // Function: list_shortest() +// Usage: +// llen = list_shortest(array); +// Topics: List Handling +// See Also: list_longest() // Description: // Returns the length of the shortest sublist in a list of lists. // Arguments: // array = A list of lists. +// Example: +// slen = list_shortest([[3,4,5],[6,7,8,9]]); // Returns: 3 function list_shortest(array) = assert(is_list(array), "Invalid input." ) min([for (v = array) len(v)]); // Function: list_longest() +// Usage: +// llen = list_longest(array); +// Topics: List Handling +// See Also: list_shortest() // Description: // Returns the length of the longest sublist in a list of lists. // Arguments: // array = A list of lists. +// Example: +// llen = list_longest([[3,4,5],[6,7,8,9]]); // Returns: 4 function list_longest(array) = assert(is_list(array), "Invalid input." ) max([for (v = array) len(v)]); // Function: list_pad() +// Usage: +// arr = list_pad(array, minlen, ); +// Topics: List Handling +// See Also: list_trim(), list_fit() // Description: // If the list `array` is shorter than `minlen` length, pad it to length with the value given in `fill`. // Arguments: // array = A list. // minlen = The minimum length to pad the list to. -// fill = The value to pad the list with. -function list_pad(array, minlen, fill=undef) = +// fill = The value to pad the list with. Default: `undef` +// Example: +// list = [3,4,5]; +// nlist = list_pad(list,5,23); // Returns: [3,4,5,23,23] +function list_pad(array, minlen, fill) = assert(is_list(array), "Invalid input." ) concat(array,repeat(fill,minlen-len(array))); // Function: list_trim() +// Usage: +// arr = list_trim(array, maxlen); +// Topics: List Handling +// See Also: list_pad(), list_fit() // Description: // If the list `array` is longer than `maxlen` length, truncates it to be `maxlen` items long. // Arguments: // array = A list. // minlen = The minimum length to pad the list to. +// Example: +// list = [3,4,5,6,7,8]; +// nlist = list_trim(list,4); // Returns: [3,4,5,6] function list_trim(array, maxlen) = assert(is_list(array), "Invalid input." ) [for (i=[0:1:min(len(array),maxlen)-1]) array[i]]; // Function: list_fit() +// Usage: +// arr = list_fit(array, length, fill); +// Topics: List Handling +// See Also: list_pad(), list_trim() // Description: // If the list `array` is longer than `length` items long, truncates it to be exactly `length` items long. // If the list `array` is shorter than `length` items long, pad it to length with the value given in `fill`. // Arguments: // array = A list. // minlen = The minimum length to pad the list to. -// fill = The value to pad the list with. +// fill = The value to pad the list with. Default: `undef` +// Example: +// list = [3,4,5,6]; +// nlist = list_fit(list,3); // Returns: [3,4,5] +// Example: +// list = [3,4,5,6]; +// nlist = list_fit(list,6,23); // Returns: [3,4,5,6,23,23] function list_fit(array, length, fill) = assert(is_list(array), "Invalid input." ) let(l=len(array)) @@ -673,17 +868,28 @@ function _valid_idx(idx,imin,imax) = // Function: shuffle() // Usage: -// shuffled = shuffle(list,[seed]) +// shuffled = shuffle(list,); +// Topics: List Handling +// See Also: sort(), sortidx(), unique(), unique_count() // Description: // Shuffles the input list into random order. // If given a string, shuffles the characters within the string. // If you give a numeric seed value then the permutation // will be repeatable. +// Arguments: +// list = The list to shuffle. +// seed = Optional random number seed for the shuffling. +// Example: +// // Spades Hearts Diamonds Clubs +// suits = ["\u2660", "\u2661", "\u2662", "\u2663"]; +// ranks = [2,3,4,5,6,7,8,9,10,"J","Q","K","A"]; +// cards = [for (suit=suits, rank=ranks) str(rank,suit)]; +// deck = shuffle(cards); function shuffle(list,seed) = assert(is_list(list)||is_string(list), "Invalid input." ) is_string(list)? str_join(shuffle([for (x = list) x],seed=seed)) : len(list)<=1 ? list : - let ( + let( rval = is_num(seed) ? rands(0,1,len(list),seed_value=seed) : rands(0,1,len(list)), left = [for (i=[0:len(list)-1]) if (rval[i]< 0.5) list[i]], @@ -714,7 +920,7 @@ function _sort_vectors(arr, _i=0) = lesser = [ for (entry=arr) if (entry[_i] < pivot ) entry ], equal = [ for (entry=arr) if (entry[_i] == pivot ) entry ], greater = [ for (entry=arr) if (entry[_i] > pivot ) entry ] - ) + ) concat( _sort_vectors(lesser, _i ), _sort_vectors(equal, _i+1 ), @@ -777,7 +983,9 @@ function _indexed_sort(arrind) = // Function: sort() // Usage: -// sort(list, [idx]) +// slist = sort(list, ); +// Topics: List Handling +// See Also: shuffle(), sortidx(), unique(), unique_count() // Description: // Sorts the given list in lexicographic order. If the input is a homogeneous simple list or a homogeneous // list of vectors (see function is_homogeneous), the sorting method uses the native comparison operator and is faster. @@ -815,12 +1023,19 @@ function sort(list, idx=undef) = // Function: sortidx() +// Usage: +// idxlist = sortidx(list, ); +// Topics: List Handling +// See Also: shuffle(), sort(), unique(), unique_count() // Description: // Given a list, sort it as function `sort()`, and returns // a list of indexes into the original list in that sorted order. // If you iterate the returned list in order, and use the list items // to index into the original list, you will be iterating the original // values in sorted order. +// Arguments: +// list = The list to sort. +// idx = If given, do the comparison based just on the specified index, range or list of indices. // Example: // lst = ["d","b","e","c"]; // idxs = sortidx(lst); // Returns: [1,3,0,2] @@ -861,11 +1076,15 @@ function sortidx(list, idx=undef) = // Function: unique() // Usage: -// l = unique(list); +// ulist = unique(list); +// Topics: List Handling +// See Also: shuffle(), sort(), sortidx(), unique_count() // Description: // Returns a sorted list with all repeated items removed. // Arguments: // list = The list to uniquify. +// Example: +// sorted = unique([5,2,8,3,1,3,8,7,5]); // Returns: [1,2,3,5,7,8] function unique(list) = assert(is_list(list)||is_string(list), "Invalid input." ) is_string(list)? str_join(unique([for (x = list) x])) : @@ -880,11 +1099,15 @@ function unique(list) = // Function: unique_count() // Usage: // counts = unique_count(list); +// Topics: List Handling +// See Also: shuffle(), sort(), sortidx(), unique() // Description: // Returns `[sorted,counts]` where `sorted` is a sorted list of the unique items in `list` and `counts` is a list such // that `count[i]` gives the number of times that `sorted[i]` appears in `list`. // Arguments: // list = The list to analyze. +// Example: +// sorted = unique([5,2,8,3,1,3,8,3,5]); // Returns: [ [1,2,3,5,8], [1,1,3,2,2] ] function unique_count(list) = assert(is_list(list) || is_string(list), "Invalid input." ) list == [] ? [[],[]] : @@ -897,24 +1120,36 @@ function unique_count(list) = // Function: idx() // Usage: -// i = idx(list); -// for(i=idx(list)) ... +// rng = idx(list, , , ); +// for(i=idx(list, , , )) ... +// Topics: List Handling, Iteration +// See Also: enumerate(), pair(), triplet(), combinations(), permutations() // Description: // Returns the range of indexes for the given list. // Arguments: // list = The list to returns the index range of. +// s = The starting index. Default: 0 +// e = The delta from the end of the list. Default: -1 (end of list) // step = The step size to stride through the list. Default: 1 -// end = The delta from the end of the list. Default: -1 -// start = The starting index. Default: 0 // Example(2D): // colors = ["red", "green", "blue"]; // for (i=idx(colors)) right(20*i) color(colors[i]) circle(d=10); -function idx(list, step=1, end=-1,start=0) = +function idx(list, s=0, e=-1, step=1) = assert(is_list(list)||is_string(list), "Invalid input." ) - [start : step : len(list)+end]; + let( ll = len(list) ) + ll == 0 ? [0:1:ll-1] : + let( + _s = posmod(s,ll), + _e = posmod(e,ll) + ) [_s : step : _e]; // Function: enumerate() +// Usage: +// arr = enumerate(l, ); +// for (x = enumerate(l, )) ... // x[0] is the index number, x[1] is the item. +// Topics: List Handling, Iteration +// See Also: idx(), pair(), triplet(), combinations(), permutations() // Description: // Returns a list, with each item of the given list `l` numbered in a sublist. // Something like: `[[0,l[0]], [1,l[1]], [2,l[2]], ...]` @@ -936,94 +1171,71 @@ function enumerate(l,idx=undef) = : [for (i=[0:1:len(l)-1]) [ i, for (j=idx) l[i][j]] ]; -// Function: force_list() -// Usage: -// list = force_list(value, [n], [fill]) -// Description: -// Coerces non-list values into a list. Makes it easy to treat a scalar input -// consistently as a singleton list, as well as list inputs. -// - If `value` is a list, then that list is returned verbatim. -// - If `value` is not a list, and `fill` is not given, then a list of `n` copies of `value` will be returned. -// - If `value` is not a list, and `fill` is given, then a list `n` items long will be returned where `value` will be the first item, and the rest will contain the value of `fill`. -// Arguments: -// value = The value or list to coerce into a list. -// n = The number of items in the coerced list. Default: 1 -// fill = The value to pad the coerced list with, after the firt value. Default: undef (pad with copies of `value`) -// Examples: -// x = force_list([3,4,5]); // Returns: [3,4,5] -// y = force_list(5); // Returns: [5] -// z = force_list(7, n=3); // Returns: [7,7,7] -// w = force_list(4, n=3, fill=1); // Returns: [4,1,1] -function force_list(value, n=1, fill) = - is_list(value) ? value : - is_undef(fill)? [for (i=[1:1:n]) value] : [value, for (i=[2:1:n]) fill]; - - // Function: pair() // Usage: -// pair(v) +// p = pair(list, ); +// for (p = pair(list, )) ... // On each iteration, p contains a list of two adjacent items. +// Topics: List Handling, Iteration +// See Also: idx(), enumerate(), triplet(), combinations(), permutations() // Description: -// Takes a list, and returns a list of adjacent pairs from it. -// Example(2D): Note that the last point and first point do NOT get paired together. -// for (p = pair(circle(d=20, $fn=12))) -// move(p[0]) -// rot(from=BACK, to=p[1]-p[0]) -// trapezoid(w1=1, w2=0, h=norm(p[1]-p[0]), anchor=FRONT); +// Takes a list, and returns a list of adjacent pairs from it, optionally wrapping back to the front. +// Arguments: +// list = The list to iterate. +// wrap = If true, wrap back to the start from the end. ie: return the last and first items as the last pair. Default: false +// Example(2D): Does NOT wrap from end to start, +// for (p = pair(circle(d=40, $fn=12))) +// stroke(p, endcap2="arrow2"); +// Example(2D): Wraps around from end to start. +// for (p = pair(circle(d=40, $fn=12), wrap=true)) +// stroke(p, endcap2="arrow2"); // Example: // l = ["A","B","C","D"]; // echo([for (p=pair(l)) str(p.y,p.x)]); // Outputs: ["BA", "CB", "DC"] -function pair(v) = - assert(is_list(v)||is_string(v), "Invalid input." ) - [for (i=[0:1:len(v)-2]) [v[i],v[i+1]]]; - - -// Function: pair_wrap() -// Usage: -// pair_wrap(v) -// Description: -// Takes a list, and returns a list of adjacent pairs from it, wrapping around from the end to the start of the list. -// Example(2D): -// for (p = pair_wrap(circle(d=20, $fn=12))) -// move(p[0]) -// rot(from=BACK, to=p[1]-p[0]) -// trapezoid(w1=1, w2=0, h=norm(p[1]-p[0]), anchor=FRONT); -// Example: -// l = ["A","B","C","D"]; -// echo([for (p=pair_wrap(l)) str(p.y,p.x)]); // Outputs: ["BA", "CB", "DC", "AD"] -function pair_wrap(v) = - assert(is_list(v)||is_string(v), "Invalid input." ) - [for (i=[0:1:len(v)-1]) [v[i],v[(i+1)%len(v)]]]; +function pair(list, wrap=false) = + assert(is_list(list)||is_string(list), "Invalid input." ) + assert(is_bool(wrap)) + let( + ll = len(list) + ) wrap + ? [for (i=[0:1:ll-1]) [list[i], list[(i+1) % ll]]] + : [for (i=[0:1:ll-2]) [list[i], list[i+1]]]; // Function: triplet() // Usage: -// triplet(v) +// list = triplet(list, ); +// for (t = triplet(list, )) ... +// Topics: List Handling, Iteration +// See Also: idx(), enumerate(), pair(), combinations(), permutations() // Description: -// Takes a list, and returns a list of adjacent triplets from it. +// Takes a list, and returns a list of adjacent triplets from it, optionally wrapping back to the front. // Example: // l = ["A","B","C","D","E"]; // echo([for (p=triplet(l)) str(p.z,p.y,p.x)]); // Outputs: ["CBA", "DCB", "EDC"] -function triplet(v) = - assert(is_list(v)||is_string(v), "Invalid input." ) - [for (i=[0:1:len(v)-3]) [v[i],v[i+1],v[i+2]]]; +// Example(2D): +// path = [for (i=[0:24]) polar_to_xy(i*2, i*360/12)]; +// for (t = triplet(path)) { +// a = t[0]; b = t[1]; c = t[2]; +// v = unit(unit(a-b) + unit(c-b)); +// translate(b) rot(from=FWD,to=v) anchor_arrow2d(); +// } +// stroke(path); +function triplet(list, wrap=false) = + assert(is_list(list)||is_string(list), "Invalid input." ) + assert(is_bool(wrap)) + let( + ll = len(list) + ) wrap + ? [for (i=[0:1:ll-1]) [ list[i], list[(i+1)%ll], list[(i+2)%ll] ]] + : [for (i=[0:1:ll-3]) [ list[i], list[i+1], list[i+2] ]]; -// Function: triplet_wrap() +// Function: combinations() // Usage: -// triplet_wrap(v) -// Description: -// Takes a list, and returns a list of adjacent triplets from it, wrapping around from the end to the start of the list. -// Example: -// l = ["A","B","C","D"]; -// echo([for (p=triplet_wrap(l)) str(p.z,p.y,p.x)]); // Outputs: ["CBA", "DCB", "ADC", "BAD"] -function triplet_wrap(v) = - assert(is_list(v)||is_string(v), "Invalid input." ) - [for (i=[0:1:len(v)-1]) [v[i],v[(i+1)%len(v)],v[(i+2)%len(v)]]]; - - -// Function: permute() -// Usage: -// list = permute(l, [n]); +// list = combinations(l, ); +// for (p = combinations(l, )) ... +// Topics: List Handling, Iteration +// See Also: idx(), enumerate(), pair(), triplet(), permutations() // Description: // Returns an ordered list of every unique permutation of `n` items out of the given list `l`. // For the list `[1,2,3,4]`, with `n=2`, this will return `[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]`. @@ -1032,16 +1244,95 @@ function triplet_wrap(v) = // l = The list to provide permutations for. // n = The number of items in each permutation. Default: 2 // Example: -// pairs = permute([3,4,5,6]); // Returns: [[3,4],[3,5],[3,6],[4,5],[4,6],[5,6]] -// triplets = permute([3,4,5,6],n=3); // Returns: [[3,4,5],[3,4,6],[3,5,6],[4,5,6]] +// pairs = combinations([3,4,5,6]); // Returns: [[3,4],[3,5],[3,6],[4,5],[4,6],[5,6]] +// triplets = combinations([3,4,5,6],n=3); // Returns: [[3,4,5],[3,4,6],[3,5,6],[4,5,6]] // Example(2D): -// for (p=permute(regular_ngon(n=7,d=100))) stroke(p); -function permute(l,n=2,_s=0) = +// for (p=combinations(regular_ngon(n=7,d=100))) stroke(p); +function combinations(l,n=2,_s=0) = assert(is_list(l), "Invalid list." ) assert( is_finite(n) && n>=1 && n<=len(l), "Invalid number `n`." ) n==1 - ? [for (i=[_s:1:len(l)-1]) [l[i]]] - : [for (i=[_s:1:len(l)-n], p=permute(l,n=n-1,_s=i+1)) concat([l[i]], p)]; + ? [for (i=[_s:1:len(l)-1]) [l[i]]] + : [for (i=[_s:1:len(l)-n], p=combinations(l,n=n-1,_s=i+1)) concat([l[i]], p)]; + + +// Function: permutations() +// Usage: +// list = permutations(l, ); +// for (p = permutations(l, )) ... +// Topics: List Handling, Iteration +// See Also: idx(), enumerate(), pair(), triplet(), combinations() +// Description: +// Returns an ordered list of every unique permutation of `n` items out of the given list `l`. +// For the list `[1,2,3,4]`, with `n=2`, this will return `[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]`. +// For the list `[1,2,3,4]`, with `n=3`, this will return `[[1,2,3], [1,2,4], [1,3,4], [2,3,4]]`. +// Arguments: +// l = The list to provide permutations for. +// n = The number of items in each permutation. Default: 2 +// Example: +// pairs = permutations([3,4,5,6]); // Returns: [[3,4],[3,5],[3,6],[4,5],[4,6],[5,6]] +// triplets = permutations([3,4,5,6],n=3); // Returns: [[3,4,5],[3,4,6],[3,5,6],[4,5,6]] +// Example(2D): +// for (p=permutations(regular_ngon(n=7,d=100))) stroke(p); +function permutations(l,n=2) = + assert(is_list(l), "Invalid list." ) + assert( is_finite(n) && n>=1 && n<=len(l), "Invalid number `n`." ) + n==1 + ? [for (i=[0:1:len(l)-1]) [l[i]]] + : [for (i=idx(l), p=permutations([for (j=idx(l)) if (i!=j) l[j]], n=n-1)) concat([l[i]], p)]; + + +// Function: zip() +// Usage: +// pairs = zip(a,b); +// triples = zip(a,b,c); +// quads = zip([LIST1,LIST2,LIST3,LIST4]); +// Description: +// Zips together two or more lists into a single list. For example, if you have two +// lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. +// The list returned will be as long as the shortest list passed to zip(). +// Arguments: +// a = The first list, or a list of lists if b and c are not given. +// b = The second list, if given. +// c = The third list, if given. +// Example: +// a = [9,8,7,6]; b = [1,2,3]; +// for (p=zip(a,b)) echo(p); +// // ECHO: [9,1] +// // ECHO: [8,2] +// // ECHO: [7,3] +function zip(a,b,c) = + b!=undef? zip([a,b,if (c!=undef) c]) : + let(n = list_shortest(a)) + [for (i=[0:1:n-1]) [for (x=a) x[i]]]; + + +// Function: zip_long() +// Usage: +// pairs = zip_long(a,b); +// triples = zip_long(a,b,c); +// quads = zip_long([LIST1,LIST2,LIST3,LIST4]); +// Description: +// Zips together two or more lists into a single list. For example, if you have two +// lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. +// The list returned will be as long as the longest list passed to zip_long(), with +// shorter lists padded by the value in `fill`. +// Arguments: +// a = The first list, or a list of lists if b and c are not given. +// b = The second list, if given. +// c = The third list, if given. +// fill = The value to pad shorter lists with. Default: undef +// Example: +// a = [9,8,7,6]; b = [1,2,3]; +// for (p=zip_long(a,b,fill=88)) echo(p); +// // ECHO: [9,1] +// // ECHO: [8,2] +// // ECHO: [7,3] +// // ECHO: [6,88]] +function zip_long(a,b,c,fill) = + b!=undef? zip_long([a,b,if (c!=undef) c],fill=fill) : + let(n = list_longest(a)) + [for (i=[0:1:n-1]) [for (x=a) i); +// Topics: Set Handling, List Handling +// See Also: set_difference(), set_intersection() // Description: // Given two sets (lists with unique items), returns the set of unique items that are in either `a` or `b`. // If `get_indices` is true, a list of indices into the new union set are returned for each item in `b`, @@ -1090,6 +1383,8 @@ function set_union(a, b, get_indices=false) = // Function: set_difference() // Usage: // s = set_difference(a, b); +// Topics: Set Handling, List Handling +// See Also: set_union(), set_intersection() // Description: // Given two sets (lists with unique items), returns the set of items that are in `a`, but not `b`. // Arguments: @@ -1109,6 +1404,8 @@ function set_difference(a, b) = // Function: set_intersection() // Usage: // s = set_intersection(a, b); +// Topics: Set Handling, List Handling +// See Also: set_union(), set_difference() // Description: // Given two sets (lists with unique items), returns the set of items that are in both sets. // Arguments: @@ -1128,25 +1425,11 @@ function set_intersection(a, b) = // Section: Array Manipulation -// Function: add_scalar() -// Usage: -// add_scalar(v,s); -// Description: -// Given an array and a scalar, returns the array with the scalar added to each item in it. -// If given a list of arrays, recursively adds the scalar to the each array. -// Arguments: -// v = The initial array. -// s = A scalar value to add to every item in the array. -// Example: -// add_scalar([1,2,3],3); // Returns: [4,5,6] -// add_scalar([[1,2,3],[3,4,5]],3); // Returns: [[4,5,6],[6,7,8]] -function add_scalar(v,s) = - is_finite(s) ? [for (x=v) is_list(x)? add_scalar(x,s) : is_finite(x) ? x+s: x] : v; - - // Function: subindex() // Usage: -// subindex(M, idx) +// list = subindex(M, idx); +// Topics: Array Handling, List Handling +// See Also: select(), slice() // Description: // Extracts the entries listed in idx from each entry in M. For a matrix this means // selecting a specified set of columns. If idx is a number the return is a vector, @@ -1157,12 +1440,12 @@ function add_scalar(v,s) = // idx = The index, list of indices, or range of indices to fetch. // Example: // M = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; -// subindex(M,2); // Returns [3, 7, 11, 15] -// subindex(M,[2]); // Returns [[3], [7], [11], [15]] -// subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] -// subindex(M,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] +// a = subindex(M,2); // Returns [3, 7, 11, 15] +// b = subindex(M,[2]); // Returns [[3], [7], [11], [15]] +// c = subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] +// d = subindex(M,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] // N = [ [1,2], [3], [4,5], [6,7,8] ]; -// subindex(N,[0,1]); // Returns [ [1,2], [3,undef], [4,5], [6,7] ] +// e = subindex(N,[0,1]); // Returns [ [1,2], [3,undef], [4,5], [6,7] ] function subindex(M, idx) = assert( is_list(M), "The input is not a list." ) assert( !is_undef(idx) && _valid_idx(idx,0,1/0), "Invalid index input." ) @@ -1173,7 +1456,9 @@ function subindex(M, idx) = // Function: submatrix() // Usage: -// mat = submatrix(M, idx1, idx2) +// mat = submatrix(M, idx1, idx2); +// Topics: Matrices, Array Handling +// See Also: subindex(), block_matrix(), submatrix_set() // Description: // The input must be a list of lists (a matrix or 2d array). Returns a submatrix by selecting the rows listed in idx1 and columns listed in idx2. // Arguments: @@ -1201,84 +1486,137 @@ function submatrix(M,idx1,idx2) = [for(i=idx1) [for(j=idx2) M[i][j] ] ]; -// Function: zip() -// Usage: -// zip(v1, v2, v3, [fit], [fill]); -// zip(vecs, [fit], [fill]); +// Function: hstack() +// Usage: +// A = hstack(M1, M2) +// A = hstack(M1, M2, M3) +// A = hstack([M1, M2, M3, ...]) +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), block_matrix() // Description: -// Zips together corresponding items from two or more lists. -// Returns a list of lists, where each sublist contains corresponding -// items from each of the input lists. `[[A1, B1, C1], [A2, B2, C2], ...]` +// Constructs a matrix by horizontally "stacking" together compatible matrices or vectors. Vectors are treated as columsn in the stack. +// This command is the inverse of subindex. Note: strings given in vectors are broken apart into lists of characters. Strings given +// in matrices are preserved as strings. If you need to combine vectors of strings use array_group as shown below to convert the +// vector into a column matrix. Also note that vertical stacking can be done directly with concat. // Arguments: -// vecs = A list of two or more lists to zipper together. -// fit = If `fit=="short"`, the zips together up to the length of the shortest list in vecs. If `fit=="long"`, then pads all lists to the length of the longest, using the value in `fill`. If `fit==false`, then requires all lists to be the same length. Default: false. -// fill = The default value to fill in with if one or more lists if short. Default: undef +// M1 = If given with other arguments, the first matrix (or vector) to stack. If given alone, a list of matrices/vectors to stack. +// M2 = Second matrix/vector to stack +// M3 = Third matrix/vector to stack. // Example: -// v1 = [1,2,3,4]; +// M = ident(3); +// v1 = [2,3,4]; // v2 = [5,6,7]; -// v3 = [8,9,10,11]; -// zip(v1,v3); // returns [[1,8], [2,9], [3,10], [4,11]] -// zip([v1,v3]); // returns [[1,8], [2,9], [3,10], [4,11]] -// zip([v1,v2], fit="short"); // returns [[1,5], [2,6], [3,7]] -// zip([v1,v2], fit="long"); // returns [[1,5], [2,6], [3,7], [4,undef]] -// zip([v1,v2], fit="long, fill=0); // returns [[1,5], [2,6], [3,7], [4,0]] -// zip([v1,v2,v3], fit="long"); // returns [[1,5,8], [2,6,9], [3,7,10], [4,undef,11]] -// Example: -// v1 = [[1,2,3], [4,5,6], [7,8,9]]; -// v2 = [[20,19,18], [17,16,15], [14,13,12]]; -// zip(v1,v2); // Returns [[1,2,3,20,19,18], [4,5,6,17,16,15], [7,8,9,14,13,12]] -function zip(vecs, v2, v3, fit=false, fill=undef) = - (v3!=undef)? zip([vecs,v2,v3], fit=fit, fill=fill) : - (v2!=undef)? zip([vecs,v2], fit=fit, fill=fill) : - assert(in_list(fit, [false, "short", "long"]), "Invalid fit value." ) - assert(all([for(v=vecs) is_list(v)]), "One of the inputs to zip is not a list") +// v3 = [8,9,10]; +// a = hstack(v1,v2); // Returns [[2, 5], [3, 6], [4, 7]] +// b = hstack(v1,v2,v3); // Returns [[2, 5, 8], +// // [3, 6, 9], +// // [4, 7, 10]] +// c = hstack([M,v1,M]); // Returns [[1, 0, 0, 2, 1, 0, 0], +// // [0, 1, 0, 3, 0, 1, 0], +// // [0, 0, 1, 4, 0, 0, 1]] +// d = hstack(subindex(M,0), subindex(M,[1 2])); // Returns M +// strvec = ["one","two"]; +// strmat = [["three","four"], ["five","six"]]; +// e = hstack(strvec,strvec); // Returns [["o", "n", "e", "o", "n", "e"], +// // ["t", "w", "o", "t", "w", "o"]] +// f = hstack(array_group(strvec,1), array_group(strvec,1)); +// // Returns [["one", "one"], +// // ["two", "two"]] +// g = hstack(strmat,strmat); // Returns: [["three", "four", "three", "four"], +// // [ "five", "six", "five", "six"]] +function hstack(M1, M2, M3) = + (M3!=undef)? hstack([M1,M2,M3]) : + (M2!=undef)? hstack([M1,M2]) : + assert(all([for(v=M1) is_list(v)]), "One of the inputs to hstack is not a list") let( - minlen = list_shortest(vecs), - maxlen = list_longest(vecs) + minlen = list_shortest(M1), + maxlen = list_longest(M1) ) - assert(fit!=false || minlen==maxlen, "Input vectors to zip must have the same length") - (fit == "long") - ? [for(i=[0:1:maxlen-1]) [for(v=vecs) for(x=(i); +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix() // Description: // Creates a square matrix with the items in the list `diag` on // its diagonal. The off diagonal entries are set to offdiag, // which is zero by default. -function diagonal_matrix(diag,offdiag=0) = +// Arguments: +// diag = A list of items to put in the diagnal cells of the matrix. +// offdiag = Value to put in non-diagonal matrix cells. +function diagonal_matrix(diag, offdiag=0) = assert(is_list(diag) && len(diag)>0) [for(i=[0:1:len(diag)-1]) [for(j=[0:len(diag)-1]) i==j?diag[i] : offdiag]]; // Function: submatrix_set() // Usage: -// mat = submatrix_set(M,A,[m],[n]) +// mat = submatrix_set(M,A,,); +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix() // Description: -// Sets a submatrix of M equal to the matrix A. By default the top left corner of M is set to A, but -// you can specify offset coordinates m and n. If A (as adjusted by m and n) extends beyond the bounds -// of M then the extra entries are ignored. You can pass in A=[[]], a null matrix, and M will be -// returned unchanged. Note that the input M need not be rectangular in shape. +// Sets a submatrix of M equal to the matrix A. By default the top left corner of M is set to A, but +// you can specify offset coordinates m and n. If A (as adjusted by m and n) extends beyond the bounds +// of M then the extra entries are ignored. You can pass in A=[[]], a null matrix, and M will be +// returned unchanged. Note that the input M need not be rectangular in shape. +// Arguments: +// M = Original matrix. +// A = Sub-matrix of parts to set. +// m = Row number of upper-left corner to place A at. +// n = Column number of upper-left corner to place A at. function submatrix_set(M,A,m=0,n=0) = assert(is_list(M)) assert(is_list(A)) @@ -1293,39 +1631,57 @@ function submatrix_set(M,A,m=0,n=0) = // Function: array_group() +// Usage: +// groups = array_group(v, , ); // Description: // Takes a flat array of values, and groups items in sets of `cnt` length. // The opposite of this is `flatten()`. +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), hstack(), flatten(), full_flatten() // Arguments: // v = The list of items to group. -// cnt = The number of items to put in each grouping. -// dflt = The default value to fill in with is the list is not a multiple of `cnt` items long. +// cnt = The number of items to put in each grouping. Default:2 +// dflt = The default value to fill in with is the list is not a multiple of `cnt` items long. Default: 0 // Example: // v = [1,2,3,4,5,6]; -// array_group(v,2) returns [[1,2], [3,4], [5,6]] -// array_group(v,3) returns [[1,2,3], [4,5,6]] -// array_group(v,4,0) returns [[1,2,3,4], [5,6,0,0]] -function array_group(v, cnt=2, dflt=0) = [for (i = [0:cnt:len(v)-1]) [for (j = [0:1:cnt-1]) default(v[i+j], dflt)]]; +// a = array_group(v,2) returns [[1,2], [3,4], [5,6]] +// b = array_group(v,3) returns [[1,2,3], [4,5,6]] +// c = array_group(v,4,0) returns [[1,2,3,4], [5,6,0,0]] +function array_group(v, cnt=2, dflt=0) = + [for (i = [0:cnt:len(v)-1]) [for (j = [0:1:cnt-1]) default(v[i+j], dflt)]]; // Function: flatten() -// Description: Takes a list of lists and flattens it by one level. +// Usage: +// list = flatten(l); +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), hstack(), full_flatten() +// Description: +// Takes a list of lists and flattens it by one level. // Arguments: // l = List to flatten. // Example: -// flatten([[1,2,3], [4,5,[6,7,8]]]) returns [1,2,3,4,5,[6,7,8]] -function flatten(l) = [for (a = l) each a]; +// l = flatten([[1,2,3], [4,5,[6,7,8]]]); // returns [1,2,3,4,5,[6,7,8]] +function flatten(l) = + !is_list(l)? l : + [for (a=l) if (is_list(a)) (each a) else a]; // Function: full_flatten() +// Usage: +// list = full_flatten(l); +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), hstack(), flatten() // Description: // Collects in a list all elements recursively found in any level of the given list. // The output list is ordered in depth first order. // Arguments: // l = List to flatten. // Example: -// full_flatten([[1,2,3], [4,5,[6,7,8]]]) returns [1,2,3,4,5,6,7,8] -function full_flatten(l) = [for(a=l) if(is_list(a)) (each full_flatten(a)) else a ]; +// l = full_flatten([[1,2,3], [4,5,[6,7,8]]]); // returns [1,2,3,4,5,6,7,8] +function full_flatten(l) = + !is_list(l)? l : + [for (a=l) if (is_list(a)) (each full_flatten(a)) else a]; // Internal. Not exposed. @@ -1350,24 +1706,23 @@ function _array_dim_recurse(v) = // Function: array_dim() // Usage: -// array_dim(v, [depth]) +// dims = array_dim(v, ); +// Topics: Matrices, Array Handling // Description: -// Returns the size of a multi-dimensional array. Returns a list of -// dimension lengths. The length of `v` is the dimension `0`. The -// length of the items in `v` is dimension `1`. The length of the -// items in the items in `v` is dimension `2`, etc. For each dimension, -// if the length of items at that depth is inconsistent, `undef` will -// be returned. If no items of that dimension depth exist, `0` is -// returned. Otherwise, the consistent length of items in that -// dimensional depth is returned. +// Returns the size of a multi-dimensional array. Returns a list of dimension lengths. The length +// of `v` is the dimension `0`. The length of the items in `v` is dimension `1`. The length of the +// items in the items in `v` is dimension `2`, etc. For each dimension, if the length of items at +// that depth is inconsistent, `undef` will be returned. If no items of that dimension depth exist, +// `0` is returned. Otherwise, the consistent length of items in that dimensional depth is +// returned. // Arguments: // v = Array to get dimensions of. // depth = Dimension to get size of. If not given, returns a list of dimension lengths. // Examples: -// array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]); // Returns [2,2,3] -// array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0); // Returns 2 -// array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2); // Returns 3 -// array_dim([[[1,2,3],[4,5,6]],[[7,8,9]]]); // Returns [2,undef,3] +// a = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]); // Returns [2,2,3] +// b = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0); // Returns 2 +// c = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2); // Returns 3 +// d = array_dim([[[1,2,3],[4,5,6]],[[7,8,9]]]); // Returns [2,undef,3] function array_dim(v, depth=undef) = assert( is_undef(depth) || ( is_finite(depth) && depth>=0 ), "Invalid depth.") ! is_list(v) ? 0 : @@ -1383,7 +1738,9 @@ function array_dim(v, depth=undef) = // Function: transpose() // Usage: -// transpose(arr, [reverse]) +// arr = transpose(arr, ); +// Topics: Matrices, Array Handling +// See Also: submatrix(), block_matrix(), hstack(), flatten() // Description: // Returns the transpose of the given input array. The input should be a list of lists that are // all the same length. If you give a vector then transpose returns it unchanged. @@ -1443,4 +1800,17 @@ function transpose(arr, reverse=false) = arr; +// Function: is_matrix_symmetric() +// Usage: +// b = is_matrix_symmetric(A,) +// Description: +// Returns true if the input matrix is symmetric, meaning it equals its transpose. +// Matrix should have numerical entries. +// Arguments: +// A = matrix to test +// eps = epsilon for comparing equality. Default: 1e-12 +function is_matrix_symmetric(A,eps=1e-12) = + approx(A,transpose(A)); + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/attachments.scad b/attachments.scad index 2c174106..04d8a17d 100644 --- a/attachments.scad +++ b/attachments.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: attachments.scad // This is the file that handles attachments and orientation of children. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -31,67 +29,65 @@ $tags_hidden = []; // Section: Anchors, Spin, and Orientation // This library adds the concept of anchoring, spin and orientation to the `cube()`, `cylinder()` // and `sphere()` builtins, as well as to most of the shapes provided by this library itself. -// * An anchor is a place on an object which you can align the object to, or attach other objects +// - An anchor is a place on an object which you can align the object to, or attach other objects // to using `attach()` or `position()`. An anchor has a position, a direction, and a spin. // The direction and spin are used to orient other objects to match when using `attach()`. -// * Spin is a simple rotation around the Z axis. -// * Orientation is rotating an object so that its top is pointed towards a given vector. +// - Spin is a simple rotation around the Z axis. +// - Orientation is rotating an object so that its top is pointed towards a given vector. // An object will first be translated to its anchor position, then spun, then oriented. // . // ## Anchor -// Anchoring is specified with the `anchor` argument in most shape modules. -// Specifying `anchor` when creating an object will translate the object so -// that the anchor point is at the origin (0,0,0). Anchoring always occurs -// before spin and orientation are applied. +// Anchoring is specified with the `anchor` argument in most shape modules. Specifying `anchor` +// when creating an object will translate the object so that the anchor point is at the origin +// (0,0,0). Anchoring always occurs before spin and orientation are applied. // . -// An anchor can be referred to in one of two ways; as a directional vector, -// or as a named anchor string. +// An anchor can be referred to in one of two ways; as a directional vector, or as a named anchor string. +// . +// When given as a vector, it points, in a general way, towards the face, edge, or corner of the +// object that you want the anchor for, relative to the center of the object. There are directional +// constants named `TOP`, `BOTTOM`, `FRONT`, `BACK`, `LEFT`, and `RIGHT` that you can add together +// to specify an anchor point. // . -// When given as a vector, it points, in a general way, towards the face, edge, or -// corner of the object that you want the anchor for, relative to the center of -// the object. There are directional constants named `TOP`, `BOTTOM`, `FRONT`, `BACK`, -// `LEFT`, and `RIGHT` that you can add together to specify an anchor point. // For example: -// - `[0,0,1]` is the same as `TOP` and refers to the center of the top face. -// - `[-1,0,1]` is the same as `TOP+LEFT`, and refers to the center of the top-left edge. -// - `[1,1,-1]` is the same as `BOTTOM+BACK+RIGHT`, and refers to the bottom-back-right corner. +// - `[0,0,1]` is the same as `TOP` and refers to the center of the top face. +// - `[-1,0,1]` is the same as `TOP+LEFT`, and refers to the center of the top-left edge. +// - `[1,1,-1]` is the same as `BOTTOM+BACK+RIGHT`, and refers to the bottom-back-right corner. // . -// The components of the directional vector should all be `1`, `0`, or `-1`. -// When the object is cylindrical, conical, or spherical in nature, the anchors will be -// located around the surface of the cylinder, cone, or sphere, relative to the center. -// The direction of a face anchor will be perpendicular to the face, pointing outward. -// The direction of a edge anchor will be the average of the anchor directions of the -// two faces the edge is between. The direction of a corner anchor will be the average -// of the anchor directions of the three faces the corner is on. The spin of all standard -// anchors is 0. +// When the object is cylindrical, conical, or spherical in nature, the anchors will be located +// around the surface of the cylinder, cone, or sphere, relative to the center. The direction of a +// face anchor will be perpendicular to the face, pointing outward. The direction of a edge anchor +// will be the average of the anchor directions of the two faces the edge is between. The direction +// of a corner anchor will be the average of the anchor directions of the three faces the corner is +// on. The spin of all standard anchors is 0. // . -// Some more complex objects, like screws and stepper motors, have named anchors -// to refer to places on the object that are not at one of the standard faces, edges -// or corners. For example, stepper motors have anchors for `"screw1"`, `"screw2"`, -// etc. to refer to the various screwholes on the stepper motor shape. The names, -// positions, directions, and spins of these anchors will be specific to the object, -// and will be documented when they exist. +// Some more complex objects, like screws and stepper motors, have named anchors to refer to places +// on the object that are not at one of the standard faces, edges or corners. For example, stepper +// motors have anchors for `"screw1"`, `"screw2"`, etc. to refer to the various screwholes on the +// stepper motor shape. The names, positions, directions, and spins of these anchors will be +// specific to the object, and will be documented when they exist. // . // ## Spin -// Spin is specified with the `spin` argument in most shape modules. Specifying `spin` -// when creating an object will rotate the object counter-clockwise around the Z axis -// by the given number of degrees. Spin is always applied after anchoring, and before -// orientation. +// Spin is specified with the `spin` argume// nt in most shape modules. Specifying a scalar `spin` +// when creating an object will rotate the object counter-clockwise around the Z axis by the given +// number of degrees. If given as a 3D vector, the object will be rotated around each of the X, Y, Z +// axes by the number of degrees in each component of the vector. Spin is always applied after +// anchoring, and before orientation. // . // ## Orient -// Orientation is specified with the `orient` argument in most shape modules. Specifying -// `orient` when creating an object will rotate the object such that the top of the -// object will be pointed at the vector direction given in the `orient` argument. -// Orientation is always applied after anchoring and spin. The constants `UP`, `DOWN`, -// `FRONT`, `BACK`, `LEFT`, and `RIGHT` can be added together to form the directional -// vector for this. ie: `LEFT+BACK` +// Orientation is specified with the `orient` argument in most shape modules. Specifying `orient` +// when creating an object will rotate the object such that the top of the object will be pointed +// at the vector direction given in the `orient` argument. Orientation is always applied after +// anchoring and spin. The constants `UP`, `DOWN`, `FRONT`, `BACK`, `LEFT`, and `RIGHT` can be +// added together to form the directional vector for this. ie: `LEFT+BACK` // Section: Functions // Function: anchorpt() // Usage: -// anchor(name, pos, , ) +// a = anchorpt(name, pos, , ); +// Topics: Attachments +// See Also: attach_geom(), reorient(), attachable() // Description: // Creates a anchor data structure. // Arguments: @@ -105,26 +101,30 @@ function anchorpt(name, pos=[0,0,0], orient=UP, spin=0) = [name, pos, orient, sp // Function: attach_geom() // // Usage: Square/Trapezoid Geometry -// geom = attach_geom(anchor, spin, two_d, size, , , , , ); +// geom = attach_geom(two_d=true, size=, , , ...); // Usage: Circle/Oval Geometry -// geom = attach_geom(anchor, spin, two_d, r|d, , , ); +// geom = attach_geom(two_d=true, r=|d=, ...); // Usage: 2D Path/Polygon Geometry -// geom = attach_geom(anchor, spin, two_d, path, , , , ); +// geom = attach_geom(two_d=true, path=, , ...); // Usage: Cubical/Prismoidal Geometry -// geom = attach_geom(anchor, spin, , size, , , , , ); +// geom = attach_geom(size=, , , ...); // Usage: Cylindrical Geometry -// geom = attach_geom(anchor, spin, , r|d, l, , , , ); +// geom = attach_geom(r=|d=, l=, , ...); // Usage: Conical Geometry -// geom = attach_geom(anchor, spin, , r1|d1, r2|d2, l, , , , ); +// geom = attach_geom(r1|d1=, r2=|d2=, l=, , ...); // Usage: Spheroid/Ovoid Geometry -// geom = attach_geom(anchor, spin, , r|d, , , ); +// geom = attach_geom(r=|d=, ...); // Usage: VNF Geometry -// geom = attach_geom(anchor, spin, , vnf, , , , ); +// geom = attach_geom(vnf=, , ...); +// +// Topics: Attachments +// See Also: reorient(), attachable() // // Description: // Given arguments that describe the geometry of an attachable object, returns the internal geometry description. // // Arguments: +// --- // size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length. // size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape. // shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift. @@ -145,65 +145,61 @@ function anchorpt(name, pos=[0,0,0], orient=UP, spin=0) = [name, pos, orient, sp // axis = The vector pointing along the axis of a cylinder geometry. Default: UP // // Example(NORENDER): Cubical Shape -// geom = attach_geom(anchor, spin, orient, size=size); +// geom = attach_geom(size=size); // // Example(NORENDER): Prismoidal Shape // geom = attach_geom( -// anchor, spin, orient, // size=point3d(botsize,h), // size2=topsize, shift=shift // ); // // Example(NORENDER): Cylindrical Shape, Z-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r=r, h=h); +// geom = attach_geom(r=r, h=h); // // Example(NORENDER): Cylindrical Shape, Y-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r=r, h=h, axis=BACK); +// geom = attach_geom(r=r, h=h, axis=BACK); // // Example(NORENDER): Cylindrical Shape, X-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r=r, h=h, axis=RIGHT); +// geom = attach_geom(r=r, h=h, axis=RIGHT); // // Example(NORENDER): Conical Shape, Z-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r1=r1, r2=r2, h=h); +// geom = attach_geom(r1=r1, r2=r2, h=h); // // Example(NORENDER): Conical Shape, Y-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r1=r1, r2=r2, h=h, axis=BACK); +// geom = attach_geom(r1=r1, r2=r2, h=h, axis=BACK); // // Example(NORENDER): Conical Shape, X-Axis Aligned -// geom = attach_geom(anchor, spin, orient, r1=r1, r2=r2, h=h, axis=RIGHT); +// geom = attach_geom(r1=r1, r2=r2, h=h, axis=RIGHT); // // Example(NORENDER): Spherical Shape -// geom = attach_geom(anchor, spin, orient, r=r); +// geom = attach_geom(r=r); // // Example(NORENDER): Ovoid Shape -// geom = attach_geom(anchor, spin, orient, r=[r_x, r_y, r_z]); +// geom = attach_geom(r=[r_x, r_y, r_z]); // // Example(NORENDER): Arbitrary VNF Shape, Anchored by Extents -// geom = attach_geom(anchor, spin, orient, vnf=vnf); +// geom = attach_geom(vnf=vnf); // // Example(NORENDER): Arbitrary VNF Shape, Anchored by Intersection -// geom = attach_geom(anchor, spin, orient, vnf=vnf, extent=false); +// geom = attach_geom(vnf=vnf, extent=false); // // Example(NORENDER): 2D Rectangular Shape -// geom = attach_geom(anchor, spin, orient, size=size); +// geom = attach_geom(two_d=true, size=size); // // Example(NORENDER): 2D Trapezoidal Shape -// geom = attach_geom( -// anchor, spin, orient, -// size=[x1,y], size2=x2, shift=shift -// ); +// geom = attach_geom(two_d=true, size=[x1,y], size2=x2, shift=shift); // // Example(NORENDER): 2D Circular Shape -// geom = attach_geom(anchor, spin, orient, two_d=true, r=r); +// geom = attach_geom(two_d=true, r=r); // // Example(NORENDER): 2D Oval Shape -// geom = attach_geom(anchor, spin, orient, two_d=true, r=[r_x, r_y]); +// geom = attach_geom(two_d=true, r=[r_x, r_y]); // // Example(NORENDER): Arbitrary 2D Polygon Shape, Anchored by Extents -// geom = attach_geom(anchor, spin, orient, path=path); +// geom = attach_geom(two_d=true, path=path); // // Example(NORENDER): Arbitrary 2D Polygon Shape, Anchored by Intersection -// geom = attach_geom(anchor, spin, orient, path=path, extent=false); +// geom = attach_geom(two_d=true, path=path, extent=false); // function attach_geom( size, size2, shift, @@ -284,7 +280,9 @@ function attach_geom( // Function: attach_geom_2d() // Usage: -// attach_geom_2d(geom); +// bool = attach_geom_2d(geom); +// Topics: Attachments +// See Also: reorient(), attachable() // Description: // Returns true if the given attachment geometry description is for a 2D shape. function attach_geom_2d(geom) = @@ -295,7 +293,9 @@ function attach_geom_2d(geom) = // Function: attach_geom_size() // Usage: -// attach_geom_size(geom); +// bounds = attach_geom_size(geom); +// Topics: Attachments +// See Also: reorient(), attachable() // Description: // Returns the `[X,Y,Z]` bounding size for the given attachment geometry description. function attach_geom_size(geom) = @@ -348,8 +348,12 @@ function attach_geom_size(geom) = // Function: attach_transform() -// Usage: +// Usage: To Get a Transformation Matrix // mat = attach_transform(anchor, spin, orient, geom); +// Usage: To Transform Points, Paths, Patches, or VNFs +// new_p = attach_transform(anchor, spin, orient, geom, p); +// Topics: Attachments +// See Also: reorient(), attachable() // Description: // Returns the affine3d transformation matrix needed to `anchor`, `spin`, and `orient` // the given geometry `geom` shape into position. @@ -359,10 +363,14 @@ function attach_geom_size(geom) = // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // geom = The geometry description of the shape. // p = If given as a VNF, path, or point, applies the affine3d transformation matrix to it and returns the result. -function attach_transform(anchor=CENTER, spin=0, orient=UP, geom, p) = - assert(is_string(anchor) || is_vector(anchor)) - assert(is_vector(orient)) +function attach_transform(anchor, spin, orient, geom, p) = + assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor)) + assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin)) + assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient)) let( + anchor = default(anchor, CENTER), + spin = default(spin, 0), + orient = default(orient, UP), two_d = attach_geom_2d(geom), m = ($attach_to != undef)? ( let( @@ -419,7 +427,9 @@ function attach_transform(anchor=CENTER, spin=0, orient=UP, geom, p) = // Function: find_anchor() // Usage: -// find_anchor(anchor, geom); +// anchorinfo = find_anchor(anchor, geom); +// Topics: Attachments +// See Also: reorient(), attachable() // Description: // Calculates the anchor data for the given `anchor` vector or name, in the given attachment // geometry. Returns `[ANCHOR, POS, VEC, ANG]` where `ANCHOR` is the requested anchorname @@ -579,7 +589,7 @@ function find_anchor(anchor, geom) = path = move(-point2d(cp), p=geom[1]), anchor = point2d(anchor), isects = [ - for (t=triplet_wrap(path)) let( + for (t=triplet(path,true)) let( seg1 = [t[0],t[1]], seg2 = [t[1],t[2]], isect = ray_segment_intersection([[0,0],anchor], seg1), @@ -613,7 +623,9 @@ function find_anchor(anchor, geom) = // Function: attachment_is_shown() // Usage: -// attachment_is_shown(tags); +// bool = attachment_is_shown(tags); +// Topics: Attachments +// See Also: reorient(), attachable() // Description: // Returns true if the given space-delimited string of tag names should currently be shown. function attachment_is_shown(tags) = @@ -629,21 +641,32 @@ function attachment_is_shown(tags) = // Function: reorient() // // Usage: Square/Trapezoid Geometry -// reorient(anchor, spin, , two_d, size, , , , , ,

); +// mat = reorient(anchor, spin, , two_d=true, size=, , , ...); +// pts = reorient(anchor, spin, , two_d=true, size=, , , p=, ...); // Usage: Circle/Oval Geometry -// reorient(anchor, spin, , two_d, r|d, , , ,

); +// mat = reorient(anchor, spin, , two_d=true, r=|d=, ...); +// pts = reorient(anchor, spin, , two_d=true, r=|d=, p=, ...); // Usage: 2D Path/Polygon Geometry -// reorient(anchor, spin, , two_d, path, , , , ,

); +// mat = reorient(anchor, spin, , two_d=true, path=, , ...); +// pts = reorient(anchor, spin, , two_d=true, path=, , p=, ...); // Usage: Cubical/Prismoidal Geometry -// reorient(anchor, spin, , size, , , , , ,

); +// mat = reorient(anchor, spin, , size=, , , ...); +// pts = reorient(anchor, spin, , size=, , , p=, ...); // Usage: Cylindrical Geometry -// reorient(anchor, spin, , r|d, l, , , , ,

); +// mat = reorient(anchor, spin, , r=|d=, l=, , ...); +// pts = reorient(anchor, spin, , r=|d=, l=, , p=, ...); // Usage: Conical Geometry -// reorient(anchor, spin, , r1|d1, r2|d2, l, , , , ,

); +// mat = reorient(anchor, spin, , r1=|d1=, r2=|d2=, l=, , ...); +// pts = reorient(anchor, spin, , r1=|d1=, r2=|d2=, l=, , p=, ...); // Usage: Spheroid/Ovoid Geometry -// reorient(anchor, spin, , r|d, , , ,

); +// mat = reorient(anchor, spin, , r|d=, ...); +// pts = reorient(anchor, spin, , r|d=, p=, ...); // Usage: VNF Geometry -// reorient(anchor, spin, , vnf, , , , ,

); +// mat = reorient(anchor, spin, , vnf, , ...); +// pts = reorient(anchor, spin, , vnf, , p=, ...); +// +// Topics: Attachments +// See Also: reorient(), attachable() // // Description: // Given anchor, spin, orient, and general geometry info for a managed volume, this calculates @@ -671,6 +694,7 @@ function attachment_is_shown(tags) = // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// --- // size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length. // size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape. // shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift. @@ -691,9 +715,7 @@ function attachment_is_shown(tags) = // axis = The vector pointing along the axis of a cylinder geometry. Default: UP // p = The VNF, path, or point to transform. function reorient( - anchor=CENTER, - spin=0, - orient=UP, + anchor, spin, orient, size, size2, shift, r,r1,r2, d,d1,d2, l,h, vnf, path, @@ -704,17 +726,26 @@ function reorient( two_d=false, axis=UP, p=undef -) = (anchor==CENTER && spin==0 && orient==UP && p!=undef)? p : let( - geom = attach_geom( - size=size, size2=size2, shift=shift, - r=r, r1=r1, r2=r2, h=h, - d=d, d1=d1, d2=d2, l=l, - vnf=vnf, path=path, extent=extent, - cp=cp, offset=offset, anchors=anchors, - two_d=two_d, axis=axis - ), - $attach_to = undef -) attach_transform(anchor,spin,orient,geom,p); +) = + assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor)) + assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin)) + assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient)) + let( + anchor = default(anchor, CENTER), + spin = default(spin, 0), + orient = default(orient, UP) + ) + (anchor==CENTER && spin==0 && orient==UP && p!=undef)? p : let( + geom = attach_geom( + size=size, size2=size2, shift=shift, + r=r, r1=r1, r2=r2, h=h, + d=d, d1=d1, d2=d2, l=l, + vnf=vnf, path=path, extent=extent, + cp=cp, offset=offset, anchors=anchors, + two_d=two_d, axis=axis + ), + $attach_to = undef + ) attach_transform(anchor,spin,orient,geom,p); @@ -723,21 +754,24 @@ function reorient( // Module: attachable() // // Usage: Square/Trapezoid Geometry -// attachable(anchor, spin, two_d, size, , , , , ... +// attachable(anchor, spin, two_d=true, size=, , , ...) {...} // Usage: Circle/Oval Geometry -// attachable(anchor, spin, two_d, r|d, , , ) ... +// attachable(anchor, spin, two_d=true, r=|d=, ...) {...} // Usage: 2D Path/Polygon Geometry -// attachable(anchor, spin, two_d, path, , , , ... +// attachable(anchor, spin, two_d=true, path=, , ...) {...} // Usage: Cubical/Prismoidal Geometry -// attachable(anchor, spin, , size, , , , , ... +// attachable(anchor, spin, , size=, , , ...) {...} // Usage: Cylindrical Geometry -// attachable(anchor, spin, , r|d, l, , , , ) ... +// attachable(anchor, spin, , r=|d=, l=, , ...) {...} // Usage: Conical Geometry -// attachable(anchor, spin, , r1|d1, r2|d2, l, , , , ) ... +// attachable(anchor, spin, , r1=|d1=, r2=|d2=, l=, , ...) {...} // Usage: Spheroid/Ovoid Geometry -// attachable(anchor, spin, , r|d, , , ) ... +// attachable(anchor, spin, , r=|d=, ...) {...} // Usage: VNF Geometry -// attachable(anchor, spin, , vnf, , , , ) ... +// attachable(anchor, spin, , vnf=, , ...) {...} +// +// Topics: Attachments +// See Also: reorient() // // Description: // Manages the anchoring, spin, orientation, and attachments for a 3D volume or 2D area. @@ -770,6 +804,7 @@ function reorient( // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// --- // size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length. // size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape. // shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift. @@ -890,9 +925,7 @@ function reorient( // children(); // } module attachable( - anchor=CENTER, - spin=0, - orient=UP, + anchor, spin, orient, size, size2, shift, r,r1,r2, d,d1,d2, l,h, vnf, path, @@ -903,10 +936,14 @@ module attachable( two_d=false, axis=UP ) { - assert($children==2, "attachable() expects exactly two children; the shape to manage, and the union of all attachment candidates."); - assert(!is_undef(anchor), str("anchor undefined in attachable(). Did you forget to set a default value for anchor in ", parent_module(1))); - assert(!is_undef(spin), str("spin undefined in attachable(). Did you forget to set a default value for spin in ", parent_module(1))); - assert(!is_undef(orient), str("orient undefined in attachable(). Did you forget to set a default value for orient in ", parent_module(1))); + dummy1 = + assert($children==2, "attachable() expects exactly two children; the shape to manage, and the union of all attachment candidates.") + assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor)) + assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin)) + assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient)); + anchor = default(anchor, CENTER); + spin = default(spin, 0); + orient = default(orient, UP); geom = attach_geom( size=size, size2=size2, shift=shift, r=r, r1=r1, r2=r2, h=h, @@ -942,7 +979,11 @@ module attachable( // Module: position() // Usage: -// position(from) ... +// position(from) {...} +// +// Topics: Attachments +// See Also: attachable() +// // Description: // Attaches children to a parent object at an anchor point. // Arguments: @@ -969,8 +1010,10 @@ module position(from) // Module: attach() // Usage: -// attach(from, ) ... -// attach(from, to, ) ... +// attach(from, , ) {...} +// attach(from, to, , ) {...} +// Topics: Attachments +// See Also: attachable(), position(), face_profile(), edge_profile(), corner_profile() // Description: // Attaches children to a parent object at an anchor point and orientation. // Attached objects will be overlapped into the parent object by a little bit, @@ -981,6 +1024,7 @@ module position(from) // Arguments: // from = The vector, or name of the parent anchor point to attach to. // to = Optional name of the child anchor point. If given, orients the child such that the named anchors align together rotationally. +// --- // overlap = Amount to sink child into the parent. Equivalent to `down(X)` after the attach. This defaults to the value in `$overlap`, which is `0.01` by default. // norot = If true, don't rotate children when attaching to the anchor point. Only translate to the anchor point. // Example: @@ -989,7 +1033,7 @@ module position(from) // attach(RIGHT, BOTTOM) down(1.5) cyl(l=11.5, d1=10, d2=5); // attach(FRONT, BOTTOM, overlap=1.5) cyl(l=11.5, d1=10, d2=5); // } -module attach(from, to=undef, overlap=undef, norot=false) +module attach(from, to, overlap, norot=false) { assert($parent_geom != undef, "No object to attach to!"); overlap = (overlap!=undef)? overlap : $overlap; @@ -1012,14 +1056,24 @@ module attach(from, to=undef, overlap=undef, norot=false) // Module: face_profile() // Usage: -// face_profile(faces, r, d, ) ... +// face_profile(faces, r|d=, ) {...} +// Topics: Attachments +// See Also: attachable(), position(), attach(), edge_profile(), corner_profile() // Description: // Given a 2D edge profile, extrudes it into a mask for all edges and corners bounding each given face. // Arguments: // faces = Faces to mask edges and corners of. // r = Radius of corner mask. +// --- // d = Diameter of corner mask. // convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10 +// Side Effects: +// Sets `$tags = "mask"` for all children. +// Example: +// diff("mask") +// cube([50,60,70],center=true) +// face_profile(TOP,r=10) +// mask2d_roundover(r=10); module face_profile(faces=[], r, d, convexity=10) { faces = is_vector(faces)? [faces] : faces; assert(all([for (face=faces) is_vector(face) && sum([for (x=face) x!=0? 1 : 0])==1]), "Vector in faces doesn't point at a face."); @@ -1032,7 +1086,9 @@ module face_profile(faces=[], r, d, convexity=10) { // Module: edge_profile() // Usage: -// edge_profile(, , ) ... +// edge_profile(, , ) {...} +// Topics: Attachments +// See Also: attachable(), position(), attach(), face_profile(), corner_profile() // Description: // Takes a 2D mask shape and attaches it to the selected edges, with the appropriate orientation // and extruded length to be `diff()`ed away, to give the edge a matching profile. @@ -1082,7 +1138,9 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) { // Module: corner_profile() // Usage: -// corner_profile(, , ) ... +// corner_profile(, , , ) {...} +// Topics: Attachments +// See Also: attachable(), position(), attach(), face_profile(), edge_profile() // Description: // Takes a 2D mask shape, rotationally extrudes and converts it into a corner mask, and attaches it // to the selected corners with the appropriate orientation. Tags it as a "mask" to allow it to be @@ -1090,6 +1148,7 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) { // Arguments: // corners = Edges to mask. See the docs for [`corners()`](edges.scad#corners) to see acceptable values. Default: All corners. // except = Edges to explicitly NOT mask. See the docs for [`corners()`](edges.scad#corners) to see acceptable values. Default: No corners. +// --- // r = Radius of corner mask. // d = Diameter of corner mask. // convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10 @@ -1144,7 +1203,9 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) { // Module: edge_mask() // Usage: -// edge_mask(, ) ... +// edge_mask(, ) {...} +// Topics: Attachments +// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask() // Description: // Takes a 3D mask shape, and attaches it to the given edges, with the // appropriate orientation to be `diff()`ed away. @@ -1186,7 +1247,9 @@ module edge_mask(edges=EDGES_ALL, except=[]) { // Module: corner_mask() // Usage: -// corner_mask(, ) ... +// corner_mask(, ) {...} +// Topics: Attachments +// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), edge_mask() // Description: // Takes a 3D mask shape, and attaches it to the given corners, with the appropriate // orientation to be `diff()`ed away. The 3D corner mask shape should be designed to @@ -1226,7 +1289,9 @@ module corner_mask(corners=CORNERS_ALL, except=[]) { // Module: tags() // Usage: -// tags(tags) ... +// tags(tags) {...} +// Topics: Attachments +// See Also: recolor(), hide(), show(), diff(), intersect() // Description: // Marks all children with the given tags, so that they will `hide()`/`show()`/`diff()` correctly. // This is especially useful for working with children that are not attachment enhanced, such as: @@ -1253,7 +1318,9 @@ module tags(tags) // Module: recolor() // Usage: -// recolor(c) ... +// recolor(c) {...} +// Topics: Attachments +// See Also: tags(), hide(), show(), diff(), intersect() // Description: // Sets the color for children that can use the $color special variable. // Arguments: @@ -1269,7 +1336,9 @@ module recolor(c) // Module: hide() // Usage: -// hide(tags) ... +// hide(tags) {...} +// Topics: Attachments +// See Also: tags(), recolor(), show(), diff(), intersect() // Description: // Hides all children with the given tags. Overrides any previous `hide()` or `show()` calls. // Example: @@ -1287,7 +1356,9 @@ module hide(tags="") // Module: show() // Usage: -// show(tags) ... +// show(tags) {...} +// Topics: Attachments +// See Also: tags(), recolor(), hide(), diff(), intersect() // Description: // Shows only children with the given tags. Overrides any previous `hide()` or `show()` calls. // Example: @@ -1305,8 +1376,10 @@ module show(tags="") // Module: diff() // Usage: -// diff(neg, ) ... -// diff(neg, pos, ) ... +// diff(neg, ) {...} +// diff(neg, pos, ) {...} +// Topics: Attachments +// See Also: tags(), recolor(), show(), hide(), intersect() // Description: // If `neg` is given, takes the union of all children with tags that are in `neg`, and differences // them from the union of all children with tags in `pos`. If `pos` is not given, then all items in @@ -1336,7 +1409,7 @@ module show(tags="") // rounding_mask_z(l=p.z, r=25); // } // } -module diff(neg, pos=undef, keep=undef) +module diff(neg, pos, keep) { // Don't perform the operation if the current tags are hidden if (attachment_is_shown($tags)) { @@ -1363,8 +1436,10 @@ module diff(neg, pos=undef, keep=undef) // Module: intersect() // Usage: -// intersect(a, ) ... -// intersect(a, b, ) ... +// intersect(a, ) {...} +// intersect(a, b, ) {...} +// Topics: Attachments +// See Also: tags(), recolor(), show(), hide(), diff() // Description: // If `a` is given, takes the union of all children with tags that are in `a`, and `intersection()`s // them with the union of all children with tags in `b`. If `b` is not given, then the union of all @@ -1375,6 +1450,7 @@ module diff(neg, pos=undef, keep=undef) // Arguments: // a = String containing space delimited set of tag names of children. // b = String containing space delimited set of tag names of children. +// --- // keep = String containing space delimited set of tag names of children to keep whole. // Example: // intersect("wheel", "mask", keep="axle") @@ -1410,7 +1486,9 @@ module intersect(a, b=undef, keep=undef) // Module: hulling() // Usage: -// hulling(a) ... +// hulling(a) {...} +// Topics: Attachments +// See Also: tags(), recolor(), show(), hide(), diff(), intersect() // Description: // If `a` is not given, then all children are `hull()`ed together. // If `a` is given as a string, then all children with `$tags` that are in `a` are diff --git a/beziers.scad b/beziers.scad index 23735c9b..2a74ac38 100644 --- a/beziers.scad +++ b/beziers.scad @@ -1,48 +1,203 @@ ////////////////////////////////////////////////////////////////////// // LibFile: beziers.scad // Bezier functions and modules. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// +// Terminology: +// Path = A series of points joined by straight line segements. +// Bezier Curve = A mathematical curve that joins two endpoints, following a curve determined by one or more control points. +// Endpoint = A point that is on the end of a bezier segment. This point lies on the bezier curve. +// Control Point = A point that influences the shape of the curve that connects two endpoints. This is often *NOT* on the bezier curve. +// Degree = The number of control points, plus one endpoint, needed to specify a bezier segment. Most beziers are cubic (degree 3). +// Bezier Segment = A list consisting of an endpoint, one or more control points, and a final endpoint. The number of control points is one less than the degree of the bezier. A cubic (degree 3) bezier segment looks something like: `[endpt1, cp1, cp2, endpt2]` +// Bezier Path = A list of bezier segments flattened out into a list of points, where each segment shares the endpoint of the previous segment as a start point. A cubic Bezier Path looks something like: `[endpt1, cp1, cp2, endpt2, cp3, cp4, endpt3]` **NOTE:** A "bezier path" is *NOT* a standard path. It is only the points and controls used to define the curve. +// Bezier Patch = A surface defining grid of (N+1) by (N+1) bezier points. If a Bezier Segment defines a curved line, a Bezier Patch defines a curved surface. +// Bezier Surface = A surface defined by a list of one or more bezier patches. +// Spline Steps = The number of straight-line segments to split a bezier segment into, to approximate the bezier curve. The more spline steps, the closer the approximation will be to the curve, but the slower it will be to generate. Usually defaults to 16. -// Section: Terminology -// **Path**: A series of points joined by straight line segements. -// . -// **Bezier Curve**: A mathematical curve that joins two endpoints, following a curve determined by one or more control points. -// . -// **Endpoint**: A point that is on the end of a bezier segment. This point lies on the bezier curve. -// . -// **Control Point**: A point that influences the shape of the curve that connects two endpoints. This is often *NOT* on the bezier curve. -// . -// **Degree**: The number of control points, plus one endpoint, needed to specify a bezier segment. Most beziers are cubic (degree 3). -// . -// **Bezier Segment**: A list consisting of an endpoint, one or more control points, and a final endpoint. The number of control points is one less than the degree of the bezier. A cubic (degree 3) bezier segment looks something like: -// `[endpt1, cp1, cp2, endpt2]` -// . -// **Bezier Path**: A list of bezier segments flattened out into a list of points, where each segment shares the endpoint of the previous segment as a start point. A cubic Bezier Path looks something like: -// `[endpt1, cp1, cp2, endpt2, cp3, cp4, endpt3]` -// **NOTE**: A "bezier path" is *NOT* a standard path. It is only the points and controls used to define the curve. -// . -// **Bezier Patch**: A surface defining grid of (N+1) by (N+1) bezier points. If a Bezier Segment defines a curved line, a Bezier Patch defines a curved surface. -// . -// **Bezier Surface**: A surface defined by a list of one or more bezier patches. -// . -// **Spline Steps**: The number of straight-line segments to split a bezier segment into, to approximate the bezier curve. The more spline steps, the closer the approximation will be to the curve, but the slower it will be to generate. Usually defaults to 16. + +// Section: Bezier Path Construction + +// Function: bez_begin() +// Topics: Bezier Paths +// See Also: bez_tang(), bez_joint(), bez_end() +// Usage: +// pts = bez_begin(pt,a,r,); +// pts = bez_begin(pt,VECTOR,,); +// Description: +// This is used to create the first endpoint and control point of a cubic bezier path. +// Arguments: +// pt = The starting endpoint for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the first control point. +// r = Specifies the distance of the control point from the endpoint `pt`. +// p = If given, specifies the number of degrees away from the Z+ axis. +// Example(2D): 2D Bezier Path by Angle +// bezpath = flatten([ +// bez_begin([-50, 0], 45,20), +// bez_tang ([ 0, 0],-135,20), +// bez_joint([ 20,-25], 135, 90, 10, 15), +// bez_end ([ 50, 0], -90,20), +// ]); +// trace_bezier(bezpath); +// Example(2D): 2D Bezier Path by Vector +// bezpath = flatten([ +// bez_begin([-50,0],[0,-20]), +// bez_tang ([-10,0],[0,-20]), +// bez_joint([ 20,-25], [-10,10], [0,15]), +// bez_end ([ 50,0],[0, 20]), +// ]); +// trace_bezier(bezpath); +// Example(2D): 2D Bezier Path by Vector and Distance +// bezpath = flatten([ +// bez_begin([-30,0],FWD, 30), +// bez_tang ([ 0,0],FWD, 30), +// bez_joint([ 20,-25], 135, 90, 10, 15), +// bez_end ([ 30,0],BACK,30), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=200): 3D Bezier Path by Angle +// bezpath = flatten([ +// bez_begin([-30,0,0],90,20,p=135), +// bez_tang ([ 0,0,0],-90,20,p=135), +// bez_joint([20,-25,0], 135, 90, 15, 10, p1=135, p2=45), +// bez_end ([ 30,0,0],-90,20,p=45), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=225): 3D Bezier Path by Vector +// bezpath = flatten([ +// bez_begin([-30,0,0],[0,-20, 20]), +// bez_tang ([ 0,0,0],[0,-20,-20]), +// bez_joint([20,-25,0],[0,10,-10],[0,15,15]), +// bez_end ([ 30,0,0],[0,-20,-20]), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=225): 3D Bezier Path by Vector and Distance +// bezpath = flatten([ +// bez_begin([-30,0,0],FWD, 20), +// bez_tang ([ 0,0,0],DOWN,20), +// bez_joint([20,-25,0],LEFT,DOWN,r1=20,r2=15), +// bez_end ([ 30,0,0],DOWN,20), +// ]); +// trace_bezier(bezpath); +function bez_begin(pt,a,r,p) = + assert(is_finite(r) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + is_vector(a)? [pt, pt+(is_undef(r)? a : r*unit(a))] : + is_finite(a)? [pt, pt+spherical_to_xyz(r,a,default(p,90))] : + assert(false, "Bad arguments."); + + +// Function: bez_tang() +// Topics: Bezier Paths +// See Also: bez_begin(), bez_joint(), bez_end() +// Usage: +// pts = bez_tang(pt,a,r1,r2,); +// pts = bez_tang(pt,VECTOR,,,); +// Description: +// This creates a smooth joint in a cubic bezier path. It creates three points, being the +// approaching control point, the fixed bezier control point, and the departing control +// point. The two control points will be collinear with the fixed point, making for a +// smooth bezier curve at the fixed point. See {{bez_begin()}} for examples. +// Arguments: +// pt = The fixed point for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the departing control point. +// r1 = Specifies the distance of the approching control point from the fixed point. Overrides the distance component of the vector if `a` contains a vector. +// r2 = Specifies the distance of the departing control point from the fixed point. Overrides the distance component of the vector if `a` contains a vector. If `r1` is given and `r2` is not, uses the value of `r1` for `r2`. +// p = If given, specifies the number of degrees away from the Z+ axis. +function bez_tang(pt,a,r1,r2,p) = + assert(is_finite(r1) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + let( + r1 = is_num(r1)? r1 : norm(a), + r2 = default(r2,r1), + p = default(p, 90) + ) + is_vector(a)? [pt-r1*unit(a), pt, pt+r2*unit(a)] : + is_finite(a)? [ + pt-spherical_to_xyz(r1,a,p), + pt, + pt+spherical_to_xyz(r2,a,p) + ] : + assert(false, "Bad arguments."); + + +// Function: bez_joint() +// Topics: Bezier Paths +// See Also: bez_begin(), bez_tang(), bez_end() +// Usage: +// pts = bez_joint(pt,a1,a2,r1,r2,,); +// pts = bez_joint(pt,VEC1,VEC2,,,,); +// Description: +// This creates a disjoint corner joint in a cubic bezier path. It creates three points, being +// the aproaching control point, the fixed bezier control point, and the departing control point. +// The two control points can be directed in different arbitrary directions from the fixed bezier +// point. See {{bez_begin()}} for examples. +// Arguments: +// pt = The fixed point for the bezier path. +// a1 = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the approaching control point. +// a2 = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the departing control point. +// r1 = Specifies the distance of the approching control point from the fixed point. Overrides the distance component of the vector if `a1` contains a vector. +// r2 = Specifies the distance of the departing control point from the fixed point. Overrides the distance component of the vector if `a2` contains a vector. +// p1 = If given, specifies the number of degrees away from the Z+ axis of the approaching control point. +// p2 = If given, specifies the number of degrees away from the Z+ axis of the departing control point. +function bez_joint(pt,a1,a2,r1,r2,p1,p2) = + assert(is_finite(r1) || is_vector(a1)) + assert(is_finite(r2) || is_vector(a2)) + assert(len(pt)==3 || (is_undef(p1) && is_undef(p2))) + let( + r1 = is_num(r1)? r1 : norm(a1), + r2 = is_num(r2)? r2 : norm(a2), + p1 = default(p1, 90), + p2 = default(p2, 90) + ) [ + if (is_vector(a1)) (pt+r1*unit(a1)) + else if (is_finite(a1)) (pt+spherical_to_xyz(r1,a1,p1)) + else assert(false, "Bad Arguments"), + pt, + if (is_vector(a2)) (pt+r2*unit(a2)) + else if (is_finite(a2)) (pt+spherical_to_xyz(r2,a2,p2)) + else assert(false, "Bad Arguments") + ]; + + +// Function: bez_end() +// Topics: Bezier Paths +// See Also: bez_tang(), bez_joint(), bez_end() +// Usage: +// pts = bez_end(pt,a,r,); +// pts = bez_end(pt,VECTOR,,); +// Description: +// This is used to create the approaching control point, and the endpoint of a cubic bezier path. +// See {{bez_begin()}} for examples. +// Arguments: +// pt = The starting endpoint for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the first control point. +// r = Specifies the distance of the control point from the endpoint `pt`. +// p = If given, specifies the number of degrees away from the Z+ axis. +function bez_end(pt,a,r,p) = + assert(is_finite(r) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + is_vector(a)? [pt+(is_undef(r)? a : r*unit(a)), pt] : + is_finite(a)? [pt+spherical_to_xyz(r,a,default(p,90)), pt] : + assert(false, "Bad arguments."); // Section: Segment Functions // Function: bezier_points() // Usage: -// bezier_points(curve, u) +// pt = bezier_points(curve, u); +// ptlist = bezier_points(curve, RANGE); +// ptlist = bezier_points(curve, LIST); +// Topics: Bezier Segments // Description: -// Computes bezier points for bezier with control points specified by `curve` at parameter values specified by `u`, which can be a scalar or a list. -// This function uses an optimized method which is best when `u` is a long list and the bezier degree is 10 or less. -// The degree of the bezier curve given is `len(curve)-1`. +// Computes bezier points for bezier with control points specified by `curve` at parameter values +// specified by `u`, which can be a scalar or a list. This function uses an optimized method which +// is best when `u` is a long list and the bezier degree is 10 or less. The degree of the bezier +// curve given is `len(curve)-1`. // Arguments: // curve = The list of endpoints and control points for this bezier segment. // u = The proportion of the way along the curve to find the point of. 0<=`u`<=1 If given as a list or range, returns a list of point, one for each u value. @@ -181,7 +336,11 @@ function _bezier_matrix(N) = // Function: bezier_derivative() // Usage: -// d = bezier_derivative(curve, u, [order]); +// deriv = bezier_derivative(curve, u, ); +// derivs = bezier_derivative(curve, LIST, ); +// derivs = bezier_derivative(curve, RANGE, ); +// Topics: Bezier Segments +// See Also: bezier_curvature(), bezier_tangent(), bezier_points() // Description: // Finds the `order`th derivative of the bezier segment at the given position `u`. // The degree of the bezier segment is one less than the number of points in `curve`. @@ -201,7 +360,11 @@ function bezier_derivative(curve, u, order=1) = // Function: bezier_tangent() // Usage: -// tanvec= bezier_tangent(curve, u); +// tanvec = bezier_tangent(curve, u); +// tanvecs = bezier_tangent(curve, LIST); +// tanvecs = bezier_tangent(curve, RANGE); +// Topics: Bezier Segments +// See Also: bezier_curvature(), bezier_derivative(), bezier_points() // Description: // Returns the unit vector of the tangent at the given position `u` on the bezier segment `curve`. // Arguments: @@ -218,6 +381,10 @@ function bezier_tangent(curve, u) = // Function: bezier_curvature() // Usage: // crv = bezier_curvature(curve, u); +// crvlist = bezier_curvature(curve, LIST); +// crvlist = bezier_curvature(curve, RANGE); +// Topics: Bezier Segments +// See Also: bezier_tangent(), bezier_derivative(), bezier_points() // Description: // Returns the curvature value for the given position `u` on the bezier segment `curve`. // The curvature is the inverse of the radius of the tangent circle at the given point. @@ -240,19 +407,22 @@ function bezier_curvature(curve, u) = ]; - // Function: bezier_curve() // Usage: -// bezier_curve(curve, n); +// path = bezier_curve(curve, n, ); +// Topics: Bezier Segments +// See Also: bezier_curvature(), bezier_tangent(), bezier_derivative(), bezier_points() // Description: -// Takes a list of bezier curve control points, and a count of path points to generate. The points -// returned will be along the curve, starting at the first control point, then about every `1/n`th -// of the way along the curve, ending about `1/n`th of the way *before* the final control point. -// The distance between the points will *not* be equidistant. The degree of the curve, N, is one +// Takes a list of bezier curve control points and generates n points along the bezier path. +// Points start at the first control point and are sampled every `1/n`th +// of the way along the bezier parameter, ending *before* the final control point by default. +// The distance between the points will *not* be equidistant. If you wish to add the +// endpoint you can set `endpoint` to true. The degree of the bezier curve is one // less than the number of points in `curve`. // Arguments: // curve = The list of endpoints and control points for this bezier segment. // n = The number of points to generate along the bezier curve. +// endpoint = if true then add the endpoint (an extra point, giving n+1 points output). Default: False // Example(2D): Quadratic (Degree 2) Bezier. // bez = [[0,0], [30,30], [80,0]]; // move_copies(bezier_curve(bez, 8)) sphere(r=1.5, $fn=12); @@ -265,12 +435,15 @@ function bezier_curvature(curve, u) = // bez = [[0,0], [5,15], [40,20], [60,-15], [80,0]]; // move_copies(bezier_curve(bez, 8)) sphere(r=1.5, $fn=12); // trace_bezier(bez, N=len(bez)-1); -function bezier_curve(curve,n) = bezier_points(curve, [0:1/n:(n-0.5)/n]); - +function bezier_curve(curve,n,endpoint) = [each bezier_points(curve, [0:1/n:(n-0.5)/n]), + if (endpoint) curve[len(curve)-1] + ]; // Function: bezier_segment_closest_point() // Usage: -// bezier_segment_closest_point(bezier,pt) +// u = bezier_segment_closest_point(bezier, pt, ); +// Topics: Bezier Segments +// See Also: bezier_points() // Description: // Finds the closest part of the given bezier segment to point `pt`. // The degree of the curve, N, is one less than the number of points in `curve`. @@ -315,11 +488,11 @@ function bezier_segment_closest_point(curve, pt, max_err=0.01, u=0, end_u=1) = bezier_segment_closest_point(curve, pt, max_err=max_err, u=minima[0], end_u=minima[1]); - - // Function: bezier_segment_length() // Usage: -// bezier_segment_length(curve, [start_u], [end_u], [max_deflect]); +// pathlen = bezier_segment_length(curve, , , ); +// Topics: Bezier Segments +// See Also: bezier_points() // Description: // Approximates the length of the bezier segment between start_u and end_u. // Arguments: @@ -336,7 +509,7 @@ function bezier_segment_length(curve, start_u=0, end_u=1, max_deflect=0.01) = uvals = [for (i=[0:1:segs]) lerp(start_u, end_u, i/segs)], path = bezier_points(curve,uvals), defl = max([ - for (i=idx(path,end=-3)) let( + for (i=idx(path,e=-3)) let( mp = (path[i] + path[i+2]) / 2 ) norm(path[i+1] - mp) ]), @@ -352,20 +525,47 @@ function bezier_segment_length(curve, start_u=0, end_u=1, max_deflect=0.01) = +// Function: bezier_line_intersection() +// Usage: +// u = bezier_line_intersection(curve, line); +// Topics: Bezier Segments, Geometry, Intersection +// See Also: bezier_points(), bezier_segment_length(), bezier_segment_closest_point() +// Description: +// Finds the parameter(s) of the 2d curve whose Bezier control points are `curve` +// corresponding to its intersection points with the line through +// the pair of distinct 2d points in `line`. +// Arguments: +// curve = List of 2d control points for the Bezier curve +// line = a list of two distinct 2d points defining a line +function bezier_line_intersection(curve, line) = + assert(is_path(curve,2), "The input ´curve´ must be a 2d bezier") + assert(_valid_line(line,2), "The input `line` is not a valid 2d line") + let( + a = _bezier_matrix(len(curve)-1)*curve, // curve algebraic coeffs. + n = [-line[1].y+line[0].y, line[1].x-line[0].x], // line normal + q = [for(i=[len(a)-1:-1:1]) a[i]*n, (a[0]-line[0])*n] // curve SDF to line + ) + [for(u=real_roots(q)) if (u>=0 && u<=1) u]; + + + // Function: fillet3pts() // Usage: -// fillet3pts(p0, p1, p2, r|d); +// bez_path_pts = fillet3pts(p0, p1, p2, r); +// bez_path_pts = fillet3pts(p0, p1, p2, d=); +// Topics: Bezier Segments, Rounding +// See Also: bezier_points(), bezier_curve() // Description: -// Takes three points, defining two line segments, and works out the -// cubic (degree 3) bezier segment (and surrounding control points) -// needed to approximate a rounding of the corner with radius `r`. -// If there isn't room for a radius `r` rounding, uses the largest -// radius that will fit. Returns [cp1, endpt1, cp2, cp3, endpt2, cp4] +// Takes three points, defining two line segments, and works out the cubic (degree 3) bezier segment +// (and surrounding control points) needed to approximate a rounding of the corner with radius `r`. +// If there isn't room for a radius `r` rounding, uses the largest radius that will fit. Returns +// [cp1, endpt1, cp2, cp3, endpt2, cp4] // Arguments: // p0 = The starting point. // p1 = The middle point. // p2 = The ending point. // r = The radius of the fillet/rounding. +// --- // d = The diameter of the fillet/rounding. // maxerr = Max amount bezier curve should diverge from actual curve. Default: 0.1 // Example(2D): @@ -401,7 +601,11 @@ function fillet3pts(p0, p1, p2, r, d, maxerr=0.1, w=0.5, dw=0.25) = let( // Function: bezier_path_point() // Usage: -// bezier_path_point(path, seg, u, [N]) +// pt = bezier_path_point(path, seg, u, ); +// ptlist = bezier_path_point(path, seg, LIST, ); +// path = bezier_path_point(path, seg, RANGE, ); +// Topics: Bezier Paths +// See Also: bezier_points(), bezier_curve() // Description: // Returns the coordinates of bezier path segment `seg` at position `u`. // Arguments: @@ -416,7 +620,9 @@ function bezier_path_point(path, seg, u, N=3) = // Function: bezier_path_closest_point() // Usage: -// bezier_path_closest_point(bezier,pt) +// res = bezier_path_closest_point(bezier,pt); +// Topics: Bezier Paths +// See Also: bezier_points(), bezier_curve(), bezier_segment_closest_point() // Description: // Finds the closest part of the given bezier path to point `pt`. // Returns [segnum, u] for the closest position on the bezier path to the given point `pt`. @@ -458,7 +664,9 @@ function bezier_path_closest_point(path, pt, N=3, max_err=0.01, seg=0, min_seg=u // Function: bezier_path_length() // Usage: -// bezier_path_length(path, [N], [max_deflect]); +// plen = bezier_path_length(path, , ); +// Topics: Bezier Paths +// See Also: bezier_points(), bezier_curve(), bezier_segment_length() // Description: // Approximates the length of the bezier path. // Arguments: @@ -482,7 +690,9 @@ function bezier_path_length(path, N=3, max_deflect=0.001) = // Function: bezier_path() // Usage: -// bezier_path(bezier, [splinesteps], [N]) +// path = bezier_path(bezier, , ) +// Topics: Bezier Paths +// See Also: bezier_points(), bezier_curve() // Description: // Takes a bezier path and converts it into a path of points. // Arguments: @@ -515,28 +725,31 @@ function bezier_path(bezier, splinesteps=16, N=3) = // Function: path_to_bezier() // Usage: -// path_to_bezier(path, [size|relsize], [tangents], [uniform], [closed]) +// bezpath = path_to_bezier(path, , , , |); +// Topics: Bezier Paths, Rounding +// See Also: path_tangents(), fillet_path() // Description: -// Given a 2d or 3d input path and optional list of tangent vectors, computes a cubic (dgree 3) bezier -// path that passes through every poin on the input path and matches the tangent vectors. If you do -// not supply the tangent it will be computed using path_tangents. If the path is closed specify this -// by setting closed=true. The size or relsize parameter determines how far the curve can deviate from +// Given a 2d or 3d input path and optional list of tangent vectors, computes a cubic (degree 3) bezier +// path that passes through every point on the input path and matches the tangent vectors. If you do +// not supply the tangent it will be computed using `path_tangents()`. If the path is closed specify this +// by setting `closed=true`. The size or relsize parameter determines how far the curve can deviate from // the input path. In the case where the curve has a single hump, the size specifies the exact distance // between the specified path and the bezier. If you give relsize then it is relative to the segment // length (e.g. 0.05 means 5% of the segment length). In 2d when the bezier curve makes an S-curve // the size parameter specifies the sum of the deviations of the two peaks of the curve. In 3-space // the bezier curve may have three extrema: two maxima and one minimum. In this case the size specifies -// the sum of the maxima minus the minimum. If you do not supply the tangents then they are -// computed using path_tangents with uniform=false by default. Tangents computed on non-uniform -// data tend to display overshoots. See smooth_path for examples. +// the sum of the maxima minus the minimum. If you do not supply the tangents then they are computed +// using `path_tangents()` with `uniform=false` by default. Tangents computed on non-uniform data tend +// to display overshoots. See `smooth_path()` for examples. // Arguments: -// path = 2d or 3d point list that the curve must pass through -// size = absolute size specification for the curve, a number or vector -// relsize = relative size specification for the curve, a number or vector. Default: 0.1. +// path = 2D or 3D point list that the curve must pass through +// closed = true if the curve is closed . Default: false // tangents = tangents constraining curve direction at each point // uniform = set to true to compute tangents with uniform=true. Default: false -// closed = true if the curve is closed . Default: false -function path_to_bezier(path, tangents, size, relsize, uniform=false, closed=false) = +// --- +// size = absolute size specification for the curve, a number or vector +// relsize = relative size specification for the curve, a number or vector. Default: 0.1. +function path_to_bezier(path, closed=false, tangents, uniform=false, size, relsize) = assert(is_bool(closed)) assert(is_bool(uniform)) assert(num_defined([size,relsize])<=1, "Can't define both size and relsize") @@ -594,7 +807,9 @@ function path_to_bezier(path, tangents, size, relsize, uniform=false, closed=fal // Function: fillet_path() // Usage: -// fillet_path(pts, fillet, [maxerr]); +// bezpath = fillet_path(pts, fillet, ); +// Topics: Bezier Paths, Rounding +// See Also: path_to_bezier(), bezier_path() // Description: // Takes a 3D path and fillets the corners, returning a 3d cubic (degree 3) bezier path. // Arguments: @@ -621,13 +836,15 @@ function fillet_path(pts, fillet, maxerr=0.1) = concat( // Function: bezier_close_to_axis() // Usage: -// bezier_close_to_axis(bezier, [N], [axis]); +// bezpath = bezier_close_to_axis(bezier, , ); +// Topics: Bezier Paths +// See Also: bezier_offset() // Description: // Takes a 2D bezier path and closes it to the specified axis. // Arguments: // bezier = The 2D bezier path to close to the axis. -// N = The degree of the bezier curves. Cubic beziers have N=3. Default: 3 // axis = The axis to close to, "X", or "Y". Default: "X" +// N = The degree of the bezier curves. Cubic beziers have N=3. Default: 3 // Example(2D): // bez = [[50,30], [40,10], [10,50], [0,30], [-10, 10], [-30,10], [-50,20]]; // closed = bezier_close_to_axis(bez); @@ -636,7 +853,7 @@ function fillet_path(pts, fillet, maxerr=0.1) = concat( // bez = [[30,50], [10,40], [50,10], [30,0], [10, -10], [10,-30], [20,-50]]; // closed = bezier_close_to_axis(bez, axis="Y"); // trace_bezier(closed, size=1); -function bezier_close_to_axis(bezier, N=3, axis="X") = +function bezier_close_to_axis(bezier, axis="X", N=3) = assert(is_path(bezier,2), "bezier_close_to_axis() can only work on 2D bezier paths.") assert(is_int(N)) assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")) @@ -661,7 +878,9 @@ function bezier_close_to_axis(bezier, N=3, axis="X") = // Function: bezier_offset() // Usage: -// bezier_offset(offset, bezier, [N]); +// bezpath = bezier_offset(offset, bezier, ); +// Topics: Bezier Paths +// See Also: bezier_close_to_axis() // Description: // Takes a 2D bezier path and closes it with a matching reversed path that is offset by the given `offset` [X,Y] distance. // Arguments: @@ -698,7 +917,9 @@ function bezier_offset(offset, bezier, N=3) = // Module: bezier_polygon() // Usage: -// bezier_polygon(bezier, [splinesteps], [N]) { +// bezier_polygon(bezier, , ) { +// Topics: Bezier Paths +// See Also: bezier_path() // Description: // Takes a closed 2D bezier path, and creates a 2D polygon from it. // Arguments: @@ -725,171 +946,20 @@ module bezier_polygon(bezier, splinesteps=16, N=3) { } -// Module: linear_sweep_bezier() -// Usage: -// linear_sweep_bezier(bezier, height, [splinesteps], [N], [center], [convexity], [twist], [slices], [scale]); -// Description: -// Takes a closed 2D bezier path, centered on the XY plane, and -// extrudes it linearly upwards, forming a solid. -// Arguments: -// bezier = Array of 2D points of a bezier path, to be extruded. -// splinesteps = Number of steps to divide each bezier segment into. default=16 -// N = The degree of the bezier curves. Cubic beziers have N=3. Default: 3 -// convexity = max number of walls a line could pass through, for preview. default=10 -// twist = Angle in degrees to twist over the length of extrusion. default=0 -// scale = Relative size of top of extrusion to the bottom. default=1.0 -// slices = Number of vertical slices to use for twisted extrusion. default=20 -// center = If true, the extruded solid is centered vertically at z=0. -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `BOTTOM` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Example: -// bez = [ -// [-10, 0], [-15, -5], -// [ -5, -10], [ 0, -10], [ 5, -10], -// [ 10, -5], [ 15, 0], [10, 5], -// [ 5, 10], [ 0, 10], [-5, 10], -// [ 25, -15], [-10, 0] -// ]; -// linear_sweep_bezier(bez, height=20, splinesteps=32); -module linear_sweep_bezier(bezier, height=100, splinesteps=16, N=3, center, convexity, twist, slices, scale, anchor, spin=0, orient=UP) { - assert(is_path(bezier,2), "linear_sweep_bezier() can only work on 2D bezier paths."); - assert(is_num(height)); - assert(is_int(splinesteps)); - assert(is_int(N)); - assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); - maxx = max([for (pt = bezier) abs(pt[0])]); - maxy = max([for (pt = bezier) abs(pt[1])]); - anchor = get_anchor(anchor,center,BOT,BOT); - attachable(anchor,spin,orient, size=[maxx*2,maxy*2,height]) { - if (height > 0) { - linear_extrude(height=height, center=true, convexity=convexity, twist=twist, slices=slices, scale=scale) { - bezier_polygon(bezier, splinesteps=splinesteps, N=N); - } - } - children(); - } -} - - -// Module: rotate_sweep_bezier() -// Usage: -// rotate_sweep_bezier(bezier, [splinesteps], [N], [convexity], [angle]) -// Description: -// Takes a closed 2D bezier and rotates it around the Z axis, forming a solid. -// Behaves like rotate_extrude(), except for beziers instead of shapes. -// Arguments: -// bezier = array of 2D points for the bezier path to rotate. -// splinesteps = number of segments to divide each bezier segment into. default=16 -// N = number of points in each bezier segment. default=3 (cubic) -// convexity = max number of walls a line could pass through, for preview. default=2 -// angle = Degrees of sweep to make. Default: 360 -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Example(Spin): -// path = [ -// [ 0, 10], [ 50, 0], [ 50, 40], -// [ 95, 40], [100, 40], [100, 45], -// [ 95, 45], [ 66, 45], [ 0, 20], -// [ 0, 12], [ 0, 12], [ 0, 10], -// [ 0, 10] -// ]; -// rotate_sweep_bezier(path, splinesteps=32, $fn=180); -module rotate_sweep_bezier(bezier, splinesteps=16, N=3, convexity=undef, angle=360, anchor=CENTER, spin=0, orient=UP) -{ - assert(is_path(bezier,2), "rotate_sweep_bezier() can only work on 2D bezier paths."); - assert(is_int(splinesteps)); - assert(is_int(N)); - assert(is_num(angle)); - assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); - oline = bezier_path(bezier, splinesteps=splinesteps, N=N); - maxx = max([for (pt = oline) abs(pt[0])]); - miny = min(subindex(oline,1)); - maxy = max(subindex(oline,1)); - attachable(anchor,spin,orient, r=maxx, l=max(abs(miny),abs(maxy))*2) { - rotate_extrude(convexity=convexity, angle=angle) { - polygon(oline); - } - children(); - } -} - - -// Module: bezier_path_extrude() -// Usage: -// bezier_path_extrude(bezier, [splinesteps], [N], [convexity], [clipsize]) ... -// Description: -// Extrudes 2D shape children along a bezier path. -// Arguments: -// bezier = array of points for the bezier path to extrude along. -// splinesteps = number of segments to divide each bezier segment into. default=16 -// N = The degree of the bezier path to extrude. -// convexity = max number of walls a line could pass through, for preview. default=2 -// clipsize = Size of cube to use for clipping beveled ends with. -// Example(FR): -// path = [ [0, 0, 0], [33, 33, 33], [66, -33, -33], [100, 0, 0] ]; -// bezier_path_extrude(path) difference(){ -// circle(r=10); -// fwd(10/2) circle(r=8); -// } -module bezier_path_extrude(bezier, splinesteps=16, N=3, convexity=undef, clipsize=1000) { - assert(is_path(bezier)); - assert(is_int(splinesteps)); - assert(is_int(N)); - assert(is_num(clipsize)); - assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); - path = slice(bezier_path(bezier, splinesteps, N), 0, -1); - path_extrude(path, convexity=convexity, clipsize=clipsize) children(); -} - - -// Module: bezier_sweep_bezier() -// Usage: -// bezier_sweep_bezier(bezier, path, [pathsteps], [bezsteps], [bezN], [pathN]); -// Description: -// Takes a closed 2D bezier path, centered on the XY plane, and -// extrudes it perpendicularly along a 3D bezier path, forming a solid. -// Arguments: -// bezier = Array of 2D points of a bezier path, to be extruded. -// path = Array of 3D points of a bezier path, to extrude along. -// pathsteps = number of steps to divide each path segment into. -// bezsteps = number of steps to divide each bezier segment into. -// bezN = number of points in each extruded bezier segment. default=3 (cubic) -// pathN = number of points in each path bezier segment. default=3 (cubic) -// Example(FlatSpin): -// bez = [ -// [-10, 0], [-15, -5], -// [ -5, -10], [ 0, -10], [ 5, -10], -// [ 10, -5], [ 15, 0], [10, 5], -// [ 5, 10], [ 0, 10], [-5, 10], -// [ 25, -15], [-10, 0] -// ]; -// path = [ [0, 0, 0], [33, 33, 33], [90, 33, -33], [100, 0, 0] ]; -// bezier_sweep_bezier(bez, path, pathsteps=32, bezsteps=16); -module bezier_sweep_bezier(bezier, path, pathsteps=16, bezsteps=16, bezN=3, pathN=3) { - assert(is_path(bezier,2), "Argument bezier must be a 2D bezier path."); - assert(is_path(path)); - assert(is_int(pathsteps)); - assert(is_int(bezsteps)); - assert(is_int(bezN)); - assert(is_int(pathN)); - assert(len(bezier)%bezN == 1, str("For argument bezier, a degree ",bezN," bezier path shound have a multiple of ",bezN," points in it, plus 1.")); - assert(len(path)%pathN == 1, str("For argument bezier, a degree ",pathN," bezier path shound have a multiple of ",pathN," points in it, plus 1.")); - bez_points = simplify_path(bezier_path(bezier, bezsteps, bezN)); - path_points = simplify_path(path3d(bezier_path(path, pathsteps, pathN))); - path_sweep(bez_points, path_points); -} - // Module: trace_bezier() +// Usage: +// trace_bezier(bez, , ) { +// Topics: Bezier Paths, Debugging +// See Also: bezier_path() // Description: // Renders 2D or 3D bezier paths and their associated control points. // Useful for debugging bezier paths. // Arguments: // bez = the array of points in the bezier. -// N = Mark the first and every Nth vertex after in a different color and shape. // size = diameter of the lines drawn. +// --- +// N = Mark the first and every Nth vertex after in a different color and shape. // Example(2D): // bez = [ // [-10, 0], [-15, -5], @@ -898,7 +968,7 @@ module bezier_sweep_bezier(bezier, path, pathsteps=16, bezsteps=16, bezN=3, path // [ 5, 10], [ 0, 10] // ]; // trace_bezier(bez, N=3, size=0.5); -module trace_bezier(bez, N=3, size=1) { +module trace_bezier(bez, size=1, N=3) { assert(is_path(bez)); assert(is_int(N)); assert(len(bez)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); @@ -913,13 +983,16 @@ module trace_bezier(bez, N=3, size=1) { // Function: bezier_patch_points() // Usage: -// bezier_patch_points(patch, u, v) +// pt = bezier_patch_points(patch, u, v); +// ptgrid = bezier_patch_points(patch, LIST, LIST); +// ptgrid = bezier_patch_points(patch, RANGE, RANGE); +// Topics: Bezier Patches +// See Also: bezier_points(), bezier_curve(), bezier_path(), bezier_triangle_point() // Description: -// Given a square 2-dimensional array of (N+1) by (N+1) points size, -// that represents a Bezier Patch of degree N, returns a point on that -// surface, at positions `u`, and `v`. A cubic bezier patch will be 4x4 -// points in size. If given a non-square array, each direction will have -// its own degree. +// Given a square 2-dimensional array of (N+1) by (N+1) points size, that represents a Bezier Patch +// of degree N, returns a point on that surface, at positions `u`, and `v`. A cubic bezier patch +// will be 4x4 points in size. If given a non-square array, each direction will have its own +// degree. // Arguments: // patch = The 2D array of endpoints and control points for this bezier patch. // u = The proportion of the way along the horizontal inner list of the patch to find the point of. 0<=`u`<=1. If given as a list or range of values, returns a list of point lists. @@ -956,7 +1029,9 @@ function bezier_patch_points(patch, u, v) = // Function: bezier_triangle_point() // Usage: -// bezier_triangle_point(tri, u, v) +// pt = bezier_triangle_point(tri, u, v); +// Topics: Bezier Patches +// See Also: bezier_points(), bezier_curve(), bezier_path(), bezier_patch_points() // Description: // Given a triangular 2-dimensional array of N+1 by (for the first row) N+1 points, // that represents a Bezier triangular patch of degree N, returns a point on @@ -988,26 +1063,49 @@ function bezier_triangle_point(tri, u, v) = // Function: is_tripatch() +// Usage: +// bool = is_tripatch(x); +// Topics: Bezier Patches, Type Checking +// See Also: is_rectpatch(), is_patch() // Description: // Returns true if the given item is a triangular bezier patch. -function is_tripatch(x) = is_list(x) && is_list(x[0]) && is_vector(x[0][0]) && len(x[0])>1 && len(x[len(x)-1])==1; +// Arguments: +// x = The value to check the type of. +function is_tripatch(x) = + is_list(x) && is_list(x[0]) && is_vector(x[0][0]) && len(x[0])>1 && len(x[len(x)-1])==1; // Function: is_rectpatch() +// Usage: +// bool = is_rectpatch(x); +// Topics: Bezier Patches, Type Checking +// See Also: is_tripatch(), is_patch() // Description: // Returns true if the given item is a rectangular bezier patch. -function is_rectpatch(x) = is_list(x) && is_list(x[0]) && is_vector(x[0][0]) && len(x[0]) == len(x[len(x)-1]); +// Arguments: +// x = The value to check the type of. +function is_rectpatch(x) = + is_list(x) && is_list(x[0]) && is_vector(x[0][0]) && len(x[0]) == len(x[len(x)-1]); // Function: is_patch() +// Usage: +// bool = is_patch(x); +// Topics: Bezier Patches, Type Checking +// See Also: is_tripatch(), is_rectpatch() // Description: // Returns true if the given item is a bezier patch. -function is_patch(x) = is_tripatch(x) || is_rectpatch(x); +// Arguments: +// x = The value to check the type of. +function is_patch(x) = + is_tripatch(x) || is_rectpatch(x); // Function: bezier_patch() // Usage: -// bezier_patch(patch, [splinesteps], [vnf], [style]); +// vnf = bezier_patch(patch, , , ); +// Topics: Bezier Patches +// See Also: bezier_points(), bezier_curve(), bezier_path(), bezier_patch_points(), bezier_triangle_point() // Description: // Calculate vertices and faces for forming a partial polyhedron from the given bezier rectangular // or triangular patch. Returns a [VNF structure](vnf.scad): a list containing two elements. The first is the @@ -1017,6 +1115,7 @@ function is_patch(x) = is_tripatch(x) || is_rectpatch(x); // Arguments: // patch = The rectangular or triangular array of endpoints and control points for this bezier patch. // splinesteps = Number of steps to divide each bezier segment into. For rectangular patches you can specify [XSTEPS,YSTEPS]. Default: 16 +// --- // vnf = Vertices'n'Faces [VNF structure](vnf.scad) to add new vertices and faces to. Default: empty VNF // style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". // Example(3D): @@ -1038,7 +1137,7 @@ function is_patch(x) = is_tripatch(x) || is_rectpatch(x); // ]; // vnf = bezier_patch(tri, splinesteps=16); // vnf_polyhedron(vnf); -// Example(3DFlatSpin): Chaining Patches +// Example(3D,FlatSpin,VPD=444): Chaining Patches // patch = [ // // u=0,v=0 u=1,v=0 // [[0, 0,0], [33, 0, 0], [67, 0, 0], [100, 0,0]], @@ -1155,11 +1254,14 @@ function _bezier_triangle(tri, splinesteps=16, vnf=EMPTY_VNF) = // Function: bezier_patch_flat() // Usage: -// bezier_patch_flat(size, [N], [spin], [orient], [trans]); +// patch = bezier_patch_flat(size, , , , ); +// Topics: Bezier Patches +// See Also: bezier_patch_points() // Description: // Returns a flat rectangular bezier patch of degree `N`, centered on the XY plane. // Arguments: // size = 2D XY size of the patch. +// --- // N = Degree of the patch to generate. Since this is flat, a degree of 1 should usually be sufficient. // orient = The orientation to rotate the edge patch into. Given as an [X,Y,Z] rotation angle list. // trans = Amount to translate patch, after rotating to `orient`. @@ -1181,17 +1283,25 @@ function bezier_patch_flat(size=[100,100], N=4, spin=0, orient=UP, trans=[0,0,0] // Function: patch_reverse() // Usage: -// patch_reverse(patch) +// rpatch = patch_reverse(patch); +// Topics: Bezier Patches +// See Also: bezier_patch_points(), bezier_patch_flat() // Description: // Reverses the patch, so that the faces generated from it are flipped back to front. // Arguments: // patch = The patch to reverse. -function patch_reverse(patch) = [for (row=patch) reverse(row)]; +function patch_reverse(patch) = + [for (row=patch) reverse(row)]; + + +// Section: Bezier Surface Modules // Function: bezier_surface() // Usage: -// bezier_surface(patches, [splinesteps], [vnf], [style]); +// vnf = bezier_surface(patches, , , ); +// Topics: Bezier Patches +// See Also: bezier_patch_points(), bezier_patch_flat() // Description: // Calculate vertices and faces for forming a (possibly partial) polyhedron from the given // rectangular and/or triangular bezier patches. Returns a [VNF structure](vnf.scad): a list @@ -1202,6 +1312,7 @@ function patch_reverse(patch) = [for (row=patch) reverse(row)]; // Arguments: // patches = A list of triangular and/or rectangular bezier patches. // splinesteps = Number of steps to divide each bezier segment into. Default: 16 +// --- // vnf = Vertices'n'Faces [VNF structure](vnf.scad) to add new vertices and faces to. Default: empty VNF // style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". // Example(3D): @@ -1228,47 +1339,12 @@ function bezier_surface(patches=[], splinesteps=16, vnf=EMPTY_VNF, style="defaul -// Section: Bezier Surface Modules - - -// Module: bezier_polyhedron() -// Usage: -// bezier_polyhedron(patches, [splinesteps], [vnf], [style], [convexity]) -// Description: -// Takes a list of two or more bezier patches and attempts to make a complete polyhedron from them. -// Arguments: -// patches = A list of triangular and/or rectangular bezier patches. -// splinesteps = Number of steps to divide each bezier segment into. Default: 16 -// vnf = Vertices'n'Faces [VNF structure](vnf.scad) to add extra vertices and faces to. Default: empty VNF -// style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". -// convexity = Max number of times a line could intersect a wall of the shape. -// Example: -// patch1 = [ -// [[18,18,0], [33, 0, 0], [ 67, 0, 0], [ 82, 18,0]], -// [[ 0,40,0], [ 0, 0, 20], [100, 0, 20], [100, 40,0]], -// [[ 0,60,0], [ 0,100, 20], [100,100,100], [100, 60,0]], -// [[18,82,0], [33,100, 0], [ 67,100, 0], [ 82, 82,0]], -// ]; -// patch2 = [ -// [[18,82,0], [33,100, 0], [ 67,100, 0], [ 82, 82,0]], -// [[ 0,60,0], [ 0,100,-50], [100,100,-50], [100, 60,0]], -// [[ 0,40,0], [ 0, 0,-50], [100, 0,-50], [100, 40,0]], -// [[18,18,0], [33, 0, 0], [ 67, 0, 0], [ 82, 18,0]], -// ]; -// bezier_polyhedron([patch1, patch2], splinesteps=8); -module bezier_polyhedron(patches=[], splinesteps=16, vnf=EMPTY_VNF, style="default", convexity=10) -{ - vnf_polyhedron( - bezier_surface(patches=patches, splinesteps=splinesteps, vnf=vnf, style=style), - convexity=convexity - ); -} - - // Module: trace_bezier_patches() // Usage: // trace_bezier_patches(patches, [size], [splinesteps], [showcps], [showdots], [showpatch], [convexity], [style]); +// Topics: Bezier Patches, Debugging +// See Also: bezier_patch_points(), bezier_patch_flat(), bezier_surface() // Description: // Shows the surface, and optionally, control points of a list of bezier patches. // Arguments: diff --git a/bottlecaps.scad b/bottlecaps.scad index 01cc0983..30916f91 100644 --- a/bottlecaps.scad +++ b/bottlecaps.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: bottlecaps.scad // Bottle caps and necks for PCO18XX standard plastic beverage bottles. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -18,11 +16,12 @@ include // Module: pco1810_neck() // Usage: -// pco1810_neck() +// pco1810_neck() // Description: // Creates an approximation of a standard PCO-1810 threaded beverage bottle neck. // Arguments: // wall = Wall thickness in mm. +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -31,6 +30,12 @@ include // "support-ring" = Centered at the bottom of the support ring. // Example: // pco1810_neck(); +// Example: Standard Anchors +// pco1810_neck() show_anchors(custom=false); +// Example: Custom Named Anchors +// expose_anchors(0.3) +// pco1810_neck() +// show_anchors(std=false); module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) { inner_d = 21.74; @@ -64,7 +69,7 @@ module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) anchorpt("support-ring", [0,0,neck_h-h/2]), anchorpt("tamper-ring", [0,0,h/2-tamper_base_h]) ]; - attachable(anchor,spin,orient, d=support_d, l=h, anchors=anchors) { + attachable(anchor,spin,orient, d1=neck_d, d2=lip_recess_d+2*lip_leadin_r, l=h, anchors=anchors) { down(h/2) { rotate_extrude(convexity=10) { polygon(turtle( @@ -105,12 +110,12 @@ module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) bottom_half() { difference() { thread_helix( - base_d=threadbase_d-0.1, + d=threadbase_d-0.1, pitch=thread_pitch, thread_depth=thread_h+0.1, thread_angle=thread_angle, twist=810, - higbee=75, + higbee=thread_h*2, anchor=TOP ); zrot_copies(rots=[90,270]) { @@ -126,15 +131,19 @@ module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) } } +function pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) = + no_function("pco1810_neck"); + // Module: pco1810_cap() // Usage: -// pco1810_cap(wall, [texture]); +// pco1810_cap(, ); // Description: // Creates a basic cap for a PCO1810 threaded beverage bottle. // Arguments: // wall = Wall thickness in mm. // texture = The surface texture of the cap. Valid values are "none", "knurled", or "ribbed". Default: "none" +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -144,6 +153,12 @@ module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) // pco1810_cap(); // pco1810_cap(texture="knurled"); // pco1810_cap(texture="ribbed"); +// Example: Standard Anchors +// pco1810_cap(texture="ribbed") show_anchors(custom=false); +// Example: Custom Named Anchors +// expose_anchors(0.3) +// pco1810_cap(texture="ribbed") +// show_anchors(std=false); module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) { cap_id = 28.58; @@ -177,23 +192,28 @@ module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) } up(wall) cyl(d=cap_id, h=tamper_ring_h+wall, anchor=BOTTOM); } - up(wall+2) thread_helix(base_d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, thread_angle=thread_angle, twist=810, higbee=45, internal=true, anchor=BOTTOM); + up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, thread_angle=thread_angle, twist=810, higbee=thread_depth, internal=true, anchor=BOTTOM); } children(); } } +function pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) = + no_function("pco1810_cap"); + + // Section: PCO-1881 Bottle Threading // Module: pco1881_neck() // Usage: -// pco1881_neck() +// pco1881_neck() // Description: // Creates an approximation of a standard PCO-1881 threaded beverage bottle neck. // Arguments: // wall = Wall thickness in mm. +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -202,6 +222,12 @@ module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) // "support-ring" = Centered at the bottom of the support ring. // Example: // pco1881_neck(); +// Example: +// pco1881_neck() show_anchors(custom=false); +// Example: +// expose_anchors(0.3) +// pco1881_neck() +// show_anchors(std=false); module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) { inner_d = 21.74; @@ -236,7 +262,7 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) anchorpt("support-ring", [0,0,neck_h-h/2]), anchorpt("tamper-ring", [0,0,h/2-tamper_base_h]) ]; - attachable(anchor,spin,orient, d=support_d, l=h, anchors=anchors) { + attachable(anchor,spin,orient, d1=neck_d, d2=lip_recess_d+2*lip_leadin_r, l=h, anchors=anchors) { down(h/2) { rotate_extrude(convexity=10) { polygon(turtle( @@ -277,12 +303,12 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) up(h-lip_h) { difference() { thread_helix( - base_d=threadbase_d-0.1, + d=threadbase_d-0.1, pitch=thread_pitch, thread_depth=thread_h+0.1, thread_angle=thread_angle, twist=650, - higbee=75, + higbee=thread_h*2, anchor=TOP ); zrot_copies(rots=[90,270]) { @@ -297,6 +323,9 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) } } +function pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) = + no_function("pco1881_neck"); + // Module: pco1881_cap() // Usage: @@ -315,6 +344,12 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) // pco1881_cap(); // pco1881_cap(texture="knurled"); // pco1881_cap(texture="ribbed"); +// Example: Standard Anchors +// pco1881_cap(texture="ribbed") show_anchors(custom=false); +// Example: Custom Named Anchors +// expose_anchors(0.5) +// pco1881_cap(texture="ribbed") +// show_anchors(std=false); module pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) { $fn = segs(33/2); @@ -341,12 +376,15 @@ module pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) } up(wall) cyl(d=28.58, h=11.2+wall, anchor=BOTTOM); } - up(wall+2) thread_helix(base_d=25.5, pitch=2.7, thread_depth=1.6, thread_angle=15, twist=650, higbee=45, internal=true, anchor=BOTTOM); + up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, thread_angle=15, twist=650, higbee=1.6, internal=true, anchor=BOTTOM); } children(); } } +function pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) = + no_function("pco1881_cap"); + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/changes/REMOVED.txt b/changes/REMOVED.txt deleted file mode 100644 index 41071fe7..00000000 --- a/changes/REMOVED.txt +++ /dev/null @@ -1,26 +0,0 @@ -Cpi PI -hypot3 norm([x,y,z]) -distance norm(p2-p1) -cdr slice(list, 1, -1) -wrap_range select() -vector2d_angle vector_angle() -vector3d_angle vector_angle() -rotate_points3d_around_axis rotate_points3d(pts, v=ang, axis=u, cp=cp) -cube2pt cuboid(p1, p2) -span_cube cuboid(p1, p2) -offsetcube cuboid(..., align) -chamfcube cuboid(..., chamfer, edges, trimcorners) -rrect cuboid(..., fillet, edges) -rcube cuboid(..., fillet) -trapezoid prismoid() -pyramid cyl(..., r2=0, $fn=N) -prism cyl(..., $fn=N) -chamferred_cylinder cyl(..., chamfer) -chamf_cyl cyl(..., chamfer) -filleted_cylinder cyl(..., fillet) -rcylinder cyl(..., fillet) -thinning_brace thinning_triangle(..., diagonly=true) -translate_copies place_copies() -line_of spread(p1, p2) -grid_of grid3d() - diff --git a/common.scad b/common.scad index d1d4021e..ab243801 100644 --- a/common.scad +++ b/common.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: common.scad // Common functions used in argument processing. -// To use, include this line at the top of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// @@ -14,9 +12,20 @@ // Function: typeof() // Usage: // typ = typeof(x); +// Topics: Type Checking +// See Also: is_type() // Description: -// Returns a string representing the type of the value. One of "undef", "boolean", "number", "nan", "string", "list", "range" or "invalid". +// Returns a string representing the type of the value. One of "undef", "boolean", "number", "nan", "string", "list", "range", "function" or "invalid". // Some malformed "ranges", like '[0:NAN:INF]' and '[0:"a":INF]', may be classified as "undef" or "invalid". +// Examples: +// typ = typeof(undef); // Returns: "undef" +// typ = typeof(true); // Returns: "boolean" +// typ = typeof(42); // Returns: "number" +// typ = typeof(NAN); // Returns: "nan" +// typ = typeof("foo"); // Returns: "string" +// typ = typeof([3,4,5]); // Returns: "list" +// typ = typeof([3:1:8]); // Returns: "range" +// typ = typeof(function (x,y) x+y); // Returns: "function" function typeof(x) = is_undef(x)? "undef" : is_bool(x)? "boolean" : @@ -25,16 +34,19 @@ function typeof(x) = is_string(x)? "string" : is_list(x)? "list" : is_range(x) ? "range" : + version_num()>20210000 && is_function(x) ? "function" : "invalid"; // Function: is_type() // Usage: -// b = is_type(x, types); +// bool = is_type(x, types); +// Topics: Type Checking +// See Also: typeof() // Description: // Returns true if the type of the value `x` is one of those given as strings in the list `types`. -// Valid types are "undef", "boolean", "number", "nan", "string", "list", or "range" +// Valid types are "undef", "boolean", "number", "nan", "string", "list", "range", or "function". // Arguments: // x = The value to check the type of. // types = A list of types to check @@ -54,54 +66,118 @@ function is_type(x,types) = // Function: is_def() // Usage: -// is_def(x) +// bool = is_def(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str() // Description: // Returns true if `x` is not `undef`. False if `x==undef`. +// Example: +// bool = is_def(undef); // Returns: false +// bool = is_def(false); // Returns: true +// bool = is_def(42); // Returns: true +// bool = is_def("foo"); // Returns: true function is_def(x) = !is_undef(x); // Function: is_str() // Usage: -// is_str(x) +// bool = is_str(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_int(), is_def() // Description: // Returns true if `x` is a string. A shortcut for `is_string()`. +// Example: +// bool = is_str(undef); // Returns: false +// bool = is_str(false); // Returns: false +// bool = is_str(42); // Returns: false +// bool = is_str("foo"); // Returns: true function is_str(x) = is_string(x); // Function: is_int() +// Alias: is_integer() // Usage: -// is_int(n) +// bool = is_int(n); +// bool = is_integer(n); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def() // Description: // Returns true if the given value is an integer (it is a number and it rounds to itself). +// Example: +// bool = is_int(undef); // Returns: false +// bool = is_int(false); // Returns: false +// bool = is_int(42); // Returns: true +// bool = is_int("foo"); // Returns: false function is_int(n) = is_finite(n) && n == round(n); function is_integer(n) = is_finite(n) && n == round(n); // Function: is_nan() // Usage: -// is_nan(x); +// bool = is_nan(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def(), is_int() // Description: // Returns true if a given value `x` is nan, a floating point value representing "not a number". +// Example: +// bool = is_nan(undef); // Returns: false +// bool = is_nan(false); // Returns: false +// bool = is_nan(42); // Returns: false +// bool = is_nan("foo"); // Returns: false +// bool = is_nan(NAN); // Returns: true function is_nan(x) = (x!=x); // Function: is_finite() // Usage: -// is_finite(x); +// bool = is_finite(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def(), is_int(), is_nan() // Description: // Returns true if a given value `x` is a finite number. +// Example: +// bool = is_finite(undef); // Returns: false +// bool = is_finite(false); // Returns: false +// bool = is_finite(42); // Returns: true +// bool = is_finite("foo"); // Returns: false +// bool = is_finite(NAN); // Returns: false +// bool = is_finite(INF); // Returns: false +// bool = is_finite(-INF); // Returns: false function is_finite(x) = is_num(x) && !is_nan(0*x); // Function: is_range() +// Usage: +// bool = is_range(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def(), is_int() // Description: // Returns true if its argument is a range +// Example: +// bool = is_range(undef); // Returns: false +// bool = is_range(false); // Returns: false +// bool = is_range(42); // Returns: false +// bool = is_range([3,4,5]); // Returns: false +// bool = is_range("foo"); // Returns: false +// bool = is_range([3:5]); // Returns: true function is_range(x) = !is_list(x) && is_finite(x[0]) && is_finite(x[1]) && is_finite(x[2]) ; // Function: valid_range() +// Usage: +// bool = valid_range(x); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def(), is_int(), is_range() // Description: // Returns true if its argument is a valid range (deprecated ranges excluded). +// Example: +// bool = is_range(undef); // Returns: false +// bool = is_range(false); // Returns: false +// bool = is_range(42); // Returns: false +// bool = is_range([3,4,5]); // Returns: false +// bool = is_range("foo"); // Returns: false +// bool = is_range([3:5]); // Returns: true +// bool = is_range([3:1]); // Returns: false function valid_range(x) = is_range(x) && ( x[1]>0 @@ -109,45 +185,40 @@ function valid_range(x) = : ( x[1]<0 && x[0]>=x[2] ) ); -// Function: is_list_of() -// Usage: -// is_list_of(list, pattern) -// Description: -// Tests whether the input is a list whose entries are all numeric lists that have the same -// list shape as the pattern. -// Example: -// is_list_of([3,4,5], 0); // Returns true -// is_list_of([3,4,undef], 0); // Returns false -// is_list_of([[3,4],[4,5]], [1,1]); // Returns true -// is_list_of([[3,"a"],[4,true]], [1,undef]); // Returns true -// is_list_of([[3,4], 6, [4,5]], [1,1]); // Returns false -// is_list_of([[1,[3,4]], [4,[5,6]]], [1,[2,3]]); // Returns true -// is_list_of([[1,[3,INF]], [4,[5,6]]], [1,[2,3]]); // Returns false -// is_list_of([], [1,[2,3]]); // Returns true -function is_list_of(list,pattern) = - let(pattern = 0*pattern) - is_list(list) && - []==[for(entry=0*list) if (entry != pattern) entry]; - - // Function: is_consistent() // Usage: -// is_consistent(list) +// bool = is_consistent(list, ); +// Topics: Type Checking +// See Also: typeof(), is_type(), is_str(), is_def(), is_int(), is_range(), is_homogeneous() // Description: // Tests whether input is a list of entries which all have the same list structure -// and are filled with finite numerical data. It returns `true`for the empty list. +// and are filled with finite numerical data. You can optionally specify a required +// list structure with the pattern argument. It returns `true` for the empty list. +// Arguments: +// list = list to check +// pattern = optional pattern required to match // Example: // is_consistent([3,4,5]); // Returns true // is_consistent([[3,4],[4,5],[6,7]]); // Returns true // is_consistent([[3,4,5],[3,4]]); // Returns false // is_consistent([[3,[3,4,[5]]], [5,[2,9,[9]]]]); // Returns true // is_consistent([[3,[3,4,[5]]], [5,[2,9,9]]]); // Returns false -function is_consistent(list) = - /*is_list(list) &&*/ is_list_of(list, _list_pattern(list[0])); - +// is_consistent([3,4,5], 0); // Returns true +// is_consistent([3,4,undef], 0); // Returns false +// is_consistent([[3,4],[4,5]], [1,1]); // Returns true +// is_consistent([[3,"a"],[4,true]], [1,undef]); // Returns true +// is_consistent([[3,4], 6, [4,5]], [1,1]); // Returns false +// is_consistent([[1,[3,4]], [4,[5,6]]], [1,[2,3]]); // Returns true +// is_consistent([[1,[3,INF]], [4,[5,6]]], [1,[2,3]]); // Returns false +// is_consistent([], [1,[2,3]]); // Returns true +function is_consistent(list, pattern) = + is_list(list) + && (len(list)==0 + || (let(pattern = is_undef(pattern) ? _list_pattern(list[0]): _list_pattern(pattern) ) + []==[for(entry=0*list) if (entry != pattern) entry])); //Internal function -//Creates a list with the same structure of `list` with each of its elements substituted by 0. +//Creates a list with the same structure of `list` with each of its elements replaced by 0. function _list_pattern(list) = is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] @@ -156,22 +227,41 @@ function _list_pattern(list) = // Function: same_shape() // Usage: -// same_shape(a,b) +// bool = same_shape(a,b); +// Topics: Type Checking +// See Also: is_homogeneous(), is_consistent() // Description: // Tests whether the inputs `a` and `b` are both numeric and are the same shaped list. // Example: // same_shape([3,[4,5]],[7,[3,4]]); // Returns true // same_shape([3,4,5], [7,[3,4]]); // Returns false -function same_shape(a,b) = _list_pattern(a) == b*0; +function same_shape(a,b) = is_def(b) && _list_pattern(a) == b*0; + + +// Function: is_bool_list() +// Usage: +// check = is_bool_list(list,) +// Topics: Type Checking +// See Also: is_homogeneous(), is_consistent() +// Description: +// Tests whether input is a list containing only booleans, and optionally checks its length. +// Arguments: +// list = list to test +// length = if given, list must be this length +function is_bool_list(list, length) = + is_list(list) && (is_undef(length) || len(list)==length) && []==[for(entry=list) if (!is_bool(entry)) 1]; // Section: Handling `undef`s. // Function: default() +// Usage: +// val = default(val, dflt); +// Topics: Undef Handling +// See Also: first_defined(), one_defined(), num_defined() // Description: -// Returns the value given as `v` if it is not `undef`. -// Otherwise, returns the value of `dflt`. +// Returns the value given as `v` if it is not `undef`. Otherwise, returns the value of `dflt`. // Arguments: // v = Value to pass through if not `undef`. // dflt = Value to return if `v` *is* `undef`. @@ -179,6 +269,10 @@ function default(v,dflt=undef) = is_undef(v)? dflt : v; // Function: first_defined() +// Usage: +// val = first_defined(v, ); +// Topics: Undef Handling +// See Also: default(), one_defined(), num_defined(), any_defined(), all_defined() // Description: // Returns the first item in the list that is not `undef`. // If all items are `undef`, or list is empty, returns `undef`. @@ -197,42 +291,92 @@ function first_defined(v,recursive=false,_i=0) = // Function: one_defined() // Usage: -// one_defined(vars, names, ) +// val = one_defined(vals, names, ) +// Topics: Undef Handling +// See Also: default(), first_defined(), num_defined(), any_defined(), all_defined() // Description: -// Examines the input list `vars` and returns the entry which is not `undef`. If more -// than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed -// by the defined items from the `names` parameter. If `required` is set to false then it is OK if all of the -// entries of `vars` are undefined, and in this case, `undef` is returned. -// Example: +// Examines the input list `vals` and returns the entry which is not `undef`. +// If more than one entry is not `undef` then an error is asserted, specifying +// "Must define exactly one of" followed by the names in the `names` parameter. +// If `dflt` is given, and all `vals` are `undef`, then the value in `dflt` is returned. +// If `dflt` is *not* given, and all `vals` are `undef`, then an error is asserted. +// Arguments: +// vals = The values to return the first one which is not `undef`. +// names = A string with comma-separated names for the arguments whose values are passed in `vals`. +// dflt = If given, the value returned if all `vals` are `undef`. +// Examples: // length = one_defined([length,L,l], ["length","L","l"]); -function one_defined(vars, names, required=true) = - assert(len(vars)==len(names)) - let ( - ok = num_defined(vars)==1 || (!required && num_defined(vars)==0) - ) - assert(ok,str("Must define ",required?"exactly":"at most"," one of ",num_defined(vars)==0?names:[for(i=[0:len(vars)]) if (is_def(vars[i])) names[i]])) - first_defined(vars); +// length = one_defined([length,L,l], "length,L,l", dflt=1); +function one_defined(vals, names, dflt=_UNDEF) = + let( + checkargs = is_list(names)? assert(len(vals) == len(names)) : + is_string(names)? let( + name_cnt = len([for (c=names) if (c==",") 1]) + 1 + ) assert(len(vals) == name_cnt) : + assert(is_list(names) || is_string(names)) 0, + ok = num_defined(vals)==1 || (dflt!=_UNDEF && num_defined(vals)==0) + ) ok? default(first_defined(vals), dflt) : + let( + names = is_string(names) ? str_split(names,",") : names, + defd = [for (i=idx(vals)) if (is_def(vals[i])) names[i]], + msg = str( + "Must define ", + dflt==_UNDEF? "exactly" : "at most", + " one of ", + num_defined(vals) == 0 ? names : defd + ) + ) assert(ok,msg); // Function: num_defined() -// Description: Counts how many items in list `v` are not `undef`. -function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); +// Usage: +// cnt = num_defined(v); +// Topics: Undef Handling +// See Also: default(), first_defined(), one_defined(), any_defined(), all_defined() +// Description: +// Counts how many items in list `v` are not `undef`. +// Example: +// cnt = num_defined([3,7,undef,2,undef,undef,1]); // Returns: 4 +function num_defined(v) = + len([for(vi=v) if(!is_undef(vi)) 1]); + // Function: any_defined() +// Usage: +// bool = any_defined(v, ); +// Topics: Undef Handling +// See Also: default(), first_defined(), one_defined(), num_defined(), all_defined() // Description: // Returns true if any item in the given array is not `undef`. // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) != undef; +// Example: +// bool = any_defined([undef,undef,undef]); // Returns: false +// bool = any_defined([undef,42,undef]); // Returns: true +// bool = any_defined([34,42,87]); // Returns: true +// bool = any_defined([undef,undef,[undef]]); // Returns: true +// bool = any_defined([undef,undef,[undef]],recursive=true); // Returns: false +// bool = any_defined([undef,undef,[42]],recursive=true); // Returns: true +function any_defined(v,recursive=false) = + first_defined(v,recursive=recursive) != undef; // Function: all_defined() +// Usage: +// bool = all_defined(v, ); // Description: // Returns true if all items in the given array are not `undef`. // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. +// Example: +// bool = all_defined([undef,undef,undef]); // Returns: false +// bool = all_defined([undef,42,undef]); // Returns: false +// bool = all_defined([34,42,87]); // Returns: true +// bool = all_defined([23,34,[undef]]); // Returns: true +// bool = all_defined([23,34,[undef]],recursive=true); // Returns: false +// bool = all_defined([23,34,[42]],recursive=true); // Returns: true function all_defined(v,recursive=false) = []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; @@ -243,7 +387,9 @@ function all_defined(v,recursive=false) = // Function: get_anchor() // Usage: -// get_anchor(anchor,center,,); +// anchr = get_anchor(anchor,center,,); +// Topics: Argument Handling +// See Also: get_radius(), get_named_args() // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -256,6 +402,13 @@ function all_defined(v,recursive=false) = // center = If not `undef`, this overrides the value of `anchor`. // uncentered = The value to return if `center` is not `undef` and evaluates as false. Default: ALLNEG // dflt = The default value to return if both `anchor` and `center` are `undef`. Default: `CENTER` +// Examples: +// anchr = get_anchor(undef, undef, BOTTOM, TOP); // Returns: [0, 0, 1] (TOP) +// anchr = get_anchor(RIGHT, undef, BOTTOM, TOP); // Returns: [1, 0, 0] (RIGHT) +// anchr = get_anchor(undef, false, BOTTOM, TOP); // Returns: [0, 0,-1] (BOTTOM) +// anchr = get_anchor(RIGHT, false, BOTTOM, TOP); // Returns: [0, 0,-1] (BOTTOM) +// anchr = get_anchor(undef, true, BOTTOM, TOP); // Returns: [0, 0, 0] (CENTER) +// anchr = get_anchor(RIGHT, true, BOTTOM, TOP); // Returns: [0, 0, 0] (CENTER) function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = !is_undef(center)? (center? CENTER : uncentered) : !is_undef(anchor)? anchor : @@ -264,24 +417,37 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius(, , , , , , ); +// r = get_radius(, , , , , , ); +// Topics: Argument Handling +// See Also: get_anchor(), get_named_args() // Description: -// Given various radii and diameters, returns the most specific radius. -// If a diameter is most specific, returns half its value, giving the radius. -// If no radii or diameters are defined, returns the value of dflt. -// Value specificity order is r1, r2, d1, d2, r, d, then dflt -// Only one of `r1`, `r2`, `d1`, or `d2` can be defined at once, or else it -// errors out, complaining about conflicting radius/diameter values. -// Only one of `r` or `d` can be defined at once, or else it errors out, -// complaining about conflicting radius/diameter values. +// Given various radii and diameters, returns the most specific radius. If a diameter is most +// specific, returns half its value, giving the radius. If no radii or diameters are defined, +// returns the value of `dflt`. Value specificity order is `r1`, `r2`, `d1`, `d2`, `r`, `d`, +// then `dflt`. Only one of `r1`, `r2`, `d1`, or `d2` can be defined at once, or else it errors +// out, complaining about conflicting radius/diameter values. Only one of `r` or `d` can be +// defined at once, or else it errors out, complaining about conflicting radius/diameter values. // Arguments: +// --- // r1 = Most specific radius. -// d1 = Most specific diameter. // r2 = Second most specific radius. -// d2 = Second most specific diameter. // r = Most general radius. +// d1 = Most specific diameter. +// d2 = Second most specific diameter. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. +// Examples: +// r = get_radius(r1=undef, r=undef, dflt=undef); // Returns: undef +// r = get_radius(r1=undef, r=undef, dflt=1); // Returns: 1 +// r = get_radius(r1=undef, r=6, dflt=1); // Returns: 6 +// r = get_radius(r1=7, r=6, dflt=1); // Returns: 7 +// r = get_radius(r1=undef, r2=8, r=6, dflt=1); // Returns: 8 +// r = get_radius(r1=undef, r2=8, d=6, dflt=1); // Returns: 8 +// r = get_radius(r1=undef, d=6, dflt=1); // Returns: 3 +// r = get_radius(d1=7, d=6, dflt=1); // Returns: 3.5 +// r = get_radius(d1=7, d2=8, d=6, dflt=1); // Returns: 3.5 +// r = get_radius(d1=undef, d2=8, d=6, dflt=1); // Returns: 4 +// r = get_radius(r1=8, d=6, dflt=1); // Returns: 8 function get_radius(r1, r2, r, d1, d2, d, dflt) = assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 @@ -296,65 +462,43 @@ function get_radius(r1, r2, r, d1, d2, d, dflt) = : dflt; -// Function: get_height() -// Usage: -// get_height(,,,) -// Description: -// Given several different parameters for height check that height is not multiply defined -// and return a single value. If the three values `l`, `h`, and `height` are all undefined -// then return the value `dflt`, if given, or undef otherwise. -// Arguments: -// l = l. -// h = h. -// height = height. -// dflt = Value to return if other values are `undef`. -function get_height(h=undef,l=undef,height=undef,dflt=undef) = - assert(num_defined([h,l,height])<=1,"You must specify only one of `l`, `h`, and `height`") - first_defined([h,l,height,dflt]); - // Function: get_named_args() // Usage: -// function f(pos1=_undef, pos2=_undef,...,named1=_undef, named2=_undef, ...) = let(args = get_named_args([pos1, pos2, ...], [[named1, default1], [named2, default2], ...]), named1=args[0], named2=args[1], ...) +// function f(pos1=_UNDEF, pos2=_UNDEF,...,named1=_UNDEF, named2=_UNDEF, ...) = let(args = get_named_args([pos1, pos2, ...], [[named1, default1], [named2, default2], ...]), named1=args[0], named2=args[1], ...) +// Topics: Argument Handling +// See Also: get_anchor(), get_radius() // Description: -// Given the values of some positional and named arguments, -// returns a list of the values assigned to named parameters. -// in the following steps: -// - First, all named parameters which were explicitly assigned in the -// function call take their provided value. +// Given the values of some positional and named arguments, returns a list of the values assigned to +// named parameters. in the following steps: +// - First, all named parameters which were explicitly assigned in the function call take their +// provided value. // - Then, any positional arguments are assigned to remaining unassigned -// parameters; this is governed both by the `priority` entries -// (if there are `N` positional arguments, then the `N` parameters with -// lowest `priority` value will be assigned) and by the order of the -// positional arguments (matching that of the assigned named parameters). -// If no priority is given, then these two ordering coincide: -// parameters are assigned in order, starting from the first one. -// - Finally, any remaining named parameters can take default values. -// If no default values are given, then `undef` is used. +// parameters; this is governed both by the `priority` entries (if there are `N` positional +// arguments, then the `N` parameters with lowest `priority` value will be assigned) and by the +// order of the positional arguments (matching that of the assigned named parameters). If no +// priority is given, then these two ordering coincide: parameters are assigned in order, starting +// from the first one. +// - Finally, any remaining named parameters can take default values. If no default values are +// given, then `undef` is used. // . -// This allows an author to declare a function prototype with named or -// optional parameters, so that the user may then call this function -// using either positional or named parameters. In practice the author -// will declare the function as using *both* positional and named -// parameters, and let `get_named_args()` do the parsing from the whole -// set of arguments. -// See the example below. +// This allows an author to declare a function prototype with named or optional parameters, so that +// the user may then call this function using either positional or named parameters. In practice the +// author will declare the function as using *both* positional and named parameters, and let +// `get_named_args()` do the parsing from the whole set of arguments. See the example below. // . -// This supports the user explicitly passing `undef` as a function argument. -// To distinguish between an intentional `undef` and -// the absence of an argument, we use a custom `_undef` value -// as a guard marking the absence of any arguments -// (in practice, `_undef` is a random-generated string, -// which will never coincide with any useful user value). -// This forces the author to declare all the function parameters -// as having `_undef` as their default value. +// This supports the user explicitly passing `undef` as a function argument. To distinguish between +// an intentional `undef` and the absence of an argument, we use a custom `_UNDEF` value as a guard +// marking the absence of any arguments (in practice, `_UNDEF` is a random-generated string, which +// will never coincide with any useful user value). This forces the author to declare all the +// function parameters as having `_UNDEF` as their default value. // Arguments: -// positional = the list of values of positional arguments. -// named = the list of named arguments; each entry of the list has the form `[passed-value, , ]`, where `passed-value` is the value that was passed at function call; `default-value` is the value that will be used if nothing is read from either named or positional arguments; `priority` is the priority assigned to this argument (lower means more priority, default value is `+inf`). Since stable sorting is used, if no priority at all is given, all arguments will be read in order. -// _undef = the default value used by the calling function for all arguments. The default value, `_undef`, is a random string. This value **must** be the default value of all parameters in the outer function call (see example below). +// positional = The list of values of positional arguments. +// named = The list of named arguments; each entry of the list has the form `[passed-value, , ]`, where `passed-value` is the value that was passed at function call; `default-value` is the value that will be used if nothing is read from either named or positional arguments; `priority` is the priority assigned to this argument (lower means more priority, default value is `+inf`). Since stable sorting is used, if no priority at all is given, all arguments will be read in order. +// _undef = The default value used by the calling function for all arguments. The default value, `_UNDEF`, is a random string. This value **must** be the default value of all parameters in the outer function call (see example below). // // Example: a function with prototype `f(named1,< , named3 >)` -// function f(_p1=_undef, _p2=_undef, _p3=_undef, -// arg1=_undef, arg2=_undef, arg3=_undef) = +// function f(_p1=_UNDEF, _p2=_UNDEF, _p3=_UNDEF, +// arg1=_UNDEF, arg2=_UNDEF, arg3=_UNDEF) = // let(named = get_named_args([_p1, _p2, _p3], // [[arg1, "default1",0], [arg2, "default2",2], [arg3, "default3",1]])) // named; @@ -376,13 +520,13 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // a value that the user should never enter randomly; // result of `dd if=/dev/random bs=32 count=1 |base64` : -_undef="LRG+HX7dy89RyHvDlAKvb9Y04OTuaikpx205CTh8BSI"; +_UNDEF="LRG+HX7dy89RyHvDlAKvb9Y04OTuaikpx205CTh8BSI"; /* Note: however tempting it might be, it is *not* possible to accept * named argument as a list [named1, named2, ...] (without default * values), because the values [named1, named2...] themselves might be * lists, and we will not be able to distinguish the two cases. */ -function get_named_args(positional, named,_undef=_undef) = +function get_named_args(positional, named, _undef=_UNDEF) = let(deft = [for(p=named) p[1]], // default is undef // indices of the values to fetch from positional args: unknown = [for(x=enumerate(named)) if(x[1][0]==_undef) x[0]], @@ -402,9 +546,13 @@ function get_named_args(positional, named,_undef=_undef) = val != _undef ? val : ass != [] ? positional[ass[0]] : deft[idx] ]; + + // Function: scalar_vec3() // Usage: -// scalar_vec3(v, ); +// vec = scalar_vec3(v, ); +// Topics: Argument Handling +// See Also: get_anchor(), get_radius(), force_list() // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -413,7 +561,12 @@ function get_named_args(positional, named,_undef=_undef) = // Arguments: // v = Value to return vector from. // dflt = Default value to set empty vector parts from. -function scalar_vec3(v, dflt=undef) = +// Examples: +// vec = scalar_vec3(undef); // Returns: undef +// vec = scalar_vec3(10); // Returns: [10,10,10] +// vec = scalar_vec3(10,1); // Returns: [10,1,1] +// vec = scalar_vec3([10,10],1); // Returns: [10,10,1] +function scalar_vec3(v, dflt) = is_undef(v)? undef : is_list(v)? [for (i=[0:2]) default(v[i], default(dflt, 0))] : !is_undef(dflt)? [v,dflt,dflt] : [v,v,v]; @@ -422,18 +575,23 @@ function scalar_vec3(v, dflt=undef) = // Function: segs() // Usage: // sides = segs(r); +// Topics: Geometry // Description: // Calculate the standard number of sides OpenSCAD would give a circle based on `$fn`, `$fa`, and `$fs`. // Arguments: // r = Radius of circle to get the number of segments for. +// Examples: +// $fn=12; sides=segs(10); // Returns: 12 +// $fa=2; $fs=3, sides=segs(10); // Returns: 21 function segs(r) = $fn>0? ($fn>3? $fn : 3) : - let( r = is_finite(r)? r: 0 ) - ceil(max(5, min(360/$fa, abs(r)*2*PI/$fs))) ; - + let( r = is_finite(r)? r : 0 ) + ceil(max(5, min(360/$fa, abs(r)*2*PI/$fs))); // Module: no_children() +// Topics: Error Checking +// See Also: no_function(), no_module() // Usage: // no_children($children); // Description: @@ -441,12 +599,45 @@ function segs(r) = // as indicated by its argument. // Arguments: // $children = number of children the module has. +// Example: +// module foo() { +// no_children($children); +// } module no_children(count) { assert($children==0, "Module no_children() does not support child modules"); - assert(count==0, str("Module ",parent_module(1),"() does not support child modules")); + if ($parent_modules>0) { + assert(count==0, str("Module ",parent_module(1),"() does not support child modules")); + } } +// Function: no_function() +// Usage: +// dummy = no_function(name) +// Topics: Error Checking +// See Also: no_children(), no_module() +// Description: +// Asserts that the function, "name", only exists as a module. +// Example: +// x = no_function("foo"); +function no_function(name) = + assert(false,str("You called ",name,"() as a function, but it is available only as a module")); + + +// Module: no_module() +// Usage: +// no_module(); +// Topics: Error Checking +// See Also: no_children(), no_function() +// Description: +// Asserts that the called module exists only as a function. +// Example: +// function foo() = no_module(); +module no_module() { + assert(false, str("You called ",parent_module(1),"() as a module but it is available only as a function")); +} + + // Section: Testing Helpers @@ -458,6 +649,8 @@ function _valstr(x) = // Module: assert_approx() // Usage: // assert_approx(got, expected, ); +// Topics: Error Checking, Debugging +// See Also: no_children(), no_function(), no_module(), assert_equal() // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -466,6 +659,8 @@ function _valstr(x) = // got = The value actually received. // expected = The value that was expected. // info = Extra info to print out to make the error clearer. +// Example: +// assert_approx(1/3, 0.333333333333333, str("numer=",1,", demon=",3)); module assert_approx(got, expected, info) { no_children($children); if (!approx(got, expected)) { @@ -486,14 +681,17 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: // assert_equal(got, expected, ); +// Topics: Error Checking, Debugging +// See Also: no_children(), no_function(), no_module(), assert_approx() // Description: -// Tests if the value gotten is what was expected. If not, then -// the expected and received values are printed to the console and -// an assertion is thrown to stop execution. +// Tests if the value gotten is what was expected. If not, then the expected and received values +// are printed to the console and an assertion is thrown to stop execution. // Arguments: // got = The value actually received. // expected = The value that was expected. // info = Extra info to print out to make the error clearer. +// Example: +// assert_approx(3*9, 27, str("a=",3,", b=",9)); module assert_equal(got, expected, info) { no_children($children); if (got != expected || (is_nan(got) && is_nan(expected))) { @@ -514,11 +712,19 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: // shape_compare() {test_shape(); expected_shape();} +// Topics: Error Checking, Debugging, Testing +// See Also: assert_approx(), assert_equal() // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. -// Returns the differential geometry if they are not nearly the same shape and size. +// Returns the differential geometry if they are not quite the same shape and size. // Arguments: // eps = The surface of the two shapes must be within this size of each other. Default: 1/1024 +// Example: +// $fn=36; +// shape_compare() { +// sphere(d=100); +// rotate_extrude() right_half(planar=true) circle(d=100); +// } module shape_compare(eps=1/1024) { union() { difference() { @@ -528,7 +734,7 @@ module shape_compare(eps=1/1024) { } else { minkowski() { children(1); - cube(eps, center=true); + spheroid(r=eps, style="octa"); } } } @@ -539,7 +745,7 @@ module shape_compare(eps=1/1024) { } else { minkowski() { children(0); - cube(eps, center=true); + spheroid(r=eps, style="octa"); } } } @@ -562,6 +768,7 @@ module shape_compare(eps=1/1024) { // to run the loop one extra time to return the final value. This tends to make the loop code // look rather ugly. The `looping()`, `loop_while()` and `loop_done()` functions // can make this somewhat more legible. +// . // ```openscad // function flat_sum(l) = [ // for ( @@ -584,11 +791,13 @@ module shape_compare(eps=1/1024) { // Function: looping() // Usage: -// looping(state) +// bool = looping(state); +// Topics: Iteration +// See Also: loop_while(), loop_done() // Description: -// Returns true if the `state` value indicates the current loop should continue. -// This is useful when using C-style for loops to iteratively calculate a value. -// Used with `loop_while()` and `loop_done()`. See [Looping Helpers](#5-looping-helpers) for an example. +// Returns true if the `state` value indicates the current loop should continue. This is useful +// when using C-style for loops to iteratively calculate a value. Used with `loop_while()` and +// `loop_done()`. See [Looping Helpers](section-looping-helpers) for an example. // Arguments: // state = The loop state value. function looping(state) = state < 2; @@ -596,12 +805,14 @@ function looping(state) = state < 2; // Function: loop_while() // Usage: -// state = loop_while(state, continue) +// state = loop_while(state, continue); +// Topics: Iteration +// See Also: looping(), loop_done() // Description: // Given the current `state`, and a boolean `continue` that indicates if the loop should still be -// continuing, returns the updated state value for the the next loop. -// This is useful when using C-style for loops to iteratively calculate a value. -// Used with `looping()` and `loop_done()`. See [Looping Helpers](#5-looping-helpers) for an example. +// continuing, returns the updated state value for the the next loop. This is useful when using +// C-style for loops to iteratively calculate a value. Used with `looping()` and `loop_done()`. +// See [Looping Helpers](section-looping-helpers) for an example. // Arguments: // state = The loop state value. // continue = A boolean value indicating whether the current loop should progress. @@ -612,11 +823,13 @@ function loop_while(state, continue) = // Function: loop_done() // Usage: -// loop_done(state) +// bool = loop_done(state); +// Topics: Iteration +// See Also: looping(), loop_while() // Description: -// Returns true if the `state` value indicates the loop is finishing. -// This is useful when using C-style for loops to iteratively calculate a value. -// Used with `looping()` and `loop_while()`. See [Looping Helpers](#5-looping-helpers) for an example. +// Returns true if the `state` value indicates the loop is finishing. This is useful when using +// C-style for loops to iteratively calculate a value. Used with `looping()` and `loop_while()`. +// See [Looping Helpers](#5-looping-helpers) for an example. // Arguments: // state = The loop state value. function loop_done(state) = state > 0; diff --git a/constants.scad b/constants.scad index ed5bd806..cc6daf1d 100644 --- a/constants.scad +++ b/constants.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: constants.scad // Useful Constants. -// To use this, add the following line to the top of your file. -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -12,9 +10,92 @@ // Constant: $slop // Description: -// The printer specific amount of slop in mm to print with to make parts fit exactly. -// You may need to override this value for your printer. -$slop = 0.20; +// A number of printers, particularly FDM/FFF printers, tend to be a bit sloppy in their printing. +// This has made it so that some parts won't fit together without adding a bit of extra slop space. +// That is what the `$slop` value is for. The value for this will vary from printer to printer. +// By default, we use a value of 0.00 so that parts should fit exactly for resin and other precision +// printers. This value is measured in millimeters. When making your own parts, you should add +// `$slop` to both sides of a hole that another part is to fit snugly into. For a loose fit, add +// `2*$slop` to each side. This should be done for both X and Y axes. The Z axis will require a +// slop that depends on your layer height and bridging settings, and hole sizes. We leave that as +// a more complicated exercise for the user. +// DefineHeader(NumList): Calibration +// Calibration: To calibrate the `$slop` value for your printer, follow this procedure: +// Print the Slop Calibration part from the example below. +// Take the long block and orient it so the numbers are upright, facing you. +// Take the plug and orient it so that the arrow points down, facing you. +// Starting with the hole with the largest number in front of it, insert the small end of the plug into the hole. +// If you can insert and remove the small end of the plug from the hole without much force, then try again with the hole with the next smaller number. +// Repeat step 5 until you have found the hole with the smallest number that the plug fits into without much force. +// The correct hole should hold the plug when the long block is turned upside-down. +// The number in front of that hole will indicate the `$slop` value that is ideal for your printer. +// Remember to set that slop value in your scripts after you include the BOSL2 library: ie: `$slop = 0.15;` +// Example(3D,Med): Slop Calibration Part. +// min_slop = 0.00; +// slop_step = 0.05; +// holes = 8; +// holesize = [15,15,15]; +// height = 20; +// gap = 5; +// l = holes * (holesize.x + gap) + gap; +// w = holesize.y + 2*gap; +// h = holesize.z + 5; +// diff("holes") +// cuboid([l, w, h], anchor=BOT) { +// for (i=[0:holes-1]) { +// right((i-holes/2+0.5)*(holesize.x+gap)) { +// s = min_slop + slop_step * i; +// tags("holes") { +// cuboid([holesize.x + 2*s, holesize.y + 2*s, h+0.2]); +// fwd(w/2-1) xrot(90) linear_extrude(1.1) { +// text( +// text=fmt_fixed(s,2), +// size=0.4*holesize.x, +// halign="center", +// valign="center" +// ); +// } +// } +// } +// } +// } +// back(holesize.y*2.5) { +// difference() { +// union() { +// cuboid([holesize.x+10, holesize.y+10, 15], anchor=BOT); +// cuboid([holesize.x, holesize.y, 15+holesize.z], anchor=BOT); +// } +// up(3) fwd((holesize.y+10)/2) { +// prismoid([holesize.x/2,1], [0,1], h=holesize.y-6); +// } +// } +// } +// Example(2D): Where to add `$slop` gaps. +// $slop = 0.2; +// difference() { +// square([20,12],center=true); +// back(3) square([10+2*$slop,11],center=true); +// } +// back(8) { +// rect([15,5],anchor=FWD); +// rect([10,8],anchor=BACK); +// } +// color("#000") { +// arrow_path = [[5.1,6.1], [6.0,7.1], [8,7.1], [10.5,10]]; +// xflip_copy() +// stroke(arrow_path, width=0.3, endcap1="arrow2"); +// xcopies(21) back(10.5) { +// back(1.8) text("$slop", size=1.5, halign="center"); +// text("gap", size=1.5, halign="center"); +// } +// } +$slop = 0.0; + + +// Constant: INCH +// Description: +// The number of millimeters in an inch. +INCH = 25.4; @@ -22,99 +103,87 @@ $slop = 0.20; // Vectors useful for `rotate()`, `mirror()`, and `anchor` arguments for `cuboid()`, `cyl()`, etc. // Constant: LEFT +// Topics: Constants, Vectors +// See Also: RIGHT, FRONT, BACK, UP, DOWN, CENTER, ALLPOS, ALLNEG // Description: Vector pointing left. [-1,0,0] // Example(3D): Usage with `anchor` // cuboid(20, anchor=LEFT); LEFT = [-1, 0, 0]; // Constant: RIGHT +// Topics: Constants, Vectors +// See Also: LEFT, FRONT, BACK, UP, DOWN, CENTER, ALLPOS, ALLNEG // Description: Vector pointing right. [1,0,0] // Example(3D): Usage with `anchor` // cuboid(20, anchor=RIGHT); RIGHT = [ 1, 0, 0]; // Constant: FRONT +// Aliases: FWD, FORWARD +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, BACK, UP, DOWN, CENTER, ALLPOS, ALLNEG // Description: Vector pointing forward. [0,-1,0] // Example(3D): Usage with `anchor` // cuboid(20, anchor=FRONT); FRONT = [ 0, -1, 0]; +FWD = FRONT; +FORWARD = FRONT; // Constant: BACK +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, UP, DOWN, CENTER, ALLPOS, ALLNEG // Description: Vector pointing back. [0,1,0] // Example(3D): Usage with `anchor` // cuboid(20, anchor=BACK); BACK = [ 0, 1, 0]; // Constant: BOTTOM +// Aliases: BOT, BTM, DOWN +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, BACK, UP, CENTER, ALLPOS, ALLNEG // Description: Vector pointing down. [0,0,-1] // Example(3D): Usage with `anchor` // cuboid(20, anchor=BOTTOM); BOTTOM = [ 0, 0, -1]; +BOT = BOTTOM; +BTM = BOTTOM; +DOWN = BOTTOM; // Constant: TOP +// Aliases: UP +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, BACK, DOWN, CENTER, ALLPOS, ALLNEG // Description: Vector pointing up. [0,0,1] // Example(3D): Usage with `anchor` // cuboid(20, anchor=TOP); TOP = [ 0, 0, 1]; +UP = TOP; // Constant: ALLPOS +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, BACK, UP, DOWN, CENTER, ALLNEG // Description: Vector pointing right, back, and up. [1,1,1] // Example(3D): Usage with `anchor` // cuboid(20, anchor=ALLPOS); ALLPOS = [ 1, 1, 1]; // Vector pointing X+,Y+,Z+. // Constant: ALLNEG +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, BACK, UP, DOWN, CENTER, ALLPOS // Description: Vector pointing left, forwards, and down. [-1,-1,-1] // Example(3D): Usage with `anchor` // cuboid(20, anchor=ALLNEG); ALLNEG = [-1, -1, -1]; // Vector pointing X-,Y-,Z-. // Constant: CENTER +// Aliases: CTR +// Topics: Constants, Vectors +// See Also: LEFT, RIGHT, FRONT, BACK, UP, DOWN, ALLNEG, ALLPOS // Description: Zero vector. Centered. [0,0,0] // Example(3D): Usage with `anchor` // cuboid(20, anchor=CENTER); CENTER = [ 0, 0, 0]; // Centered zero vector. - - -// Section: Vector Aliases -// Useful aliases for use with `anchor`. - -CTR = CENTER; // Zero vector, `[0,0,0]`. Alias to `CENTER`. -UP = TOP; // Vector pointing up, alias to `TOP`. -DOWN = BOTTOM; // Vector pointing down, alias to `BOTTOM`. -BTM = BOTTOM; // Vector pointing down, alias to `BOTTOM`. -BOT = BOTTOM; // Vector pointing down, alias to `BOTTOM`. -FWD = FRONT; // Vector pointing forward, alias to `FRONT`. -FORWARD = FRONT; // Vector pointing forward, alias to `FRONT`. - - - -// CommonCode: -// orientations = [ -// RIGHT, BACK, UP, -// LEFT, FWD, DOWN, -// ]; -// axiscolors = ["red", "forestgreen", "dodgerblue"]; -// module text3d(text, h=0.01, size=3) { -// linear_extrude(height=h, convexity=10) { -// text(text=text, size=size, valign="center", halign="center"); -// } -// } -// module orient_cube(ang) { -// color("lightgray") cube(20, center=true); -// color(axiscolors.x) up ((20-1)/2+0.01) back ((20-1)/2+0.01) cube([18,1,1], center=true); -// color(axiscolors.y) up ((20-1)/2+0.01) right((20-1)/2+0.01) cube([1,18,1], center=true); -// color(axiscolors.z) back((20-1)/2+0.01) right((20-1)/2+0.01) cube([1,1,18], center=true); -// for (axis=[0:2], neg=[0:1]) { -// idx = axis + 3*neg; -// rot(ang, from=UP, to=orientations[idx]) { -// up(10) { -// fwd(4) color("black") text3d(text=str(ang), size=4); -// back(4) color(axiscolors[axis]) text3d(text=str(["X","Y","Z"][axis], ["+","NEG"][neg]), size=4); -// } -// } -// } -// } +CTR = CENTER; diff --git a/coords.scad b/coords.scad index 3d8be118..1843c7ad 100644 --- a/coords.scad +++ b/coords.scad @@ -1,19 +1,20 @@ ////////////////////////////////////////////////////////////////////// // LibFile: coords.scad // Coordinate transformations and coordinate system conversions. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// // Section: Coordinate Manipulation // Function: point2d() +// Usage: +// pt = point2d(p, ); +// Topics: Coordinates, Points +// See Also: path2d(), point3d(), path3d() // Description: -// Returns a 2D vector/point from a 2D or 3D vector. -// If given a 3D point, removes the Z coordinate. +// Returns a 2D vector/point from a 2D or 3D vector. If given a 3D point, removes the Z coordinate. // Arguments: // p = The coordinates to force into a 2D vector/point. // fill = Value to fill missing values in vector with. @@ -21,11 +22,14 @@ function point2d(p, fill=0) = [for (i=[0:1]) (p[i]==undef)? fill : p[i]]; // Function: path2d() +// Usage: +// pts = path2d(points); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), point3d(), path3d() // Description: -// Returns a list of 2D vectors/points from a list of 2D, 3D or higher -// dimensional vectors/points. Removes the extra coordinates from -// higher dimensional points. The input must be a path, where -// every vector has the same length. +// Returns a list of 2D vectors/points from a list of 2D, 3D or higher dimensional vectors/points. +// Removes the extra coordinates from higher dimensional points. The input must be a path, where +// every vector has the same length. // Arguments: // points = A list of 2D or 3D points/vectors. function path2d(points) = @@ -36,6 +40,10 @@ function path2d(points) = // Function: point3d() +// Usage: +// pt = point3d(p, ); +// Topics: Coordinates, Points +// See Also: path2d(), point2d(), path3d() // Description: // Returns a 3D vector/point from a 2D or 3D vector. // Arguments: @@ -45,6 +53,10 @@ function point3d(p, fill=0) = [for (i=[0:2]) (p[i]==undef)? fill : p[i]]; // Function: path3d() +// Usage: +// pts = path3d(points, ); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), path2d(), point3d() // Description: // Returns a list of 3D vectors/points from a list of 2D or higher dimensional vectors/points // by removing extra coordinates or adding the z coordinate. @@ -65,6 +77,10 @@ function path3d(points, fill=0) = // Function: point4d() +// Usage: +// pt = point4d(p, ); +// Topics: Coordinates, Points +// See Also: point2d(), path2d(), point3d(), path3d(), path4d() // Description: // Returns a 4D vector/point from a 2D or 3D vector. // Arguments: @@ -74,12 +90,15 @@ function point4d(p, fill=0) = [for (i=[0:3]) (p[i]==undef)? fill : p[i]]; // Function: path4d() +// Usage: +// pt = path4d(points, ); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), path2d(), point3d(), path3d(), point4d() // Description: // Returns a list of 4D vectors/points from a list of 2D or 3D vectors/points. // Arguments: // points = A list of 2D or 3D points/vectors. // fill = Value to fill missing values in vectors with. - function path4d(points, fill=0) = assert(is_num(fill) || is_vector(fill)) assert(is_path(points, dim=undef, fast=true), "Input to path4d is not a path") @@ -104,8 +123,10 @@ function path4d(points, fill=0) = // Function: polar_to_xy() // Usage: -// polar_to_xy(r, theta); -// polar_to_xy([r, theta]); +// pt = polar_to_xy(r, theta); +// pt = polar_to_xy([r, theta]); +// Topics: Coordinates, Points, Paths +// See Also: xy_to_polar(), xyz_to_cylindrical(), cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert polar coordinates to 2D cartesian coordinates. // Returns [X,Y] cartesian coordinates. @@ -116,6 +137,13 @@ function path4d(points, fill=0) = // xy = polar_to_xy(20,45); // Returns: ~[14.1421365, 14.1421365] // xy = polar_to_xy(40,30); // Returns: ~[34.6410162, 15] // xy = polar_to_xy([40,30]); // Returns: ~[34.6410162, 15] +// Example(2D): +// r=40; ang=30; $fn=36; +// pt = polar_to_xy(r,ang); +// stroke(circle(r=r), closed=true, width=0.5); +// color("black") stroke([[r,0], [0,0], pt], width=0.5); +// color("black") stroke(arc(r=15, angle=ang), width=0.5); +// color("red") move(pt) circle(d=3); function polar_to_xy(r,theta=undef) = let( rad = theta==undef? r[0] : r, t = theta==undef? r[1] : theta @@ -124,8 +152,10 @@ function polar_to_xy(r,theta=undef) = let( // Function: xy_to_polar() // Usage: -// xy_to_polar(x,y); -// xy_to_polar([X,Y]); +// r_theta = xy_to_polar(x,y); +// r_theta = xy_to_polar([X,Y]); +// Topics: Coordinates, Points, Paths +// See Also: polar_to_xy(), xyz_to_cylindrical(), cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert 2D cartesian coordinates to polar coordinates. // Returns [radius, theta] where theta is the angle counter-clockwise of X+. @@ -135,6 +165,13 @@ function polar_to_xy(r,theta=undef) = let( // Examples: // plr = xy_to_polar(20,30); // plr = xy_to_polar([40,60]); +// Example(2D): +// pt = [-20,30]; $fn = 36; +// rt = xy_to_polar(pt); +// r = rt[0]; ang = rt[1]; +// stroke(circle(r=r), closed=true, width=0.5); +// zrot(ang) stroke([[0,0],[r,0]],width=0.5); +// color("red") move(pt) circle(d=3); function xy_to_polar(x,y=undef) = let( xx = y==undef? x[0] : x, yy = y==undef? x[1] : y @@ -143,11 +180,13 @@ function xy_to_polar(x,y=undef) = let( // Function: project_plane() // Usage: With the plane defined by 3 Points -// xyz = project_plane(point, a, b, c); +// pt = project_plane(point, a, b, c); // Usage: With the plane defined by Pointlist -// xyz = project_plane(point, POINTLIST); +// pt = project_plane(point, POINTLIST); // Usage: With the plane defined by Plane Definition [A,B,C,D] Where Ax+By+Cz=D -// xyz = project_plane(point, PLANE); +// pt = project_plane(point, PLANE); +// Topics: Coordinates, Points, Paths +// See Also: lift_plane() // Description: // Converts the given 3D points from global coordinates to the 2D planar coordinates of the closest // points on the plane. This coordinate system can be useful in taking a set of nearly coplanar @@ -167,6 +206,20 @@ function xy_to_polar(x,y=undef) = let( // a=[0,0,0]; b=[10,-10,0]; c=[10,0,10]; // xy = project_plane(pt, a, b, c); // xy2 = project_plane(pt, [a,b,c]); +// Example(3D): +// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100, $fn=36)))); +// plane = plane_from_normal([1,0,1]); +// proj = project_plane(points,plane); +// n = plane_normal(plane); +// cp = centroid(proj); +// color("red") move_copies(points) sphere(d=2,$fn=12); +// color("blue") rot(from=UP,to=n,cp=cp) move_copies(proj) sphere(d=2,$fn=12); +// move(cp) { +// rot(from=UP,to=n) { +// anchor_arrow(30); +// %cube([120,150,0.1],center=true); +// } +// } function project_plane(point, a, b, c) = is_undef(b) && is_undef(c) && is_list(a)? let( mat = is_vector(a,4)? plane_transform(a) : @@ -197,6 +250,8 @@ function project_plane(point, a, b, c) = // xyz = lift_plane(point, POINTLIST); // Usage: With Plane Definition [A,B,C,D] Where Ax+By+Cz=D // xyz = lift_plane(point, PLANE); +// Topics: Coordinates, Points, Paths +// See Also: project_plane() // Description: // Converts the given 2D point from planar coordinates to the global 3D coordinates of the point on the plane. // Can be called one of three ways: @@ -234,8 +289,10 @@ function lift_plane(point, a, b, c) = // Function: cylindrical_to_xyz() // Usage: -// cylindrical_to_xyz(r, theta, z) -// cylindrical_to_xyz([r, theta, z]) +// pt = cylindrical_to_xyz(r, theta, z); +// pt = cylindrical_to_xyz([r, theta, z]); +// Topics: Coordinates, Points, Paths +// See Also: xyz_to_cylindrical(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert cylindrical coordinates to 3D cartesian coordinates. Returns [X,Y,Z] cartesian coordinates. // Arguments: @@ -254,12 +311,13 @@ function cylindrical_to_xyz(r,theta=undef,z=undef) = let( // Function: xyz_to_cylindrical() // Usage: -// xyz_to_cylindrical(x,y,z) -// xyz_to_cylindrical([X,Y,Z]) +// rtz = xyz_to_cylindrical(x,y,z); +// rtz = xyz_to_cylindrical([X,Y,Z]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: -// Convert 3D cartesian coordinates to cylindrical coordinates. -// Returns [radius,theta,Z]. Theta is the angle counter-clockwise -// of X+ on the XY plane. Z is height above the XY plane. +// Convert 3D cartesian coordinates to cylindrical coordinates. Returns [radius,theta,Z]. +// Theta is the angle counter-clockwise of X+ on the XY plane. Z is height above the XY plane. // Arguments: // x = X coordinate. // y = Y coordinate. @@ -274,11 +332,12 @@ function xyz_to_cylindrical(x,y=undef,z=undef) = let( // Function: spherical_to_xyz() // Usage: -// spherical_to_xyz(r, theta, phi); -// spherical_to_xyz([r, theta, phi]); +// pt = spherical_to_xyz(r, theta, phi); +// pt = spherical_to_xyz([r, theta, phi]); // Description: -// Convert spherical coordinates to 3D cartesian coordinates. -// Returns [X,Y,Z] cartesian coordinates. +// Convert spherical coordinates to 3D cartesian coordinates. Returns [X,Y,Z] cartesian coordinates. +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), xyz_to_cylindrical() // Arguments: // r = distance from origin. // theta = angle in degrees, counter-clockwise of X+ on the XY plane. @@ -295,12 +354,13 @@ function spherical_to_xyz(r,theta=undef,phi=undef) = let( // Function: xyz_to_spherical() // Usage: -// xyz_to_spherical(x,y,z) -// xyz_to_spherical([X,Y,Z]) +// r_theta_phi = xyz_to_spherical(x,y,z) +// r_theta_phi = xyz_to_spherical([X,Y,Z]) +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), spherical_to_xyz(), xyz_to_cylindrical() // Description: -// Convert 3D cartesian coordinates to spherical coordinates. -// Returns [r,theta,phi], where phi is the angle from the Z+ pole, -// and theta is degrees counter-clockwise of X+ on the XY plane. +// Convert 3D cartesian coordinates to spherical coordinates. Returns [r,theta,phi], where phi is +// the angle from the Z+ pole, and theta is degrees counter-clockwise of X+ on the XY plane. // Arguments: // x = X coordinate. // y = Y coordinate. @@ -315,8 +375,10 @@ function xyz_to_spherical(x,y=undef,z=undef) = let( // Function: altaz_to_xyz() // Usage: -// altaz_to_xyz(alt, az, r); -// altaz_to_xyz([alt, az, r]); +// pt = altaz_to_xyz(alt, az, r); +// pt = altaz_to_xyz([alt, az, r]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz(), xyz_to_cylindrical(), xyz_to_altaz() // Description: // Convert altitude/azimuth/range coordinates to 3D cartesian coordinates. // Returns [X,Y,Z] cartesian coordinates. @@ -336,8 +398,10 @@ function altaz_to_xyz(alt,az=undef,r=undef) = let( // Function: xyz_to_altaz() // Usage: -// xyz_to_altaz(x,y,z); -// xyz_to_altaz([X,Y,Z]); +// alt_az_r = xyz_to_altaz(x,y,z); +// alt_az_r = xyz_to_altaz([X,Y,Z]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz(), xyz_to_cylindrical(), altaz_to_xyz() // Description: // Convert 3D cartesian coordinates to altitude/azimuth/range coordinates. // Returns [altitude,azimuth,range], where altitude is angle above the diff --git a/cubetruss.scad b/cubetruss.scad index b372b751..880a301e 100644 --- a/cubetruss.scad +++ b/cubetruss.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////////// -// Libfile: cubetruss.scad +// LibFile: cubetruss.scad // Parts for making modular open-frame cross-braced trusses and connectors. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////////// $cubetruss_size = 30; @@ -14,9 +12,11 @@ $cubetruss_bracing = true; $cubetruss_clip_thickness = 1.6; +// Section: Cube Trusses + // Function: cubetruss_dist() // Usage: -// cubetruss_dist(cubes, gaps, [size], [strut]); +// cubetruss_dist(cubes, gaps, , ); // Description: // Function to calculate the length of a cubetruss truss. // Arguments: @@ -24,7 +24,8 @@ $cubetruss_clip_thickness = 1.6; // gaps = The number of extra strut widths to add in, corresponding to each time a truss butts up against another. // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) -function cubetruss_dist(cubes=0, gaps=0, size=undef, strut=undef) = +// Topics: Trusses +function cubetruss_dist(cubes=0, gaps=0, size, strut) = let( size = is_undef(size)? $cubetruss_size : size, strut = is_undef(strut)? $cubetruss_strut_size : strut @@ -33,22 +34,24 @@ function cubetruss_dist(cubes=0, gaps=0, size=undef, strut=undef) = // Module: cubetruss_segment() // Usage: -// cubetruss_segment([size], [strut], [bracing]); +// cubetruss_segment(, , ); // Description: // Creates a single cubetruss cube segment. // Arguments: // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // bracing = If true, adds internal cross-braces. Default: `$cubetruss_bracing` (usually true) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples: // cubetruss_segment(bracing=false); // cubetruss_segment(bracing=true); // cubetruss_segment(strut=4); // cubetruss_segment(size=40); -module cubetruss_segment(size=undef, strut=undef, bracing=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_segment(size, strut, bracing, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; bracing = is_undef(bracing)? $cubetruss_bracing : bracing; @@ -100,7 +103,7 @@ module cubetruss_segment(size=undef, strut=undef, bracing=undef, anchor=CENTER, // Module: cubetruss_clip() // Usage: -// cubetruss_clip(extents, [size], [strut], [clipthick]); +// cubetruss_clip(extents, , , ); // Description: // Creates a pair of clips to add onto the end of a truss. // Arguments: @@ -108,14 +111,16 @@ module cubetruss_segment(size=undef, strut=undef, bracing=undef, anchor=CENTER, // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // clipthick = The thickness of the clip. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples: // cubetruss_clip(extents=2); // cubetruss_clip(extents=1); // cubetruss_clip(clipthick=2.5); -module cubetruss_clip(extents=1, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_clip(extents=1, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; clipthick = is_undef(clipthick)? $cubetruss_clip_thickness : clipthick; @@ -162,7 +167,7 @@ module cubetruss_clip(extents=1, size=undef, strut=undef, clipthick=undef, ancho // Module: cubetruss_foot() // Usage: -// cubetruss_foot(w, [size], [strut], [clipthick]); +// cubetruss_foot(w, , , ); // Description: // Creates a foot that can be clipped onto the bottom of a truss for support. // Arguments: @@ -170,13 +175,15 @@ module cubetruss_clip(extents=1, size=undef, strut=undef, clipthick=undef, ancho // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // clipthick = The thickness of the clips. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples: // cubetruss_foot(w=1); // cubetruss_foot(w=3); -module cubetruss_foot(w=1, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_foot(w=1, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; clipthick = is_undef(clipthick)? $cubetruss_clip_thickness : clipthick; @@ -236,7 +243,7 @@ module cubetruss_foot(w=1, size=undef, strut=undef, clipthick=undef, anchor=CENT // Module: cubetruss_joiner() // Usage: -// cubetruss_joiner([w], [vert], [size], [strut], [clipthick]); +// cubetruss_joiner(, , , , ); // Description: // Creates a part to join two cubetruss trusses end-to-end. // Arguments: @@ -245,14 +252,16 @@ module cubetruss_foot(w=1, size=undef, strut=undef, clipthick=undef, anchor=CENT // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // clipthick = The thickness of the clips. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples: // cubetruss_joiner(w=1, vert=false); // cubetruss_joiner(w=1, vert=true); // cubetruss_joiner(w=2, vert=true, anchor=BOT); -module cubetruss_joiner(w=1, vert=true, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_joiner(w=1, vert=true, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; clipthick = is_undef(clipthick)? $cubetruss_clip_thickness : clipthick; @@ -301,19 +310,23 @@ module cubetruss_joiner(w=1, vert=true, size=undef, strut=undef, clipthick=undef // Module: cubetruss_uclip() // Usage: -// cubetruss_uclip(dual, [size], [strut], [clipthick]); +// cubetruss_uclip(dual, , , ); // Description: +// Creates a small clip that can snap around one or two adjacent struts. // Arguments: +// dual = If true, create a clip to clip around two adjacent struts. If false, just fit around one strut. Default: true // size = The length of each side of the cubetruss cubes. Default: `$cubetruss_size` (usually 30) // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // clipthick = The thickness of the clips. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples: // cubetruss_uclip(dual=false); // cubetruss_uclip(dual=true); -module cubetruss_uclip(dual=true, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_uclip(dual=true, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; clipthick = is_undef(clipthick)? $cubetruss_clip_thickness : clipthick; @@ -342,7 +355,7 @@ module cubetruss_uclip(dual=true, size=undef, strut=undef, clipthick=undef, anch // Module: cubetruss() // Usage: -// cubetruss(extents, clips, bracing, size, strut, clipthick); +// cubetruss(extents, , , , , ); // Description: // Creates a cubetruss truss, assembled out of one or more cubical segments. // Arguments: @@ -352,17 +365,19 @@ module cubetruss_uclip(dual=true, size=undef, strut=undef, clipthick=undef, anch // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // bracing = If true, adds internal cross-braces. Default: `$cubetruss_bracing` (usually true) // clipthick = The thickness of the clips. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` -// Examples(FlatSpin): +// Topics: Attachable, Trusses +// Examples(FlatSpin,VPD=444): // cubetruss(extents=3); // cubetruss(extents=3, clips=FRONT); // cubetruss(extents=3, clips=[FRONT,BACK]); // cubetruss(extents=[2,3]); // cubetruss(extents=[1,4,2]); // cubetruss(extents=[1,4,2], bracing=false); -module cubetruss(extents=6, clips=[], bracing=undef, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss(extents=6, clips=[], bracing, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { clips = is_vector(clips)? [clips] : clips; size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; @@ -410,7 +425,7 @@ module cubetruss(extents=6, clips=[], bracing=undef, size=undef, strut=undef, cl // Module: cubetruss_corner() // Usage: -// cubetruss_corner(h, extents, [bracing], [size], [strut], [clipthick); +// cubetruss_corner(h, extents, , , , ); // Description: // Creates a corner cubetruss with extents jutting out in one or more directions. // Arguments: @@ -420,16 +435,18 @@ module cubetruss(extents=6, clips=[], bracing=undef, size=undef, strut=undef, cl // strut = The width of the struts on the cubetruss cubes. Default: `$cubetruss_strut_size` (usually 3) // bracing = If true, adds internal cross-braces. Default: `$cubetruss_bracing` (usually true) // clipthick = The thickness of the clips. Default: `$cubetruss_clip_thickness` (usually 1.6) +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// Topics: Attachable, Trusses // Examples(FlatSpin): // cubetruss_corner(extents=2); // cubetruss_corner(extents=2, h=2); // cubetruss_corner(extents=[3,3,0,0,2]); // cubetruss_corner(extents=[3,0,3,0,2]); // cubetruss_corner(extents=[3,3,3,3,2]); -module cubetruss_corner(h=1, extents=[1,1,0,0,1], bracing=undef, size=undef, strut=undef, clipthick=undef, anchor=CENTER, spin=0, orient=UP) { +module cubetruss_corner(h=1, extents=[1,1,0,0,1], bracing, size, strut, clipthick, anchor=CENTER, spin=0, orient=UP) { size = is_undef(size)? $cubetruss_size : size; strut = is_undef(strut)? $cubetruss_strut_size : strut; bracing = is_undef(bracing)? $cubetruss_bracing : bracing; diff --git a/debug.scad b/debug.scad index 96e57156..6e4bdae2 100644 --- a/debug.scad +++ b/debug.scad @@ -1,28 +1,28 @@ ////////////////////////////////////////////////////////////////////// // LibFile: debug.scad // Helpers to make debugging OpenScad code easier. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// include -// ``` ////////////////////////////////////////////////////////////////////// // Section: Debugging Paths and Polygons // Module: trace_path() +// Usage: +// trace_path(path, , , , , ); // Description: // Renders lines between each point of a path. // Can also optionally show the individual vertex points. // Arguments: // path = The list of points in the path. +// --- // closed = If true, draw the segment from the last vertex to the first. Default: false // showpts = If true, draw vertices and control points. // N = Mark the first and every Nth vertex after in a different color and shape. // size = Diameter of the lines drawn. // color = Color to draw the lines (but not vertices) in. -// Example(FlatSpin): +// Example(FlatSpin,VPD=44.4): // path = [for (a=[0:30:210]) 10*[cos(a), sin(a), sin(a)]]; // trace_path(path, showpts=true, size=0.5, color="lightgreen"); module trace_path(path, closed=false, showpts=false, N=1, size=1, color="yellow") { @@ -32,7 +32,7 @@ module trace_path(path, closed=false, showpts=false, N=1, size=1, color="yellow" if (showpts) { for (i = [0:1:len(path)-1]) { translate(path[i]) { - if (i%N == 0) { + if (i % N == 0) { color("blue") sphere(d=size*2.5, $fn=8); } else { color("red") { @@ -48,7 +48,7 @@ module trace_path(path, closed=false, showpts=false, N=1, size=1, color="yellow" color(color) stroke(path3d(path), width=size, $fn=8); } else { for (i = [0:1:len(path)-2]) { - if (N!=3 || (i%N) != 1) { + if (N != 3 || (i % N) != 1) { color(color) extrude_from_to(path[i], path[i+1]) circle(d=size, $fn=sides); } } @@ -57,11 +57,16 @@ module trace_path(path, closed=false, showpts=false, N=1, size=1, color="yellow" // Module: debug_polygon() -// Description: A drop-in replacement for `polygon()` that renders and labels the path points. +// Usage: +// debug_polygon(points, paths, , ); +// Description: +// A drop-in replacement for `polygon()` that renders and labels the path points. // Arguments: // points = The array of 2D polygon vertices. // paths = The path connections between the vertices. +// --- // convexity = The max number of walls a ray can pass through the given polygon paths. +// size = The base size of the line and labels. // Example(Big2D): // debug_polygon( // points=concat( @@ -73,9 +78,11 @@ module trace_path(path, closed=false, showpts=false, N=1, size=1, color="yellow" // [for (i=[15:-1:8]) i] // ] // ); -module debug_polygon(points, paths=undef, convexity=2, size=1) +module debug_polygon(points, paths, convexity=2, size=1) { - pths = is_undef(paths)? [for (i=[0:1:len(points)-1]) i] : is_num(paths[0])? [paths] : paths; + paths = is_undef(paths)? [[for (i=[0:1:len(points)-1]) i]] : + is_num(paths[0])? [paths] : + paths; echo(points=points); echo(paths=paths); linear_extrude(height=0.01, convexity=convexity, center=true) { @@ -121,13 +128,16 @@ module debug_polygon(points, paths=undef, convexity=2, size=1) // Module: debug_vertices() +// Usage: +// debug_vertices(vertices, , ); // Description: // Draws all the vertices in an array, at their 3D position, numbered by their // position in the vertex array. Also draws any children of this module with // transparency. // Arguments: // vertices = Array of point vertices. -// size = The size of the text used to label the vertices. +// size = The size of the text used to label the vertices. Default: 1 +// --- // disabled = If true, don't draw numbers, and draw children without transparency. Default = false. // Example: // verts = [for (z=[-10,10], y=[-10,10], x=[-10,10]) [x,y,z]]; @@ -164,6 +174,8 @@ module debug_vertices(vertices, size=1, disabled=false) { // Module: debug_faces() +// Usage: +// debug_faces(vertices, faces, , ); // Description: // Draws all the vertices at their 3D position, numbered in blue by their // position in the vertex array. Each face will have their face number drawn @@ -171,9 +183,10 @@ module debug_vertices(vertices, size=1, disabled=false) { // with transparency. // Arguments: // vertices = Array of point vertices. -// faces = Array of faces by vertex numbers. -// size = The size of the text used to label the faces and vertices. -// disabled = If true, don't draw numbers, and draw children without transparency. Default = false. +// faces = Array of faces by vertex numbers. +// --- +// size = The size of the text used to label the faces and vertices. Default: 1 +// disabled = If true, don't draw numbers, and draw children without transparency. Default: false. // Example(EdgesMed): // verts = [for (z=[-10,10], y=[-10,10], x=[-10,10]) [x,y,z]]; // faces = [[0,1,2], [1,3,2], [0,4,5], [0,5,1], [1,5,7], [1,7,3], [3,7,6], [3,6,2], [2,6,4], [2,4,0], [4,6,7], [4,7,5]]; @@ -227,6 +240,8 @@ module debug_faces(vertices, faces, size=1, disabled=false) { // Module: debug_polyhedron() +// Usage: +// debug_polyhedron(points, faces, , , ); // Description: // A drop-in module to replace `polyhedron()` and help debug vertices and faces. // Draws all the vertices at their 3D position, numbered in blue by their @@ -235,15 +250,17 @@ module debug_faces(vertices, faces, size=1, disabled=false) { // transparency. All children of this module are drawn with transparency. // Works best with Thrown-Together preview mode, to see reversed faces. // Arguments: -// vertices = Array of point vertices. +// points = Array of point vertices. // faces = Array of faces by vertex numbers. +// --- +// convexity = The max number of walls a ray can pass through the given polygon paths. // txtsize = The size of the text used to label the faces and vertices. // disabled = If true, act exactly like `polyhedron()`. Default = false. // Example(EdgesMed): // verts = [for (z=[-10,10], a=[0:120:359.9]) [10*cos(a),10*sin(a),z]]; // faces = [[0,1,2], [5,4,3], [0,3,4], [0,4,1], [1,4,5], [1,5,2], [2,5,3], [2,3,0]]; // debug_polyhedron(points=verts, faces=faces, txtsize=1); -module debug_polyhedron(points, faces, convexity=10, txtsize=1, disabled=false) { +module debug_polyhedron(points, faces, convexity=6, txtsize=1, disabled=false) { debug_faces(vertices=points, faces=faces, size=txtsize, disabled=disabled) { polyhedron(points=points, faces=faces, convexity=convexity); } @@ -276,11 +293,11 @@ function standard_anchors(two_d=false) = [ // Usage: // anchor_arrow(, , ); // Description: -// Show an anchor orientation arrow. +// Show an anchor orientation arrow. By default, tagged with the name "anchor-arrow". // Arguments: -// s = Length of the arrows. -// color = Color of the arrow. -// flag = If true, draw the orientation flag on the arrowhead. +// s = Length of the arrows. Default: `10` +// color = Color of the arrow. Default: `[0.333, 0.333, 1]` +// flag = If true, draw the orientation flag on the arrowhead. Default: true // Example: // anchor_arrow(s=20); module anchor_arrow(s=10, color=[0.333,0.333,1], flag=true, $tags="anchor-arrow") { @@ -317,28 +334,35 @@ module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tags="anchor-arrow") { -// Module: show_internal_anchors() +// Module: expose_anchors() // Usage: -// show_internal_anchors() ... +// expose_anchors(opacity) {...} // Description: -// Makes the children transparent gray, while showing any -// anchor arrows that may exist. -// Example(FlatSpin): -// show_internal_anchors() cube(50, center=true) show_anchors(); -module show_internal_anchors(opacity=0.2) { - show("anchor-arrow") children() show_anchors(); - hide("anchor-arrow") recolor(list_pad(point3d($color),4,fill=opacity)) children(); +// Makes the children transparent gray, while showing any anchor arrows that may exist. +// Arguments: +// opacity = The opacity of the arrow. 0.0 is invisible, 1.0 is opaque. Default: 0.2 +// Example(FlatSpin,VPD=333): +// expose_anchors() cube(50, center=true) show_anchors(); +module expose_anchors(opacity=0.2) { + show("anchor-arrow") + children(); + hide("anchor-arrow") + color(is_string($color)? $color : point3d($color), opacity) + children(); } // Module: show_anchors() +// Usage: +// ... show_anchors(, , ); // Description: // Show all standard anchors for the parent object. // Arguments: // s = Length of anchor arrows. +// --- // std = If true (default), show standard anchors. // custom = If true (default), show custom anchors. -// Example(FlatSpin): +// Example(FlatSpin,VPD=333): // cube(50, center=true) show_anchors(); module show_anchors(s=10, std=true, custom=true) { check = assert($parent_geom != undef) 1; @@ -379,28 +403,35 @@ module show_anchors(s=10, std=true, custom=true) { // Module: frame_ref() +// Usage: +// frame_ref(s, opacity); // Description: // Displays X,Y,Z axis arrows in red, green, and blue respectively. // Arguments: // s = Length of the arrows. +// opacity = The opacity of the arrows. 0.0 is invisible, 1.0 is opaque. Default: 1.0 // Examples: // frame_ref(25); -module frame_ref(s=15) { +// frame_ref(30, opacity=0.5); +module frame_ref(s=15, opacity=1) { cube(0.01, center=true) { - attach(RIGHT) anchor_arrow(s=s, flag=false, color="red"); - attach(BACK) anchor_arrow(s=s, flag=false, color="green"); - attach(TOP) anchor_arrow(s=s, flag=false, color="blue"); + attach([1,0,0]) anchor_arrow(s=s, flag=false, color=[1.0, 0.3, 0.3, opacity]); + attach([0,1,0]) anchor_arrow(s=s, flag=false, color=[0.3, 1.0, 0.3, opacity]); + attach([0,0,1]) anchor_arrow(s=s, flag=false, color=[0.3, 0.3, 1.0, opacity]); children(); } } // Module: ruler() +// Usage: +// ruler(length, width, , , , , , , , , ); // Description: // Creates a ruler for checking dimensions of the model // Arguments: // length = length of the ruler. Default 100 // width = width of the ruler. Default: size of the largest unit division +// --- // thickness = thickness of the ruler. Default: 1 // depth = the depth of mark subdivisions. Default: 3 // labels = draw numeric labels for depths where labels are larger than 1. Default: false @@ -410,6 +441,9 @@ module frame_ref(s=15) { // alpha = transparency value. Default: 1.0 // unit = unit to mark. Scales the ruler marks to a different length. Default: 1 // inch = set to true for a ruler scaled to inches (assuming base dimension is mm). Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `LEFT+BACK+TOP` +// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` // Examples(2D,Big): // ruler(100,depth=3); // ruler(100,depth=3,labels=true); @@ -420,7 +454,7 @@ module frame_ref(s=15) { // Example(2D,Big): Metric vs Imperial // ruler(12,width=50,inch=true,labels=true,maxscale=0); // fwd(50)ruler(300,width=50,labels=true); -module ruler(length=100, width=undef, thickness=1, depth=3, labels=false, pipscale=1/3, maxscale=undef, colors=["black","white"], alpha=1.0, unit=1, inch=false, anchor=LEFT+BACK+TOP, spin=0, orient=UP) +module ruler(length=100, width, thickness=1, depth=3, labels=false, pipscale=1/3, maxscale, colors=["black","white"], alpha=1.0, unit=1, inch=false, anchor=LEFT+BACK+TOP, spin=0, orient=UP) { inchfactor = 25.4; assert(depth<=5, "Cannot render scales smaller than depth=5"); @@ -495,11 +529,12 @@ function mod_indent(indent=" ") = // Function: mod_trace() // Usage: -// str = mod_trace(, ); +// str = mod_trace(, , ); // Description: // Returns a string that shows the current module and its parents, indented for each unprinted parent module. // Arguments: // levs = This is the number of levels to print the names of. Prints the N most nested module names. Default: 2 +// --- // indent = The string to indent each level by. Default: " " (Two spaces) // modsep = Multiple module names will be separated by this string. Default: "->" // Example: @@ -511,4 +546,28 @@ function mod_trace(levs=2, indent=" ", modsep="->") = ); +// Function&Module: echo_matrix() +// Usage: +// echo_matrix(M, , , ); +// dummy = echo_matrix(M, , , ), +// Description: +// Display a numerical matrix in a readable columnar format with `sig` significant +// digits. Values smaller than eps display as zero. If you give a description +// it is displayed at the top. +function echo_matrix(M,description,sig=4,eps=1e-9) = + let( + horiz_line = chr(8213), + matstr = matrix_strings(M,sig=sig,eps=eps), + separator = str_join(repeat(horiz_line,10)), + dummy=echo(str(separator," ",is_def(description) ? description : "")) + [for(row=matstr) echo(row)] + ) + echo(separator); + +module echo_matrix(M,description,sig=4,eps=1e-9) +{ + dummy = echo_matrix(M,description,sig,eps); +} + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/distributors.scad b/distributors.scad index 026dc721..1710f03e 100644 --- a/distributors.scad +++ b/distributors.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: distributors.scad // Functions and modules to distribute children or copies of children. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -95,9 +93,9 @@ module move_copies(a=[[0,0,0]]) // line_of(spacing=[10,5], l=50) sphere(d=1); // line_of(l=50, n=4) sphere(d=1); // line_of(l=[50,-30], n=4) sphere(d=1); -// Example(FlatSpin): +// Example(FlatSpin,VPD=133): // line_of(p1=[0,0,0], p2=[5,5,20], n=6) cube(size=[3,2,1],center=true); -// Example(FlatSpin): +// Example(FlatSpin,VPD=133): // line_of(p1=[0,0,0], p2=[5,5,20], spacing=6) cube(size=[3,2,1],center=true); // Example: All Children are Copied at Each Spread Position // line_of(l=20, n=3) { @@ -589,8 +587,9 @@ module grid2d(spacing, n, size, stagger=false, inside=undef) // `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. // `$idx` is set to the [Xidx,Yidx,Zidx] index values of each child copy, when using `count` and `n`. // -// Examples(FlatSpin): +// Examples(FlatSpin,VPD=222): // grid3d(xa=[0:25:50],ya=[0,40],za=[-20:40:20]) sphere(r=5); +// Examples(FlatSpin,VPD=800): // grid3d(n=[3, 4, 2], spacing=[60, 50, 40]) sphere(r=10); // Examples: // grid3d(ya=[-60:40:60],za=[0,70]) sphere(r=10); diff --git a/edges.scad b/edges.scad index a81c591c..72a838ca 100644 --- a/edges.scad +++ b/edges.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: edges.scad // Routines to work with edge sets and edge set descriptors. -// To use this, add the following line to the top of your file. -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// diff --git a/examples/bezier_patches.scad b/examples/bezier_patches.scad index de7042b9..62f775b5 100644 --- a/examples/bezier_patches.scad +++ b/examples/bezier_patches.scad @@ -20,7 +20,7 @@ function CR_corner(size, spin=0, orient=UP, trans=[0,0,0]) = ) translate(trans, p=rot(a=spin, from=UP, to=orient, - p=scale(size, patch) + p=scale(size, p=patch) ) ); diff --git a/involute_gears.scad b/gears.scad similarity index 81% rename from involute_gears.scad rename to gears.scad index 86528b19..5bff0fca 100644 --- a/involute_gears.scad +++ b/gears.scad @@ -1,27 +1,11 @@ ////////////////////////////////////////////////////////////////////////////////////////////// -// LibFile: involute_gears.scad -// Involute Spur Gears and Racks -// . -// by Leemon Baird, 2011, Leemon@Leemon.com -// http://www.thingiverse.com/thing:5505 -// . -// Additional fixes and improvements by Revar Desmera, 2017-2019, revarbat@gmail.com -// . -// This file is public domain. Use it for any purpose, including commercial -// applications. Attribution would be nice, but is not required. There is -// no warranty of any kind, including its correctness, usefulness, or safety. -// . -// This is parameterized involute spur (or helical) gear. It is much simpler -// and less powerful than others on Thingiverse. But it is public domain. I -// implemented it from scratch from the descriptions and equations on Wikipedia -// and the web, using Mathematica for calculations and testing, and I now -// release it into the public domain. -// . -// To use, add the following line to the beginning of your file: -// ``` +// LibFile: gears.scad +// Spur Gears, Bevel Gears, Racks, Worms and Worm Gears. +// Originally based on code by Leemon Baird, 2011, Leemon@Leemon.com +// Almost completely rewritten for BOSL2 by Revar Desmera, 2017-2021, revarbat@gmail.com +// Includes: // include -// include -// ``` +// include ////////////////////////////////////////////////////////////////////////////////////////////// @@ -47,6 +31,7 @@ // Function: circular_pitch() // Usage: // circp = circular_pitch(pitch|mod); +// Topics: Gears // Description: // Get tooth density expressed as "circular pitch". // Arguments: @@ -63,6 +48,7 @@ function circular_pitch(pitch=5, mod) = // Function: diametral_pitch() // Usage: // dp = diametral_pitch(pitch|mod); +// Topics: Gears // Description: // Get tooth density expressed as "diametral pitch". // Arguments: @@ -79,8 +65,10 @@ function diametral_pitch(pitch=5, mod) = // Function: pitch_value() // Usage: // pitch = pitch_value(mod); +// Topics: Gears // Description: -// Get circular pitch in mm from module/modulus. +// Get circular pitch in mm from module/modulus. The circular pitch of a gear is the number of +// millimeters per tooth around the pitch radius circle. // Arguments: // mod = The module/modulus of the gear. function pitch_value(mod) = mod * PI; @@ -89,8 +77,11 @@ function pitch_value(mod) = mod * PI; // Function: module_value() // Usage: // mod = module_value(pitch); +// Topics: Gears // Description: -// Get tooth density expressed as "module" or "modulus" in millimeters +// Get tooth density expressed as "module" or "modulus" in millimeters. The module is the pitch +// diameter of the gear divided by the number of teeth on it. For example, a gear with a pitch +// diameter of 40mm, with 20 teeth on it will have a modulus of 2. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. function module_value(pitch=5) = pitch / PI; @@ -99,14 +90,24 @@ function module_value(pitch=5) = pitch / PI; // Function: adendum() // Usage: // ad = adendum(pitch|mod); +// Topics: Gears // Description: -// The height of the gear tooth above the pitch radius. +// The height of the top of a gear tooth above the pitch radius circle. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // mod = The metric module/modulus of the gear. // Examples: // ad = adendum(pitch=5); // ad = adendum(mod=2); +// Example(2D): +// pitch = 5; teeth = 17; +// pr = pitch_radius(pitch=pitch, teeth=teeth); +// adn = adendum(pitch=5); +// #spur_gear2d(pitch=pitch, teeth=teeth); +// color("black") { +// stroke(circle(r=pr),width=0.1,closed=true); +// stroke(circle(r=pr+adn),width=0.1,closed=true); +// } function adendum(pitch=5, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) module_value(pitch) * 1.0; @@ -115,6 +116,7 @@ function adendum(pitch=5, mod) = // Function: dedendum() // Usage: // ddn = dedendum(pitch|mod, ); +// Topics: Gears // Description: // The depth of the gear tooth valley, below the pitch radius. // Arguments: @@ -124,6 +126,15 @@ function adendum(pitch=5, mod) = // Examples: // ddn = dedendum(pitch=5); // ddn = dedendum(mod=2); +// Example(2D): +// pitch = 5; teeth = 17; +// pr = pitch_radius(pitch=pitch, teeth=teeth); +// ddn = dedendum(pitch=5); +// #spur_gear2d(pitch=pitch, teeth=teeth); +// color("black") { +// stroke(circle(r=pr),width=0.1,closed=true); +// stroke(circle(r=pr-ddn),width=0.1,closed=true); +// } function dedendum(pitch=5, clearance, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) is_undef(clearance)? (1.25 * module_value(pitch)) : @@ -133,6 +144,7 @@ function dedendum(pitch=5, clearance, mod) = // Function: pitch_radius() // Usage: // pr = pitch_radius(pitch|mod, teeth); +// Topics: Gears // Description: // Calculates the pitch radius for the gear. Two mated gears will have their centers spaced apart // by the sum of the two gear's pitch radii. @@ -143,6 +155,11 @@ function dedendum(pitch=5, clearance, mod) = // Examples: // pr = pitch_radius(pitch=5, teeth=11); // pr = pitch_radius(mod=2, teeth=20); +// Example(2D): +// pr = pitch_radius(pitch=5, teeth=11); +// #spur_gear2d(pitch=5, teeth=11); +// color("black") +// stroke(circle(r=pr),width=0.1,closed=true); function pitch_radius(pitch=5, teeth=11, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch * teeth / PI / 2; @@ -151,6 +168,7 @@ function pitch_radius(pitch=5, teeth=11, mod) = // Function: outer_radius() // Usage: // or = outer_radius(pitch|mod, teeth, , ); +// Topics: Gears // Description: // Calculates the outer radius for the gear. The gear fits entirely within a cylinder of this radius. // Arguments: @@ -162,6 +180,11 @@ function pitch_radius(pitch=5, teeth=11, mod) = // Examples: // or = outer_radius(pitch=5, teeth=20); // or = outer_radius(mod=2, teeth=16); +// Example(2D): +// pr = outer_radius(pitch=5, teeth=11); +// #spur_gear2d(pitch=5, teeth=11); +// color("black") +// stroke(circle(r=pr),width=0.1,closed=true); function outer_radius(pitch=5, teeth=11, clearance, interior=false, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch_radius(pitch, teeth) + @@ -171,6 +194,7 @@ function outer_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // Function: root_radius() // Usage: // rr = root_radius(pitch|mod, teeth, , ); +// Topics: Gears // Description: // Calculates the root radius for the gear, at the base of the dedendum. // Arguments: @@ -182,6 +206,11 @@ function outer_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // Examples: // rr = root_radius(pitch=5, teeth=11); // rr = root_radius(mod=2, teeth=16); +// Example(2D): +// pr = root_radius(pitch=5, teeth=11); +// #spur_gear2d(pitch=5, teeth=11); +// color("black") +// stroke(circle(r=pr),width=0.1,closed=true); function root_radius(pitch=5, teeth=11, clearance, interior=false, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch_radius(pitch, teeth) - @@ -191,6 +220,7 @@ function root_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // Function: base_radius() // Usage: // br = base_radius(pitch|mod, teeth, ); +// Topics: Gears // Description: // Get the base circle for involute teeth, at the base of the teeth. // Arguments: @@ -201,6 +231,11 @@ function root_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // Examples: // br = base_radius(pitch=5, teeth=20, pressure_angle=20); // br = base_radius(mod=2, teeth=18, pressure_angle=20); +// Example(2D): +// pr = base_radius(pitch=5, teeth=11); +// #spur_gear2d(pitch=5, teeth=11); +// color("black") +// stroke(circle(r=pr),width=0.1,closed=true); function base_radius(pitch=5, teeth=11, pressure_angle=28, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch_radius(pitch, teeth) * cos(pressure_angle); @@ -209,6 +244,8 @@ function base_radius(pitch=5, teeth=11, pressure_angle=28, mod) = // Function: bevel_pitch_angle() // Usage: // ang = bevel_pitch_angle(teeth, mate_teeth, ); +// Topics: Gears +// See Also: bevel_gear() // Description: // Returns the correct pitch angle for a bevel gear with a given number of tooth, that is // matched to another bevel gear with a (possibly different) number of teeth. @@ -218,6 +255,19 @@ function base_radius(pitch=5, teeth=11, pressure_angle=28, mod) = // drive_angle = Angle between the drive shafts of each gear. Default: 90º. // Examples: // ang = bevel_pitch_angle(teeth=18, mate_teeth=30); +// Example(2D): +// t1 = 13; t2 = 19; pitch=5; +// pang = bevel_pitch_angle(teeth=t1, mate_teeth=t2, drive_angle=90); +// color("black") { +// zrot_copies([0,pang]) +// stroke([[0,0,0], [0,-20,0]],width=0.2); +// stroke(arc(r=3, angle=[270,270+pang]),width=0.2); +// } +// #bevel_gear( +// pitch=5, teeth=t1, mate_teeth=t2, +// spiral_angle=0, cutter_radius=1000, +// slices=12, anchor="apex", orient=BACK +// ); function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) = atan(sin(drive_angle)/((mate_teeth/teeth)+cos(drive_angle))); @@ -225,6 +275,8 @@ function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) = // Function: worm_gear_thickness() // Usage: // thick = worm_gear_thickness(pitch|mod, teeth, worm_diam, , , ); +// Topics: Gears +// See Also: worm(), worm_gear() // Description: // Calculate the thickness of the worm gear. // Arguments: @@ -238,6 +290,20 @@ function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) = // Examples: // thick = worm_gear_thickness(pitch=5, teeth=36, worm_diam=30); // thick = worm_gear_thickness(mod=2, teeth=28, worm_diam=25); +// Example(2D): +// pitch = 5; teeth=17; +// worm_diam = 30; starts=2; +// y = worm_gear_thickness(pitch=pitch, teeth=teeth, worm_diam=worm_diam); +// #worm_gear( +// pitch=pitch, teeth=teeth, +// worm_diam=worm_diam, +// worm_starts=starts, +// orient=BACK +// ); +// color("black") { +// ycopies(y) stroke([[-25,0],[25,0]], width=0.5); +// stroke([[-20,-y/2],[-20,y/2]],width=0.5,endcaps="arrow"); +// } function worm_gear_thickness(pitch=5, teeth=30, worm_diam=30, worm_arc=60, crowning=1, clearance, mod) = let( pitch = is_undef(mod) ? pitch : pitch_value(mod), @@ -264,6 +330,8 @@ function _gear_q7(f,r,b,r2,t,s) = _gear_q6(b,s,t,(1-f)*max(b,r)+f*r2); // // gear_tooth_profile(pitch|mod, teeth, , , , , ); // Usage: As Function // path = gear_tooth_profile(pitch|mod, teeth, , , , , ); +// Topics: Gears +// See Also: spur_gear2d() // Description: // When called as a function, returns the 2D profile path for an individual gear tooth. // When called as a module, creates the 2D profile shape for an individual gear tooth. @@ -282,9 +350,14 @@ function _gear_q7(f,r,b,r2,t,s) = _gear_q6(b,s,t,(1-f)*max(b,r)+f*r2); // // Example(2D): Metric Gear Tooth // gear_tooth_profile(mod=2, teeth=20, pressure_angle=20); // Example(2D): -// gear_tooth_profile(pitch=5, teeth=20, pressure_angle=20, valleys=false); +// gear_tooth_profile( +// pitch=5, teeth=20, pressure_angle=20, valleys=false +// ); // Example(2D): As a function -// stroke(gear_tooth_profile(pitch=5, teeth=20, pressure_angle=20, valleys=false), width=0.1); +// path = gear_tooth_profile( +// pitch=5, teeth=20, pressure_angle=20, valleys=false +// ); +// stroke(path, width=0.1); function gear_tooth_profile( pitch = 3, teeth = 11, @@ -352,11 +425,13 @@ module gear_tooth_profile( } -// Function&Module: gear2d() +// Function&Module: spur_gear2d() // Usage: As Module -// gear2d(pitch|mod, teeth, , , , , ); +// spur_gear2d(pitch|mod, teeth, , , , , ); // Usage: As Function -// poly = gear2d(pitch|mod, teeth, , , , , ); +// poly = spur_gear2d(pitch|mod, teeth, , , , , ); +// Topics: Gears +// See Also: spur_gear() // Description: // When called as a module, creates a 2D involute spur gear. When called as a function, returns a // 2D path for the perimeter of a 2D involute spur gear. Normally, you should just specify the @@ -375,17 +450,17 @@ module gear_tooth_profile( // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example(2D): Typical Gear Shape -// gear2d(pitch=5, teeth=20); +// spur_gear2d(pitch=5, teeth=20); // Example(2D): Metric Gear -// gear2d(mod=2, teeth=20); +// spur_gear2d(mod=2, teeth=20); // Example(2D): Lower Pressure Angle -// gear2d(pitch=5, teeth=20, pressure_angle=20); +// spur_gear2d(pitch=5, teeth=20, pressure_angle=20); // Example(2D): Partial Gear -// gear2d(pitch=5, teeth=20, hide=15, pressure_angle=20); +// spur_gear2d(pitch=5, teeth=20, hide=15, pressure_angle=20); // Example(2D): Called as a Function -// path = gear2d(pitch=8, teeth=16); +// path = spur_gear2d(pitch=8, teeth=16); // polygon(path); -function gear2d( +function spur_gear2d( pitch = 3, teeth = 11, hide = 0, @@ -419,7 +494,7 @@ function gear2d( ) reorient(anchor,spin, two_d=true, r=pr, p=pts); -module gear2d( +module spur_gear2d( pitch = 3, teeth = 11, hide = 0, @@ -432,7 +507,7 @@ module gear2d( spin = 0 ) { pitch = is_undef(mod) ? pitch : pitch_value(mod); - path = gear2d( + path = spur_gear2d( pitch = pitch, teeth = teeth, hide = hide, @@ -454,6 +529,8 @@ module gear2d( // path = rack2d(pitch|mod, teeth, height, , ); // Usage: As a Module // rack2d(pitch|mod, teeth, height, , ); +// Topics: Gears +// See Also: spur_gear2d() // Description: // This is used to create a 2D rack, which is a linear bar with teeth that a gear can roll along. // A rack can mesh with any gear that has the same `pitch` and `pressure_angle`. @@ -468,7 +545,7 @@ module gear2d( // mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// Anchors: +// Extra Anchors: // "adendum" = At the tips of the teeth, at the center of rack. // "adendum-left" = At the tips of the teeth, at the left end of the rack. // "adendum-right" = At the tips of the teeth, at the right end of the rack. @@ -568,41 +645,46 @@ module rack2d( // Section: 3D Gears and Racks -// Function&Module: gear() +// Function&Module: spur_gear() // Usage: As a Module -// gear(pitch|mod, teeth, thickness, , , , , , , , ); +// spur_gear(pitch, teeth, thickness, , , , , , , , ); +// spur_gear(mod=, teeth=, thickness=, , ...); // Usage: As a Function -// vnf = gear(pitch|mod, teeth, thickness, , , , , , , , ); +// vnf = spur_gear(pitch, teeth, thickness, , ...); +// vnf = spur_gear(mod=, teeth=, thickness=, , ...); +// Topics: Gears +// See Also: rack() // Description: -// Creates a (potentially helical) involute spur gear. The module `gear()` gives an involute spur -// gear, with reasonable defaults for all the parameters. Normally, you should just choose the -// first 4 parameters, and let the rest be default values. The module `gear()` gives a gear in the -// XY plane, centered on the origin, with one tooth centered on the positive Y axis. The -// most important is `pitch_radius()`, which tells how far apart to space gears that are meshing, -// and `outer_radius()`, which gives the size of the region filled by the gear. A gear has a "pitch +// Creates a (potentially helical) involute spur gear. The module `spur_gear()` gives an involute +// spur gear, with reasonable defaults for all the parameters. Normally, you should just choose the +// first 4 parameters, and let the rest be default values. The module `spur_gear()` gives a gear in +// the XY plane, centered on the origin, with one tooth centered on the positive Y axis. The most +// important is `pitch_radius()`, which tells how far apart to space gears that are meshing, and +// `outer_radius()`, which gives the size of the region filled by the gear. A gear has a "pitch // circle", which is an invisible circle that cuts through the middle of each tooth (though not the // exact center). In order for two gears to mesh, their pitch circles should just touch. So the // distance between their centers should be `pitch_radius()` for one, plus `pitch_radius()` for the // other, which gives the radii of their pitch circles. In order for two gears to mesh, they must -// have the same `pitch` and `pressure_angle` parameters. `pitch` gives the number of millimeters of arc around -// the pitch circle covered by one tooth and one space between teeth. The `pressure_angle` controls how flat or -// bulged the sides of the teeth are. Common values include 14.5 degrees and 20 degrees, and -// occasionally 25. Though I've seen 28 recommended for plastic gears. Larger numbers bulge out -// more, giving stronger teeth, so 28 degrees is the default here. The ratio of `teeth` for two -// meshing gears gives how many times one will make a full revolution when the the other makes one -// full revolution. If the two numbers are coprime (i.e. are not both divisible by the same number -// greater than 1), then every tooth on one gear will meet every tooth on the other, for more even -// wear. So coprime numbers of teeth are good. +// have the same `pitch` and `pressure_angle` parameters. `pitch` gives the number of millimeters +// of arc around the pitch circle covered by one tooth and one space between teeth. The +// `pressure_angle` controls how flat or bulged the sides of the teeth are. Common values include +// 14.5 degrees and 20 degrees, and occasionally 25. Though I've seen 28 recommended for plastic +// gears. Larger numbers bulge out more, giving stronger teeth, so 28 degrees is the default here. +// The ratio of `teeth` for two meshing gears gives how many times one will make a full revolution +// when the the other makes one full revolution. If the two numbers are coprime (i.e. are not both +// divisible by the same number greater than 1), then every tooth on one gear will meet every tooth +// on the other, for more even wear. So coprime numbers of teeth are good. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // teeth = Total number of teeth around the entire perimeter // thickness = Thickness of gear in mm // shaft_diam = Diameter of the hole in the center, in mm. Default: 0 (no shaft hole) +// --- // hide = Number of teeth to delete to make this only a fraction of a circle // pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. // clearance = Clearance gap at the bottom of the inter-tooth valleys. // backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle -// helical = Teeth rotate this many degrees from bottom of gear to top. 360 makes the gear a screw with each thread going around once. +// helical = Teeth are slanted around the spur gear at this angle away from the gear axis of rotation. // slices = Number of vertical layers to divide gear into. Useful for refining gears with `helical`. // scale = Scale of top of gear compared to bottom. Useful for making crown gears. // interior = If true, create a mask for difference()ing from something else. @@ -611,32 +693,41 @@ module rack2d( // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // Example: Spur Gear -// gear(pitch=5, teeth=20, thickness=8, shaft_diam=5); +// spur_gear(pitch=5, teeth=20, thickness=8, shaft_diam=5); // Example: Metric Gear -// gear(mod=2, teeth=20, thickness=8, shaft_diam=5); +// spur_gear(mod=2, teeth=20, thickness=8, shaft_diam=5); // Example: Helical Gear -// gear(pitch=5, teeth=20, thickness=10, shaft_diam=5, helical=-30, slices=12, $fa=1, $fs=1); -// Example(2D): Assembly of Gears +// spur_gear( +// pitch=5, teeth=20, thickness=10, +// shaft_diam=5, helical=-30, slices=12, +// $fa=1, $fs=1 +// ); +// Example(Anim,Frames=8,VPT=[0,30,0],VPR=[0,0,0],VPD=300): Assembly of Gears // n1 = 11; //red gear number of teeth // n2 = 20; //green gear // n3 = 5; //blue gear -// n4 = 20; //orange gear -// n5 = 8; //gray rack +// n4 = 16; //orange gear +// n5 = 9; //gray rack // pitch = 9; //all meshing gears need the same `pitch` (and the same `pressure_angle`) // thickness = 6; // hole = 3; // rack_base = 12; -// d1 =pitch_radius(pitch,n1); -// d12=pitch_radius(pitch,n1) + pitch_radius(pitch,n2); -// d13=pitch_radius(pitch,n1) + pitch_radius(pitch,n3); -// d14=pitch_radius(pitch,n1) + pitch_radius(pitch,n4); -// translate([ 0, 0, 0]) rotate([0,0, $t*360/n1]) color([1.00,0.75,0.75]) gear(pitch,n1,thickness,hole); -// translate([ 0, d12, 0]) rotate([0,0,-($t+n2/2-0*n1+1/2)*360/n2]) color([0.75,1.00,0.75]) gear(pitch,n2,thickness,hole); -// translate([ d13, 0, 0]) rotate([0,0,-($t-n3/4+n1/4+1/2)*360/n3]) color([0.75,0.75,1.00]) gear(pitch,n3,thickness,hole); -// translate([ d13, 0, 0]) rotate([0,0,-($t-n3/4+n1/4+1/2)*360/n3]) color([0.75,0.75,1.00]) gear(pitch,n3,thickness,hole); -// translate([-d14, 0, 0]) rotate([0,0,-($t-n4/4-n1/4+1/2-floor(n4/4)-3)*360/n4]) color([1.00,0.75,0.50]) gear(pitch,n4,thickness,hole,hide=n4-3); -// translate([(-floor(n5/2)-floor(n1/2)+$t+n1/2)*9, -d1+0.0, 0]) color([0.75,0.75,0.75]) rack(pitch=pitch,teeth=n5,thickness=thickness,height=rack_base,anchor=CENTER,orient=BACK); -function gear( +// r1 = pitch_radius(pitch,n1); +// r2 = pitch_radius(pitch,n2); +// r3 = pitch_radius(pitch,n3); +// r4 = pitch_radius(pitch,n4); +// r5 = pitch_radius(pitch,n5); +// a1 = $t * 360 / n1; +// a2 = -$t * 360 / n2 + 180/n2; +// a3 = -$t * 360 / n3; +// a4 = -$t * 360 / n4 - 7.5*180/n4; +// color("#f77") zrot(a1) spur_gear(pitch,n1,thickness,hole); +// color("#7f7") back(r1+r2) zrot(a2) spur_gear(pitch,n2,thickness,hole); +// color("#77f") right(r1+r3) zrot(a3) spur_gear(pitch,n3,thickness,hole); +// color("#fc7") left(r1+r4) zrot(a4) spur_gear(pitch,n4,thickness,hole,hide=n4-3); +// color("#ccc") fwd(r1) right(pitch*$t) +// rack(pitch=pitch,teeth=n5,thickness=thickness,height=rack_base,anchor=CENTER,orient=BACK); +function spur_gear( pitch = 3, teeth = 11, thickness = 6, @@ -660,7 +751,7 @@ function gear( r = root_radius(pitch, teeth, clearance, interior), twist = atan2(thickness*tan(helical),p), rgn = [ - gear2d( + spur_gear2d( pitch = pitch, teeth = teeth, pressure_angle = pressure_angle, @@ -675,7 +766,7 @@ function gear( ) reorient(anchor,spin,orient, h=thickness, r=p, p=vnf); -module gear( +module spur_gear( pitch = 3, teeth = 11, thickness = 6, @@ -699,8 +790,8 @@ module gear( twist = atan2(thickness*tan(helical),p); attachable(anchor,spin,orient, r=p, l=thickness) { difference() { - linear_extrude(height=thickness, center=true, convexity=10, twist=twist) { - gear2d( + linear_extrude(height=thickness, center=true, convexity=teeth/2, twist=twist) { + spur_gear2d( pitch = pitch, teeth = teeth, pressure_angle = pressure_angle, @@ -709,6 +800,7 @@ module gear( backlash = backlash, interior = interior ); + circle(d=shaft_diam+4); } if (shaft_diam > 0) { cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2))); @@ -725,6 +817,8 @@ module gear( // bevel_gear(pitch|mod, teeth, face_width, pitch_angle, , , , , , , , , ); // Usage: As a Function // vnf = bevel_gear(pitch|mod, teeth, face_width, pitch_angle, , , , , , , , ); +// Topics: Gears +// See Also: bevel_pitch_angle() // Description: // Creates a (potentially spiral) bevel gear. The module `bevel_gear()` gives a bevel gear, with // reasonable defaults for all the parameters. Normally, you should just choose the first 4 @@ -771,11 +865,36 @@ module gear( // "pitchbase" = At the natural height of the pitch radius of the beveled gear. // "flattop" = At the top of the flat top of the bevel gear. // Example: Beveled Gear -// bevel_gear(pitch=5, teeth=36, face_width=10, shaft_diam=5, pitch_angle=45, spiral_angle=0); +// bevel_gear( +// pitch=5, teeth=36, face_width=10, shaft_diam=5, +// pitch_angle=45, spiral_angle=0 +// ); // Example: Spiral Beveled Gear and Pinion // t1 = 16; t2 = 28; -// bevel_gear(pitch=5, teeth=t1, mate_teeth=t2, slices=12, anchor="apex", orient=FWD); -// bevel_gear(pitch=5, teeth=t2, mate_teeth=t1, left_handed=true, slices=12, anchor="apex", spin=180/t2); +// bevel_gear( +// pitch=5, teeth=t1, mate_teeth=t2, +// slices=12, anchor="apex", orient=FWD +// ); +// bevel_gear( +// pitch=5, teeth=t2, mate_teeth=t1, left_handed=true, +// slices=12, anchor="apex", spin=180/t2 +// ); +// Example(Anim,Frames=4,VPD=175): Manual Spacing of Pinion and Gear +// t1 = 14; t2 = 28; pitch=5; +// back(pitch_radius(pitch=pitch, teeth=t2)) { +// yrot($t*360/t1) +// bevel_gear( +// pitch=pitch, teeth=t1, mate_teeth=t2, shaft_diam=5, +// slices=12, orient=FWD +// ); +// } +// down(pitch_radius(pitch=pitch, teeth=t1)) { +// zrot($t*360/t2) +// bevel_gear( +// pitch=pitch, teeth=t2, mate_teeth=t1, left_handed=true, +// shaft_diam=5, slices=12, spin=180/t2 +// ); +// } function bevel_gear( pitch = 5, teeth = 20, @@ -930,7 +1049,7 @@ module bevel_gear( ]; attachable(anchor,spin,orient, r1=pr, r2=ipr, h=thickness, anchors=anchors) { difference() { - vnf_polyhedron(vnf, convexity=teeth); + vnf_polyhedron(vnf, convexity=teeth/2); if (shaft_diam > 0) { cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2))); } @@ -942,9 +1061,13 @@ module bevel_gear( // Function&Module: rack() // Usage: As a Module -// rack(pitch|mod, teeth, thickness, height, , ); +// rack(pitch, teeth, thickness, height, , ); +// rack(mod=, teeth=, thickness=, height=, , =); // Usage: As a Function -// vnf = rack(pitch|mod, teeth, thickness, height, , ); +// vnf = rack(pitch, teeth, thickness, height, , ); +// vnf = rack(mod=, teeth=, thickness=, height=, , ); +// Topics: Gears +// See Also: spur_gear() // Description: // This is used to create a 3D rack, which is a linear bar with teeth that a gear can roll along. // A rack can mesh with any gear that has the same `pitch` and `pressure_angle`. @@ -955,14 +1078,16 @@ module bevel_gear( // teeth = Total number of teeth along the rack. Default: 20 // thickness = Thickness of rack in mm (affects each tooth). Default: 5 // height = Height of rack in mm, from tooth top to back of rack. Default: 10 +// --- // pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 28 // 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. +// helical = The angle of the rack teeth away from perpendicular to the rack length. Used to match helical spur gear pinions. Default: 0 // mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Anchors: +// Extra Anchors: // "adendum" = At the tips of the teeth, at the center of rack. // "adendum-left" = At the tips of the teeth, at the left end of the rack. // "adendum-right" = At the tips of the teeth, at the right end of the rack. @@ -973,10 +1098,21 @@ module bevel_gear( // "dedendum-right" = At the base of the teeth, at the right end of the rack. // "dedendum-back" = At the base of the teeth, at the back of the rack. // "dedendum-front" = At the base of the teeth, at the front of the rack. -// Example: +// Example(VPR=[60,0,325],VPD=130): // rack(pitch=5, teeth=10, thickness=5, height=5, pressure_angle=20); +// Example: Rack for Helical Gear +// rack(pitch=5, teeth=10, thickness=5, height=5, pressure_angle=20, helical=30); +// Example: Alternate Helical Gear +// rack(pitch=5, teeth=10, thickness=5, height=5, pressure_angle=20, helical=-30); // Example: Metric Rack // rack(mod=2, teeth=10, thickness=5, height=5, pressure_angle=20); +// Example(Anim,VPT=[0,0,12],VPD=100,Frames=6): Rack and Pinion +// teeth1 = 16; teeth2 = 16; +// pitch = 5; thick = 5; helical = 30; +// pr = pitch_radius(pitch=pitch, teeth=teeth2); +// right(pr*2*PI/teeth2*$t) rack(pitch=pitch, teeth=teeth1, thickness=thick, height=5, helical=helical); +// up(pr) yrot(186.5-$t*360/teeth2) +// spur_gear(pitch=pitch, teeth=teeth2, thickness=thick, helical=helical, shaft_diam=5, orient=BACK); module rack( pitch = 5, teeth = 20, @@ -985,6 +1121,7 @@ module rack( pressure_angle = 28, backlash = 0.0, clearance, + helical=0, mod, anchor = CENTER, spin = 0, @@ -1007,7 +1144,7 @@ module rack( anchorpt("dedendum-back", [0, thickness/2,-d], UP), ]; attachable(anchor,spin,orient, size=[l, thickness, 2*abs(a-height)], anchors=anchors) { - xrot(90) { + skew(sxy=tan(helical)) xrot(90) { linear_extrude(height=thickness, center=true, convexity=teeth*2) { rack2d( pitch = pitch, @@ -1032,6 +1169,7 @@ function rack( pressure_angle = 28, backlash = 0.0, clearance, + helical=0, mod, anchor = CENTER, spin = 0, @@ -1062,8 +1200,9 @@ function rack( backlash = backlash, clearance = clearance ), - vnf = linear_sweep(path, height=thickness, anchor="origin", orient=FWD) - ) reorient(anchor,spin,orient, size=[l, thickness, 2*abs(a-height)], anchors=anchors, p=vnf); + vnf = linear_sweep(path, height=thickness, anchor="origin", orient=FWD), + out = helical==0? vnf : skew(sxy=tan(helical), p=vnf) + ) reorient(anchor,spin,orient, size=[l, thickness, 2*abs(a-height)], anchors=anchors, p=out); @@ -1072,8 +1211,10 @@ function rack( // worm(pitch|mod, d, l, , , , , ); // Usage: As a Function // vnf = worm(pitch|mod, d, l, , , , , ); +// Topics: Gears +// See Also: worm_gear() // Description: -// Creates a worm shape that can be matched to a work gear. +// Creates a worm shape that can be matched to a worm gear. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 // d = The diameter of the worm. Default: 30 @@ -1127,7 +1268,7 @@ function worm( ], maxang = 360 / segs(d/2), refined_polars = [ - for (i=idx(polars,end=-2)) let( + for (i=idx(polars,e=-2)) let( delta = polars[i+1].x - polars[i].x, steps = ceil(delta/maxang), step = delta/steps @@ -1146,7 +1287,8 @@ function worm( ) apply(zrot(a)*up(z), path3d(cross_sect)) ], - vnf1 = vnf_vertex_array(profiles, caps=true, col_wrap=true, reverse=true, style="alt"), + rprofiles = [ for (prof=profiles) reverse(prof) ], + vnf1 = vnf_vertex_array(rprofiles, caps=true, col_wrap=true, style="quincunx"), vnf = left_handed? xflip(p=vnf1) : vnf1 ) reorient(anchor,spin,orient, d=d, l=l, p=vnf); @@ -1175,7 +1317,7 @@ module worm( mod=mod ); attachable(anchor,spin,orient, d=d, l=l) { - vnf_polyhedron(vnf); + vnf_polyhedron(vnf, convexity=ceil(l/pitch)*2); children(); } } @@ -1186,6 +1328,8 @@ module worm( // worm_gear(pitch|mod, teeth, worm_diam, , , , , , , , ); // Usage: As a Function // vnf = worm_gear(pitch|mod, teeth, worm_diam, , , , , , , ); +// Topics: Gears +// See Also: worm() // Description: // Creates a worm gear to match with a worm. // Arguments: @@ -1211,7 +1355,27 @@ module worm( // Example: Multiple Starts // worm_gear(pitch=5, teeth=36, worm_diam=30, worm_starts=4); // Example: Metric Worm Gear -// worm_gear(mod=25, teeth=32, worm_diam=30, worm_starts=1); +// worm_gear(mod=2, teeth=32, worm_diam=30, worm_starts=1); +// Example(Anim,Frames=4,FrameMS=125,VPD=220,VPT=[-15,0,0]): Meshing Worm and Gear +// $fn=36; +// pitch = 5; starts = 4; +// worm_diam = 30; worm_length = 50; +// gear_teeth=36; +// right(worm_diam/2) +// yrot($t*360/starts) +// worm(d=worm_diam, l=worm_length, pitch=pitch, starts=starts, orient=BACK); +// left(pitch_radius(pitch=pitch, teeth=gear_teeth)) +// zrot(-$t*360/gear_teeth) +// worm_gear(pitch=pitch, teeth=gear_teeth, worm_diam=worm_diam, worm_starts=starts); +// Example: Meshing Worm and Gear Metricly +// $fn = 72; +// modulus = 2; starts = 3; +// worm_diam = 30; worm_length = 50; +// gear_teeth=36; +// right(worm_diam/2) +// worm(d=worm_diam, l=worm_length, mod=modulus, starts=starts, orient=BACK); +// left(pitch_radius(mod=modulus, teeth=gear_teeth)) +// worm_gear(mod=modulus, teeth=gear_teeth, worm_diam=worm_diam, worm_starts=starts); // Example: Called as Function // vnf = worm_gear(pitch=8, teeth=30, worm_diam=30, worm_starts=1); // vnf_polyhedron(vnf); @@ -1257,7 +1421,7 @@ function worm_gear( tp = [0,r1,0] - spherical_to_xyz(r2, 90, 90+zang), zang2 = u * helical ) [ - for (i = [0:1:teeth]) each + for (i = [0:1:teeth-1]) each apply( zrot(-i*360/teeth+zang2) * move(tp) * @@ -1272,7 +1436,7 @@ function worm_gear( face_pts = len(tooth_profile), gear_pts = face_pts * teeth, top_faces =[ - for (i=[0:1:teeth-1], j=[0:1:(face_pts/2)-1]) each [ + for (i=[0:1:teeth-1], j=[0:1:(face_pts/2)-2]) each [ [i*face_pts+j, (i+1)*face_pts-j-1, (i+1)*face_pts-j-2], [i*face_pts+j, (i+1)*face_pts-j-2, i*face_pts+j+1] ], @@ -1333,7 +1497,7 @@ module worm_gear( thickness = pointlist_bounds(vnf[0])[1].z; attachable(anchor,spin,orient, r=p, l=thickness) { difference() { - vnf_polyhedron(vnf); + vnf_polyhedron(vnf, convexity=teeth/2); if (shaft_diam > 0) { cylinder(d=shaft_diam, l=worm_diam, center=true); } diff --git a/geometry.scad b/geometry.scad index b99290fd..b145c6f0 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: geometry.scad // Geometry helpers. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// @@ -295,21 +293,21 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) circle(r=1,$fn=12); // color("red") translate(p2) circle(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // line = [[-30,-15,0],[30,15,30]]; // pt = [5,5,5]; // p2 = line_closest_point(line,pt); // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // line = [[-30,-15,0],[30,15,30]]; // pt = [-35,-15,0]; // p2 = line_closest_point(line,pt); // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // line = [[-30,-15,0],[30,15,30]]; // pt = [40,15,25]; // p2 = line_closest_point(line,pt); @@ -353,21 +351,21 @@ function line_closest_point(line,pt) = // stroke(ray, endcap2="arrow2"); // color("blue") translate(pt) circle(r=1,$fn=12); // color("red") translate(p2) circle(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // ray = [[-30,-15,0],[30,15,30]]; // pt = [5,5,5]; // p2 = ray_closest_point(ray,pt); // stroke(ray, endcap2="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // ray = [[-30,-15,0],[30,15,30]]; // pt = [-35,-15,0]; // p2 = ray_closest_point(ray,pt); // stroke(ray, endcap2="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // ray = [[-30,-15,0],[30,15,30]]; // pt = [40,15,25]; // p2 = ray_closest_point(ray,pt); @@ -416,21 +414,21 @@ function ray_closest_point(ray,pt) = // stroke(seg); // color("blue") translate(pt) circle(r=1,$fn=12); // color("red") translate(p2) circle(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // seg = [[-30,-15,0],[30,15,30]]; // pt = [5,5,5]; // p2 = segment_closest_point(seg,pt); // stroke(seg); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // seg = [[-30,-15,0],[30,15,30]]; // pt = [-35,-15,0]; // p2 = segment_closest_point(seg,pt); // stroke(seg); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // seg = [[-30,-15,0],[30,15,30]]; // pt = [40,15,25]; // p2 = segment_closest_point(seg,pt); @@ -479,9 +477,9 @@ function line_from_points(points, fast=false, eps=EPSILON) = // C = law_of_cosines(a, b, c); // c = law_of_cosines(a, b, C); // Description: -// Applies the Law of Cosines for an arbitrary triangle. -// Given three side lengths, returns the angle in degrees for the corner opposite of the third side. -// Given two side lengths, and the angle between them, returns the length of the third side. +// Applies the Law of Cosines for an arbitrary triangle. Given three side lengths, returns the +// angle in degrees for the corner opposite of the third side. Given two side lengths, and the +// angle between them, returns the length of the third side. // Figure(2D): // stroke([[-50,0], [10,60], [50,0]], closed=true); // color("black") { @@ -512,9 +510,10 @@ function law_of_cosines(a, b, c, C) = // B = law_of_sines(a, A, b); // b = law_of_sines(a, A, B); // Description: -// Applies the Law of Sines for an arbitrary triangle. -// Given two triangle side lengths and the angle between them, returns the angle of the corner opposite of the second side. -// Given a side length, the opposing angle, and a second angle, returns the length of the side opposite of the second angle. +// Applies the Law of Sines for an arbitrary triangle. Given two triangle side lengths and the +// angle between them, returns the angle of the corner opposite of the second side. Given a side +// length, the opposing angle, and a second angle, returns the length of the side opposite of the +// second angle. // Figure(2D): // stroke([[-50,0], [10,60], [50,0]], closed=true); // color("black") { @@ -551,7 +550,7 @@ function law_of_sines(a, A, b, B) = // This is certainly more verbose and slower than writing your own calculations, but has the nice // benefit that you can just specify the info you have, and don't have to figure out which trig // formulas you need to use. -// Figure(2D): +// Figure(2D,NoAxes): // color("#ccc") { // stroke(closed=false, width=0.5, [[45,0], [45,5], [50,5]]); // stroke(closed=false, width=0.5, arc(N=6, r=15, cp=[0,0], start=0, angle=30)); @@ -1009,17 +1008,26 @@ function plane_transform(plane) = // Function: projection_on_plane() // Usage: -// projection_on_plane(points); +// pts = projection_on_plane(plane, points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal -// projection of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or +// 3d points, return the 3D orthogonal projection of the points on the plane. +// In other words, for every point given, returns the closest point to it on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project -// Example(3D): -// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100)))); -// plane = plane3pt([1,0,0],[0,1,0],[0,0,1]); +// Example(FlatSpin,VPD=500,VPT=[2,20,10]): +// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100, $fn=36)))); +// plane = plane_from_normal([1,0,1]); // proj = projection_on_plane(plane,points); +// color("red") move_copies(points) sphere(d=2,$fn=12); +// color("blue") move_copies(proj) sphere(d=2,$fn=12); +// move(centroid(proj)) { +// rot(from=UP,to=plane_normal(plane)) { +// anchor_arrow(30); +// %cube([120,150,0.1],center=true); +// } +// } function projection_on_plane(plane, points) = assert( _valid_plane(plane), "Invalid plane." ) assert( is_path(points), "Invalid list of points or dimension." ) @@ -1064,26 +1072,6 @@ function distance_from_plane(plane, point) = let( plane = normalize_plane(plane) ) point3d(plane)* point - plane[3]; - -// Function: closest_point_on_plane() -// Usage: -// pt = closest_point_on_plane(plane, point); -// Description: -// Takes a point, and a plane [A,B,C,D] where the equation of that plane is `Ax+By+Cz=D`. -// Returns the coordinates of the closest point on that plane to the given `point`. -// Arguments: -// plane = The [A,B,C,D] coefficients for the plane equation `Ax+By+Cz=D`. -// point = The 3D point to find the closest point to. -function closest_point_on_plane(plane, point) = - assert( _valid_plane(plane), "Invalid input plane." ) - assert( is_vector(point,3), "Invalid point." ) - let( plane = normalize_plane(plane), - n = point3d(plane), - d = n*point - plane[3] // distance from plane - ) - point - n*d; - - // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -1144,7 +1132,7 @@ function plane_line_angle(plane, line) = function plane_line_intersection(plane, line, bounded=false, eps=EPSILON) = assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) assert(_valid_plane(plane,eps=eps) && _valid_line(line,dim=3,eps=eps), "Invalid plane and/or line.") - assert(is_bool(bounded) || (is_list(bounded) && len(bounded)==2), "Invalid bound condition(s).") + assert(is_bool(bounded) || is_bool_list(bounded,2), "Invalid bound condition.") let( bounded = is_list(bounded)? bounded : [bounded, bounded], res = _general_plane_line_intersection(plane, line, eps=eps) @@ -1501,7 +1489,7 @@ module circle_3points(pt1, pt2, pt3, h, center=false) { // d = Diameter of the circle. // cp = The coordinates of the 2d circle centerpoint. // pt = The coordinates of the 2d external point. -// Example: +// Example(3D): // cp = [-10,-10]; r = 30; pt = [30,10]; // tanpts = circle_point_tangents(r=r, cp=cp, pt=pt); // color("yellow") translate(cp) circle(r=r); @@ -1517,7 +1505,7 @@ function circle_point_tangents(r, d, cp, pt) = dist = norm(delta), baseang = atan2(delta.y,delta.x) ) dist < r? [] : - approx(dist,r)? [[baseang, pt]] : + approx(dist,r)? [pt] : let( relang = acos(r/dist), angs = [baseang + relang, baseang - relang] @@ -1607,6 +1595,47 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = +// Function: circle_line_intersection() +// Usage: +// isect = circle_line_intersection(c,r,line,,); +// isect = circle_line_intersection(c,d,line,,); +// Description: +// Find intersection points between a 2d circle and a line, ray or segment specified by two points. +// By default the line is unbounded. +// Arguments: +// c = center of circle +// r = radius of circle +// line = two points defining the unbounded line +// bounded = false for unbounded line, true for a segment, or a vector [false,true] or [true,false] to specify a ray with the first or second end unbounded. Default: false +// eps = epsilon used for identifying the case with one solution. Default: 1e-9 +// --- +// d = diameter of circle +function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = + let(r=get_radius(r=r,d=d,dflt=undef)) + assert(_valid_line(line,2), "Input 'line' is not a valid 2d line.") + assert(is_vector(c,2), "Circle center must be a 2-vector") + assert(is_num(r) && r>0, "Radius must be positive") + assert(is_bool(bounded) || is_bool_list(bounded,2), "Invalid bound condition") + let( + bounded = force_list(bounded,2), + closest = line_closest_point(line,c), + d = norm(closest-c) + ) + d > r ? [] : + let( + isect = approx(d,r,eps) ? [closest] : + let( offset = sqrt(r*r-d*d), + uvec=unit(line[1]-line[0]) + ) [closest-offset*uvec, closest+offset*uvec] + + ) + [for(p=isect) + if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) + && (!bounded[1] || (p-line[1])*(line[0]-line[1])>=0)) p]; + + + + // Section: Pointlists @@ -2040,7 +2069,7 @@ function _split_polygon_at_x(poly, x) = ) (min(xs) >= x || max(xs) <= x)? [poly] : let( poly2 = [ - for (p = pair_wrap(poly)) each [ + for (p = pair(poly,true)) each [ p[0], if( (p[0].x < x && p[1].x > x) || @@ -2070,7 +2099,7 @@ function _split_polygon_at_y(poly, y) = ) (min(ys) >= y || max(ys) <= y)? [poly] : let( poly2 = [ - for (p = pair_wrap(poly)) each [ + for (p = pair(poly,true)) each [ p[0], if( (p[0].y < y && p[1].y > y) || @@ -2100,7 +2129,7 @@ function _split_polygon_at_z(poly, z) = ) (min(zs) >= z || max(zs) <= z)? [poly] : let( poly2 = [ - for (p = pair_wrap(poly)) each [ + for (p = pair(poly,true)) each [ p[0], if( (p[0].z < z && p[1].z > z) || diff --git a/githooks/pre-merge-commit b/githooks/pre-merge-commit deleted file mode 100755 index 28c01395..00000000 --- a/githooks/pre-merge-commit +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -VERFILE="version.scad" - -vernums=$(grep ^BOSL_VERSION "$VERFILE" | sed 's/^.*[[]\([0-9,]*\)[]].*$/\1/') -major=$(echo "$vernums" | awk -F, '{print $1}') -minor=$(echo "$vernums" | awk -F, '{print $2}') -revision=$(echo "$vernums" | awk -F, '{print $3}') - -newrev=$(($revision+1)) -echo "Current Version: $major.$minor.$revision" -echo "New Version: $major.$minor.$newrev" - -sed -i '' 's/^BOSL_VERSION = .*$/BOSL_VERSION = ['"$major,$minor,$newrev];/g" $VERFILE - -exec git add version.scad - diff --git a/hingesnaps.scad b/hingesnaps.scad index de6f3c6b..34d0ed14 100644 --- a/hingesnaps.scad +++ b/hingesnaps.scad @@ -1,13 +1,12 @@ ////////////////////////////////////////////////////////////////////// // LibFile: hingesnaps.scad // Useful hinge mask and snaps shapes. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// +// Section: Hinges and Snaps // Module: folding_hinge_mask() // Usage: @@ -130,10 +129,8 @@ module snap_socket(thick, snaplen=5, snapdiam=5, layerheight=0.2, foldangle=90, // apply_folding_hinges_and_snaps( // thick=3, foldangle=54.74, // hinges=[ -// for (a=[0,120,240]) each [ -// [100, rot(a,p=[ size/4, 0 ]), a+90], -// [100, rot(a,p=[-size/2,-size/2.33]), a+90], -// [100, rot(a,p=[-size/2, size/2.33]), a+90] +// for (a=[0,120,240], b=[-size/2,size/4]) each [ +// [200, polar_to_xy(b,a), a+90] // ] // ], // snaps=[ @@ -149,10 +146,11 @@ module snap_socket(thick, snaplen=5, snapdiam=5, layerheight=0.2, foldangle=90, // ] // ] // ) { +// $fn=3; // difference() { -// cylinder(r=size-1, h=3, spin=180, $fn=3); -// down(0.01) cylinder(r=size/4, h=3.1, spin=0, $fn=3); -// down(0.01) for (a=[0:120:359.9]) zrot(a) right(size/2) cylinder(r=size/4, h=3.1, spin=180, $fn=3); +// cylinder(r=size-1, h=3); +// down(0.01) cylinder(r=size/4.5, h=3.1, spin=180); +// down(0.01) for (a=[0:120:359.9]) zrot(a) right(size/2) cylinder(r=size/4.5, h=3.1); // } // } module apply_folding_hinges_and_snaps(thick, foldangle=90, hinges=[], snaps=[], sockets=[], snaplen=5, snapdiam=5, hingegap=undef, layerheight=0.2) diff --git a/hull.scad b/hull.scad index 5ca23e17..5658becf 100644 --- a/hull.scad +++ b/hull.scad @@ -1,13 +1,11 @@ ////////////////////////////////////////////////////////////////////// // LibFile: hull.scad // Functions to create 2D and 3D convex hulls. -// To use, add the following line to the beginning of your file: -// ``` -// include -// include -// ``` // Derived from Oskar Linde's Hull: // - https://github.com/openscad/scad-utils +// Includes: +// include +// include ////////////////////////////////////////////////////////////////////// @@ -76,6 +74,15 @@ module hull_points(points, fast=false) { } + +function _backtracking(i,points,h,t,m) = + m 0, - polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]] - ) _hull2d_iterative(points, polygon, remaining); - - - -// Adds the remaining points one by one to the convex hull -function _hull2d_iterative(points, polygon, remaining, _i=0) = - (_i >= len(remaining))? polygon : let ( - // pick a point - i = remaining[_i], - // find the segments that are in conflict with the point (point not inside) - conflicts = _find_conflicting_segments(points, polygon, points[i]) - // no conflicts, skip point and move on - ) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let( - // find the first conflicting segment and the first not conflicting - // conflict will be sorted, if not wrapping around, do it the easy way - polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i) - ) _hull2d_iterative(points, polygon, remaining, _i+1); - + len(points) < 2 ? [] : + let( n = len(points), + ip = sortidx(points) ) + // lower hull points + let( lh = + [ for( i = 2, + k = 2, + h = [ip[0],ip[1]]; // current list of hull point indices + i <= n; + k = i= -1; + k = i>=0 ? _backtracking(ip[i],points,h,t,k)+1 : k, + h = [for(j=[0:1:k-2]) h[j], if(i>0) ip[i]], + i = i-1 + ) if( i==-1 ) h ][0] ; + function _hull_collinear(points) = let( a = points[0], - n = points[1] - a, + i = max_index([for(pt=points) norm(pt-a)]), + n = points[i] - a + ) + norm(n)==0 ? [0] + : + let( points1d = [ for(p = points) (p-a)*n ], min_i = min_index(points1d), max_i = max_index(points1d) ) [min_i, max_i]; -function _find_conflicting_segments(points, polygon, point) = [ - for (i = [0:1:len(polygon)-1]) let( - j = (i+1) % len(polygon), - p1 = points[polygon[i]], - p2 = points[polygon[j]], - area = triangle_area(p1, p2, point) - ) if (area < 0) i -]; - - -// remove the conflicting segments from the polygon -function _remove_conflicts_and_insert_point(polygon, conflicts, point) = - (conflicts[0] == 0)? let( - nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ], - new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)), - polygon = concat([ for (i = new_indices) polygon[i] ], point) - ) polygon : let( - before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ], - after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ], - polygon = concat(before_conflicts, point, after_conflicts) - ) polygon; - - // Function: hull3d_faces() // Usage: diff --git a/images/BOSL2logo.png b/images/BOSL2logo.png new file mode 100644 index 00000000..95eea744 Binary files /dev/null and b/images/BOSL2logo.png differ diff --git a/joiners.scad b/joiners.scad index 38594a84..8c0625d9 100644 --- a/joiners.scad +++ b/joiners.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: joiners.scad // Snap-together joiners. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -30,7 +28,7 @@ include // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // Example: -// half_joiner_clear(spin=-90); +// half_joiner_clear(); module half_joiner_clear(h=20, w=10, a=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP) { dmnd_height = h*1.0; @@ -76,8 +74,9 @@ module half_joiner_clear(h=20, w=10, a=30, clearance=0, overlap=0.01, anchor=CEN // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // $slop = Printer specific slop value to make parts fit more closely. -// Example: -// half_joiner(screwsize=3, spin=-90); +// Examples(FlatSpin,VPD=75): +// half_joiner(screwsize=3); +// half_joiner(h=20,w=10,l=10); module half_joiner(h=20, w=10, l=10, a=30, screwsize=undef, guides=true, anchor=CENTER, spin=0, orient=UP) { dmnd_height = h*1.0; @@ -135,7 +134,6 @@ module half_joiner(h=20, w=10, l=10, a=30, screwsize=undef, guides=true, anchor= children(); } } -//half_joiner(screwsize=3); @@ -154,8 +152,9 @@ module half_joiner(h=20, w=10, l=10, a=30, screwsize=undef, guides=true, anchor= // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Example: -// half_joiner2(screwsize=3, spin=-90); +// Examples(FlatSpin,VPD=75): +// half_joiner2(screwsize=3); +// half_joiner2(h=20,w=10,l=10); module half_joiner2(h=20, w=10, l=10, a=30, screwsize=undef, guides=true, anchor=CENTER, spin=0, orient=UP) { dmnd_height = h*1.0; @@ -203,7 +202,7 @@ module half_joiner2(h=20, w=10, l=10, a=30, screwsize=undef, guides=true, anchor // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // Example: -// joiner_clear(spin=-90); +// joiner_clear(); module joiner_clear(h=40, w=10, a=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP) { dmnd_height = h*0.5; @@ -238,9 +237,9 @@ module joiner_clear(h=40, w=10, a=30, clearance=0, overlap=0.01, anchor=CENTER, // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // $slop = Printer specific slop value to make parts fit more closely. -// Examples: -// joiner(screwsize=3, spin=-90); -// joiner(w=10, l=10, h=40, spin=-90) cuboid([10, 10*2, 40], anchor=RIGHT); +// Examples(FlatSpin,VPD=125): +// joiner(screwsize=3); +// joiner(w=10, l=10, h=40); module joiner(h=40, w=10, l=10, a=30, screwsize=undef, guides=true, anchor=CENTER, spin=0, orient=UP) { attachable(anchor,spin,orient, size=[w, 2*l, h]) { @@ -312,12 +311,12 @@ module joiner_pair_clear(spacing=100, h=40, w=10, a=30, n=2, clearance=0, overla // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // $slop = Printer specific slop value to make parts fit more closely. +// Example(FlatSpin,VPD=200): +// joiner_pair(spacing=50, l=10); // Examples: -// joiner_pair(spacing=50, l=10, spin=-90) cuboid([10, 50+10-0.1, 40], anchor=RIGHT); -// joiner_pair(spacing=50, l=10, n=2, spin=-90); -// joiner_pair(spacing=50, l=10, n=3, alternate=false, spin=-90); -// joiner_pair(spacing=50, l=10, n=3, alternate=true, spin=-90); -// joiner_pair(spacing=50, l=10, n=3, alternate="alt", spin=-90); +// joiner_pair(spacing=50, l=10, n=3, alternate=false); +// joiner_pair(spacing=50, l=10, n=3, alternate=true); +// joiner_pair(spacing=50, l=10, n=3, alternate="alt"); module joiner_pair(spacing=100, h=40, w=10, l=10, a=30, n=2, alternate=true, screwsize=undef, guides=true, anchor=CENTER, spin=0, orient=UP) { attachable(anchor,spin,orient, size=[spacing+w, 2*l, h]) { @@ -387,19 +386,19 @@ module joiner_quad_clear(xspacing=undef, yspacing=undef, spacing1=undef, spacing // l = Length of the backing to the joiners. // a = Overhang angle of the joiners. // n = Number of joiners in a row. Default: 2 -// alternate = If true (default), each joiner alternates it's orientation. If alternate is "alt", do opposite alternating orientations. +// alternate = If true (default), joiners on each side alternate orientations. If alternate is "alt", do opposite alternating orientations. // screwsize = Diameter of screwhole. // guides = If true, create sliding alignment guides. // $slop = Printer specific slop value to make parts fit more closely. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Example(FlatSpin,VPD=250): +// joiner_quad(spacing1=50, spacing2=50, l=10); // Examples: -// joiner_quad(spacing1=50, spacing2=50, l=10, spin=-90) cuboid([50, 50+10-0.1, 40]); -// joiner_quad(spacing1=50, spacing2=50, l=10, n=2, spin=-90); -// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate=false, spin=-90); -// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate=true, spin=-90); -// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate="alt", spin=-90); +// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate=false); +// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate=true); +// joiner_quad(spacing1=50, spacing2=50, l=10, n=3, alternate="alt"); module joiner_quad(spacing1=undef, spacing2=undef, xspacing=undef, yspacing=undef, h=40, w=10, l=10, a=30, n=2, alternate=true, screwsize=undef, guides=true, anchor=CENTER, spin=0, orient=UP) { spacing1 = first_defined([spacing1, xspacing, 100]); @@ -407,7 +406,7 @@ module joiner_quad(spacing1=undef, spacing2=undef, xspacing=undef, yspacing=unde attachable(anchor,spin,orient, size=[w+spacing1, spacing2, h]) { zrot_copies(n=2) { back(spacing2/2) { - joiner_pair(spacing=spacing1, n=n, h=h, w=w, l=l, a=a, screwsize=screwsize, guides=guides); + joiner_pair(spacing=spacing1, n=n, h=h, w=w, l=l, a=a, screwsize=screwsize, guides=guides, alternate=alternate); } } children(); @@ -420,7 +419,7 @@ module joiner_quad(spacing1=undef, spacing2=undef, xspacing=undef, yspacing=unde // Module: dovetail() // // Usage: -// dovetail(l|length, h|height, w|width, slope|angle, taper|back_width, [chamfer], [r|radius], [round], [$slop]) +// dovetail(gender, w|width, h|height, slide, [slope|angle], [taper|back_width], [chamfer], [r|radius], [round], [$slop]) // // Description: // Produces a possibly tapered dovetail joint shape to attach to or subtract from two parts you wish to join together. @@ -428,13 +427,16 @@ module joiner_quad(spacing1=undef, spacing2=undef, xspacing=undef, yspacing=unde // it is fully closed, and then wedges tightly. You can chamfer or round the corners of the dovetail shape for better // printing and assembly, or choose a fully rounded joint that looks more like a puzzle piece. The dovetail appears // parallel to the Y axis and projecting upwards, so in its default orientation it will slide together with a translation -// in the positive Y direction. The default anchor for dovetails is BOTTOM; the default orientation depends on the gender, -// with male dovetails oriented UP and female ones DOWN. +// in the positive Y direction. The gender determines whether the shape is meant to be added to your model or +// differenced, and it also changes the anchor and orientation. The default anchor for dovetails is BOTTOM; +// the default orientation depends on the gender, with male dovetails oriented UP and female ones DOWN. // // Arguments: -// l / length = Length of the dovetail (amount the joint slides during assembly) -// h / height = Height of the dovetail +// gender = A string, "male" or "female", to specify the gender of the dovetail. // w / width = Width (at the wider, top end) of the dovetail before tapering +// h / height = Height of the dovetail (the amount it projects from its base) +// slide = Distance the dovetail slides when you assemble it (length of sliding dovetails, thickness of regular dovetails) +// --- // slope = slope of the dovetail. Standard woodworking slopes are 4, 6, or 8. Default: 6. // angle = angle (in degrees) of the dovetail. Specify only one of slope and angle. // taper = taper angle (in degrees). Dovetail gets narrower by this angle. Default: no taper @@ -444,68 +446,66 @@ module joiner_quad(spacing1=undef, spacing2=undef, xspacing=undef, yspacing=unde // round = true to round both corners of the dovetail and give it a puzzle piece look. Default: false. // extra = amount of extra length and base extension added to dovetails for unions and differences. Default: 0.01 // Example: Ordinary straight dovetail, male version (sticking up) and female version (below the xy plane) -// dovetail("male", length=30, width=15, height=8); -// right(20) dovetail("female", length=30, width=15, height=8); +// dovetail("male", width=15, height=8, slide=30); +// right(20) dovetail("female", width=15, height=8, slide=30); // Example: Adding a 6 degree taper (Such a big taper is usually not necessary, but easier to see for the example.) -// dovetail("male", length=30, width=15, height=8, taper=6); -// right(20) dovetail("female", length=30, width=15, height=8, taper=6); +// dovetail("male", w=15, h=8, slide=30, taper=6); +// right(20) dovetail("female", 15, 8, 30, taper=6); // Same as above // Example: A block that can link to itself // diff("remove") // cuboid([50,30,10]){ -// attach(BACK) dovetail("male", length=10, width=15, height=8); -// attach(FRONT) dovetail("female", length=10, width=15, height=8,$tags="remove"); +// attach(BACK) dovetail("male", slide=10, width=15, height=8); +// attach(FRONT) dovetail("female", slide=10, width=15, height=8,$tags="remove"); // } // Example: Setting the dovetail angle. This is too extreme to be useful. // diff("remove") // cuboid([50,30,10]){ -// attach(BACK) dovetail("male", length=10, width=15, height=8,angle=30); -// attach(FRONT) dovetail("female", length=10, width=15, height=8,angle=30,$tags="remove"); +// attach(BACK) dovetail("male", slide=10, width=15, height=8, angle=30); +// attach(FRONT) dovetail("female", slide=10, width=15, height=8, angle=30,$tags="remove"); // } // Example: Adding a chamfer helps printed parts fit together without problems at the corners // diff("remove") // cuboid([50,30,10]){ -// attach(BACK) dovetail("male", length=10, width=15, height=8,chamfer=1); -// attach(FRONT) dovetail("female", length=10, width=15, height=8,chamfer=1,$tags="remove"); +// attach(BACK) dovetail("male", slide=10, width=15, height=8, chamfer=1); +// attach(FRONT) dovetail("female", slide=10, width=15, height=8,chamfer=1,$tags="remove"); // } // Example: Rounding the outside corners is another option // diff("remove") -// cuboid([50,30,10]){ -// attach(BACK) dovetail("male", length=10, width=15, height=8,radius=1,$fn=32); -// attach(FRONT) dovetail("female", length=10, width=15, height=8,radius=1,$tags="remove",$fn=32); -// } +// cuboid([50,30,10]) { +// attach(BACK) dovetail("male", slide=10, width=15, height=8, radius=1, $fn=32); +// attach(FRONT, overlap=-0.1) dovetail("female", slide=10, width=15, height=8, radius=1, $tags="remove", $fn=32); +// } // Example: Or you can make a fully rounded joint // $fn=32; // diff("remove") -// cuboid([50,30,10]){ -// attach(BACK) dovetail("male", length=10, width=15, height=8,radius=1.5, round=true); -// attach(FRONT) dovetail("female", length=10, width=15, height=8,radius=1.5, round=true, $tags="remove"); +// cuboid([50,30,10]){ +// attach(BACK) dovetail("male", slide=10, width=15, height=8, radius=1.5, round=true); +// attach(FRONT,overlap=-0.1) dovetail("female", slide=10, width=15, height=8, radius=1.5, round=true, $tags="remove"); // } -// Example: With a long joint like this, a taper makes the joint easy to assemble. It will go together easily and wedge tightly if you get the tolerances right. Specifying the taper with `back_width` may be easier than using a taper angle. +// Example: With a long joint like this, a taper makes the joint easy to assemble. It will go together easily and wedge tightly if you get the tolerances right. Specifying the taper with `back_width` may be easier than using a taper angle. // cuboid([50,30,10]) -// attach(TOP) dovetail("male", length=50, width=18, height=4, back_width=15, spin=90); +// attach(TOP) dovetail("male", slide=50, width=18, height=4, back_width=15, spin=90); // fwd(35) // diff("remove") // cuboid([50,30,10]) -// attach(TOP) dovetail("female", length=50, width=18, height=4, back_width=15, spin=90,$tags="remove"); -// Example: A series of dovtails +// attach(TOP) dovetail("female", slide=50, width=18, height=4, back_width=15, spin=90, $tags="remove"); +// Example: A series of dovetails forming a tail board, with the inside of the joint up. A standard wood joint would have a zero taper. // cuboid([50,30,10]) -// attach(BACK) xcopies(10,5) dovetail("male", length=10, width=7, height=4); -// Example: Mating pin board for a right angle joint. Note that the anchor method and use of `spin` ensures that the joint works even with a taper. +// attach(BACK) xcopies(10,5) dovetail("male", slide=10, width=7, taper=4, height=4); +// Example: Mating pin board for a half-blind right angle joint, where the joint only shows on the side but not the front. Note that the anchor method and use of `spin` ensures that the joint works even with a taper. // diff("remove") // cuboid([50,30,10]) -// position(TOP+BACK) xcopies(10,5) dovetail("female", length=10, width=7, taper=4, height=4, $tags="remove",anchor=BOTTOM+FRONT,spin=180); -module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, back_width, chamfer, extra=0.01, r, radius, round=false, anchor=BOTTOM, spin=0, orient) +// position(TOP+BACK) xcopies(10,5) dovetail("female", slide=10, width=7, taper=4, height=4, $tags="remove",anchor=BOTTOM+FRONT,spin=180); +module dovetail(gender, width, height, slide, h, w, angle, slope, taper, back_width, chamfer, extra=0.01, r, radius, round=false, anchor=BOTTOM, spin=0, orient) { radius = get_radius(r1=radius,r2=r); - lcount = num_defined([l,length]); hcount = num_defined([h,height]); wcount = num_defined([w,width]); - assert(lcount==1, "Must define exactly one of l and length"); - assert(wcount==1, "Must define exactly one of w and width"); + assert(is_def(slide), "Must define slide"); assert(hcount==1, "Must define exactly one of h and height"); + assert(wcount==1, "Must define exactly one of w and width"); h = first_defined([h,height]); w = first_defined([w,width]); - length = first_defined([l,length]); orient = is_def(orient) ? orient : gender == "female" ? DOWN : UP; count = num_defined([angle,slope]); @@ -519,10 +519,10 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac extra_slop = gender == "female" ? 2*$slop : 0; width = w + extra_slop; height = h + extra_slop; - back_width = back_width + extra_slop; + back_width = u_add(back_width, extra_slop); front_offset = is_def(taper) ? -extra * tan(taper) : - is_def(back_width) ? extra * (back_width-width)/length/2 : 0; + is_def(back_width) ? extra * (back_width-width)/slide/2 : 0; size = is_def(chamfer) && chamfer>0 ? chamfer : is_def(radius) && radius>0 ? radius : 0; @@ -533,7 +533,7 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac smallend_half = round_corners( move( - [0,-length/2-extra,0], + [0,-slide/2-extra,0], p=[ [0 , 0, height], [width/2-front_offset , 0, height], @@ -544,13 +544,13 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac method=type, cut = fullsize, closed=false ); smallend_points = concat(select(smallend_half, 1, -2), [down(extra,p=select(smallend_half, -2))]); - offset = is_def(taper) ? -(length+extra) * tan(taper) : + offset = is_def(taper) ? -(slide+extra) * tan(taper) : is_def(back_width) ? (back_width-width) / 2 : 0; - bigend_points = move([offset,length+2*extra,0], p=smallend_points); + bigend_points = move([offset,slide+2*extra,0], p=smallend_points); - adjustment = gender == "male" ? -0.01 : 0.01; // Adjustment for default overlap in attach() + adjustment = $overlap * (gender == "male" ? -1 : 1); // Adjustment for default overlap in attach() - attachable(anchor,spin,orient, size=[width+2*offset, length, height]) { + attachable(anchor,spin,orient, size=[width+2*offset, slide, height]) { down(height/2+adjustment) { skin( [ @@ -765,8 +765,7 @@ module snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fi { down(lPin/2) intersection() { - if (fixed) - cube([3 * (radius + snap), radius * sqrt(2), 3 * lPin + 3 * radius], center = true); + cube([3 * (radius + snap), fixed ? radius * sqrt(2) : 3*(radius+snap), 3 * lPin + 3 * radius], center = true); union() { _pin_shaft(radius,lStraight,snap,1,1,nub_depth,pointed); if (fins) diff --git a/knurling.scad b/knurling.scad index c2c6f522..94156bc1 100644 --- a/knurling.scad +++ b/knurling.scad @@ -1,10 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: knurling.scad // Shapes and masks for knurling cylinders. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` +// include ////////////////////////////////////////////////////////////////////// diff --git a/linear_bearings.scad b/linear_bearings.scad index fe039f26..cd7b7d2c 100644 --- a/linear_bearings.scad +++ b/linear_bearings.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: linear_bearings.scad // Linear Bearing clips/holders. -// To use, add these lines to the top of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -114,7 +112,7 @@ module linear_bearing_housing(d=15, l=24, tab=7, gap=5, wall=3, tabwall=5, screw up(tabh) { // Screwhole - fwd(ogap/2-2+0.01) screw(screwsize=screwsize*1.06, screwlen=ogap, headsize=screwsize*2, headlen=10, orient=FWD); + fwd(ogap/2-2+0.01) generic_screw(screwsize=screwsize*1.06, screwlen=ogap, headsize=screwsize*2, headlen=10, orient=FWD); // Nut holder back(ogap/2-2+0.01) metric_nut(size=screwsize, hole=false, anchor=BOTTOM, orient=BACK); diff --git a/masks.scad b/masks.scad index b421e7db..28272cc6 100644 --- a/masks.scad +++ b/masks.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: masks.scad // Masking shapes. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -29,7 +27,7 @@ // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Example(FR): +// Example(Render): // angle_pie_mask(ang=30, d=100, l=20); module angle_pie_mask( ang=45, l=undef, @@ -116,34 +114,50 @@ module cylinder_mask( vang = atan2(l, r1-r2)/2; ang1 = first_defined([chamfang1, chamfang, vang]); ang2 = first_defined([chamfang2, chamfang, 90-vang]); - cham1 = first_defined([chamfer1, chamfer, 0]); - cham2 = first_defined([chamfer2, chamfer, 0]); - fil1 = first_defined([rounding1, rounding, 0]); - fil2 = first_defined([rounding2, rounding, 0]); + cham1 = first_defined([chamfer1, chamfer]); + cham2 = first_defined([chamfer2, chamfer]); + fil1 = first_defined([rounding1, rounding]); + fil2 = first_defined([rounding2, rounding]); maxd = max(r1,r2); if ($children > 0) { difference() { children(); - cylinder_mask(l=l, r1=sc*r1, r2=sc*r2, chamfer1=cham1, chamfer2=cham2, chamfang1=ang1, chamfang2=ang2, rounding1=fil1, rounding2=fil2, orient=orient, from_end=from_end); + cylinder_mask( + l=l, r1=sc*r1, r2=sc*r2, + chamfer1=cham1, chamfer2=cham2, + chamfang1=ang1, chamfang2=ang2, + rounding1=fil1, rounding2=fil2, + orient=orient, from_end=from_end + ); } } else { attachable(anchor,spin,orient, r=r1, l=l) { difference() { union() { - chlen1 = cham1 / (from_end? 1 : tan(ang1)); - chlen2 = cham2 / (from_end? 1 : tan(ang2)); + chlen1 = default(cham1,0) / (from_end? 1 : tan(ang1)); + chlen2 = default(cham2,0) / (from_end? 1 : tan(ang2)); if (!ends_only) { cylinder(r=maxd+excess, h=l+2*excess, center=true); } else { - if (cham2>0) up(l/2-chlen2) cylinder(r=maxd+excess, h=chlen2+excess, center=false); - if (cham1>0) down(l/2+excess) cylinder(r=maxd+excess, h=chlen1+excess, center=false); - if (fil2>0) up(l/2-fil2) cylinder(r=maxd+excess, h=fil2+excess, center=false); - if (fil1>0) down(l/2+excess) cylinder(r=maxd+excess, h=fil1+excess, center=false); + if (is_num(cham2) && cham2>0) up(l/2-chlen2) + cylinder(r=maxd+excess, h=chlen2+excess, center=false); + if (is_num(cham1) && cham1>0) + down(l/2+excess) cylinder(r=maxd+excess, h=chlen1+excess, center=false); + if (is_num(fil2) && fil2>0) + up(l/2-fil2) cylinder(r=maxd+excess, h=fil2+excess, center=false); + if (is_num(fil1) && fil1>0) + down(l/2+excess) cylinder(r=maxd+excess, h=fil1+excess, center=false); } } - cyl(r1=sc*r1, r2=sc*r2, l=l, chamfer1=cham1, chamfer2=cham2, chamfang1=ang1, chamfang2=ang2, from_end=from_end, rounding1=fil1, rounding2=fil2); + cyl( + r1=sc*r1, r2=sc*r2, l=l, + chamfer1=cham1, chamfer2=cham2, + chamfang1=ang1, chamfang2=ang2, + from_end=from_end, + rounding1=fil1, rounding2=fil2 + ); } - nil(); + children(); } } } @@ -259,11 +273,11 @@ module chamfer_mask_z(l=1.0, chamfer=1.0, excess=0.1, anchor=CENTER, spin=0) { // size = The size of the rectangular cuboid we want to chamfer. // edges = Edges to chamfer. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. // except_edges = Edges to explicitly NOT chamfer. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: No edges. -// Example(FR): +// Example(Render): // chamfer(chamfer=2, size=[20,40,30]) { // cube(size=[20,40,30], center=true); // } -// Example(FR): +// Example(Render): // chamfer(chamfer=2, size=[20,40,30], edges=[TOP,FRONT+RIGHT], except_edges=TOP+LEFT) { // cube(size=[20,40,30], center=true); // } @@ -565,11 +579,11 @@ module rounding_mask_z(l=1.0, r, r1, r2, d, d1, d2, anchor=CENTER, spin=0) // size = The size of the rectangular cuboid we want to chamfer. // edges = Edges to round. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. // except_edges = Edges to explicitly NOT round. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: No edges. -// Example(FR): +// Example(Render): // rounding(r=10, size=[50,100,150], $fn=24) { // cube(size=[50,100,150], center=true); // } -// Example(FR,FlatSpin): +// Example(FlatSpin,VPD=266): // rounding(r=10, size=[50,50,75], edges=[TOP,FRONT+RIGHT], except_edges=TOP+LEFT, $fn=24) { // cube(size=[50,50,75], center=true); // } @@ -843,7 +857,7 @@ module teardrop_corner_mask(r, angle, excess=0.1, d, anchor=CENTER, spin=0, orie r = get_radius(r=r, d=d, dflt=1); difference() { translate(-[1,1,1]*excess) cube(r+excess, center=false); - translate([1,1,1]*r) onion(r=r,maxang=angle,orient=DOWN); + translate([1,1,1]*r) onion(r=r, ang=angle, orient=DOWN); } } diff --git a/math.scad b/math.scad index a2b811d4..bf676e47 100644 --- a/math.scad +++ b/math.scad @@ -1,22 +1,28 @@ ////////////////////////////////////////////////////////////////////// // LibFile: math.scad // Math helper functions. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// // Section: Math Constants -PHI = (1+sqrt(5))/2; // The golden ratio phi. +// Constant: PHI +// Description: The golden ratio phi. +PHI = (1+sqrt(5))/2; -EPSILON = 1e-9; // A really small value useful in comparing FP numbers. ie: abs(a-b)=len(v) ? _total : _sum(v,_total+v[_i], _i+1); - // Function: cumsum() // Usage: // sums = cumsum(v); @@ -984,6 +990,7 @@ function is_matrix(A,m,n,square=false) = && is_list(A[0]) && (( is_undef(n) && len(A[0]) ) || len(A[0])==n) && (!square || len(A) == len(A[0])) + && is_vector(A[0]) && is_consistent(A); @@ -1459,35 +1466,113 @@ function deriv3(data, h=1, closed=false) = // Section: Complex Numbers -// Function: C_times() + +// Function: complex() // Usage: -// c = C_times(z1,z2) +// z = complex(list) // Description: -// Multiplies two complex numbers represented by 2D vectors. -// Returns a complex number as a 2D vector [REAL, IMAGINARY]. +// Converts a real valued number, vector or matrix into its complex analog +// by replacing all entries with a 2-vector that has zero imaginary part. +function complex(list) = + is_num(list) ? [list,0] : + [for(entry=list) is_num(entry) ? [entry,0] : complex(entry)]; + + +// Function: c_mul() +// Usage: +// 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 +// entries in both arguments must be complex. // Arguments: -// z1 = First complex number, given as a 2D vector [REAL, IMAGINARY] -// z2 = Second complex number, given as a 2D vector [REAL, IMAGINARY] -function C_times(z1,z2) = - assert( is_matrix([z1,z2],2,2), "Complex numbers should be represented by 2D vectors" ) +// z1 = First complex number, vector or matrix +// z2 = Second complex number, vector or matrix + +function _split_complex(data) = + is_vector(data,2) ? data + : is_num(data[0][0]) ? [data*[1,0], data*[0,1]] + : [ + [for(vec=data) vec * [1,0]], + [for(vec=data) vec * [0,1]] + ]; + +function _combine_complex(data) = + is_vector(data,2) ? data + : is_num(data[0][0]) ? [for(i=[0:len(data[0])-1]) [data[0][i],data[1][i]]] + : [for(i=[0:1:len(data[0])-1]) + [for(j=[0:1:len(data[0][0])-1]) + [data[0][i][j], data[1][i][j]]]]; + +function _c_mul(z1,z2) = [ z1.x*z2.x - z1.y*z2.y, z1.x*z2.y + z1.y*z2.x ]; -// Function: C_div() +function c_mul(z1,z2) = + is_matrix([z1,z2],2,2) ? _c_mul(z1,z2) : + _combine_complex(_c_mul(_split_complex(z1), _split_complex(z2))); + + +// Function: c_div() // Usage: -// x = C_div(z1,z2) +// x = c_div(z1,z2) // Description: // Divides two complex numbers represented by 2D vectors. // Returns a complex number as a 2D vector [REAL, IMAGINARY]. // Arguments: // z1 = First complex number, given as a 2D vector [REAL, IMAGINARY] // z2 = Second complex number, given as a 2D vector [REAL, IMAGINARY] -function C_div(z1,z2) = +function c_div(z1,z2) = assert( is_vector(z1,2) && is_vector(z2), "Complex numbers should be represented by 2D vectors." ) assert( !approx(z2,0), "The divisor `z2` cannot be zero." ) let(den = z2.x*z2.x + z2.y*z2.y) [(z1.x*z2.x + z1.y*z2.y)/den, (z1.y*z2.x - z1.x*z2.y)/den]; -// For the sake of consistence with Q_mul and vmul, C_times should be called C_mul + +// Function: c_conj() +// Usage: +// w = c_conj(z) +// Description: +// Computes the complex conjugate of the input, which can be a complex number, +// complex vector or complex matrix. +function c_conj(z) = + is_vector(z,2) ? [z.x,-z.y] : + [for(entry=z) c_conj(entry)]; + +// Function: c_real() +// Usage: +// x = c_real(z) +// Description: +// Returns real part of a complex number, vector or matrix. +function c_real(z) = + is_vector(z,2) ? z.x + : is_num(z[0][0]) ? z*[1,0] + : [for(vec=z) vec * [1,0]]; + +// Function: c_imag() +// Usage: +// x = c_imag(z) +// Description: +// Returns imaginary part of a complex number, vector or matrix. +function c_imag(z) = + is_vector(z,2) ? z.y + : is_num(z[0][0]) ? z*[0,1] + : [for(vec=z) vec * [0,1]]; + + +// Function: c_ident() +// Usage: +// I = c_ident(n) +// Description: +// Produce an n by n complex identity matrix +function c_ident(n) = [for (i = [0:1:n-1]) [for (j = [0:1:n-1]) (i==j)?[1,0]:[0,0]]]; + +// Function: c_norm() +// Usage: +// n = c_norm(z) +// Description: +// Compute the norm of a complex number or vector. +function c_norm(z) = norm_fro(z); + // Section: Polynomials @@ -1533,12 +1618,12 @@ function quadratic_roots(a,b,c,real=false) = // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. function polynomial(p,z,k,total) = -    is_undef(k) -    ?   assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) -        assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) -        polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) -    : k==len(p) ? total -    : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : C_times(total,z)+[p[k],0]); + is_undef(k) + ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) + assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) + polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) + : k==len(p) ? total + : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]); // Function: poly_mult() // Usage: @@ -1548,12 +1633,12 @@ function polynomial(p,z,k,total) = // Given a list of polynomials represented as real coefficient lists, with the highest degree coefficient first, // computes the coefficient list of the product polynomial. function poly_mult(p,q) = -    is_undef(q) ? -        len(p)==2 + is_undef(q) ? + len(p)==2 ? poly_mult(p[0],p[1]) -        : poly_mult(p[0], poly_mult(select(p,1,-1))) -    : -    assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") + : poly_mult(p[0], poly_mult(select(p,1,-1))) + : + assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") p*p==0 || q*q==0 ? [0] : _poly_trim(convolve(p,q)); @@ -1586,12 +1671,12 @@ function _poly_div(n,d,q) = _poly_div(newn,d,newq); -// Internal Function: _poly_trim() -// Usage: -// _poly_trim(p,[eps]) -// Description: -// Removes leading zero terms of a polynomial. By default zeros must be exact, -// or give epsilon for approximate zeros. +/// Internal Function: _poly_trim() +/// Usage: +/// _poly_trim(p,[eps]) +/// Description: +/// Removes leading zero terms of a polynomial. By default zeros must be exact, +/// or give epsilon for approximate zeros. function _poly_trim(p,eps=0) = let( nz = [for(i=[0:1:len(p)-1]) if ( !approx(p[i],0,eps)) i]) len(nz)==0 ? [0] : select(p,nz[0],-1); @@ -1674,10 +1759,10 @@ function _poly_roots(p, pderiv, s, z, tol, i=0) = svals = [for(zk=z) tol*polynomial(s,norm(zk))], p_of_z = [for(zk=z) polynomial(p,zk)], done = [for(k=[0:n-1]) norm(p_of_z[k])<=svals[k]], - newton = [for(k=[0:n-1]) C_div(p_of_z[k], polynomial(pderiv,z[k]))], - zdiff = [for(k=[0:n-1]) sum([for(j=[0:n-1]) if (j!=k) C_div([1,0], z[k]-z[j])])], - w = [for(k=[0:n-1]) done[k] ? [0,0] : C_div( newton[k], - [1,0] - C_times(newton[k], zdiff[k]))] + newton = [for(k=[0:n-1]) c_div(p_of_z[k], polynomial(pderiv,z[k]))], + zdiff = [for(k=[0:n-1]) sum([for(j=[0:n-1]) if (j!=k) c_div([1,0], z[k]-z[j])])], + w = [for(k=[0:n-1]) done[k] ? [0,0] : c_div( newton[k], + [1,0] - c_mul(newton[k], zdiff[k]))] ) all(done) ? z : _poly_roots(p,pderiv,s,z-w,tol,i+1); diff --git a/metric_screws.scad b/metric_screws.scad index 01ae5aa4..73cfa780 100644 --- a/metric_screws.scad +++ b/metric_screws.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: metric_screws.scad // Screws, Bolts, and Nuts. -// To use, include the following lines at the top of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -359,11 +357,11 @@ function get_metric_nut_thickness(size) = lookup(size, [ // Section: Modules -// Module: screw() +// Module: generic_screw() // Description: // Makes a very simple screw model, useful for making screwholes. // Usage: -// screw(screwsize, screwlen, headsize, headlen) +// generic_screw(screwsize, screwlen, headsize, headlen) // Arguments: // screwsize = diameter of threaded part of screw. // screwlen = length of threaded part of screw. @@ -376,16 +374,16 @@ function get_metric_nut_thickness(size) = lookup(size, [ // "base" = At the base of the head. // "countersunk" = At the head height that would be just barely exposed when countersunk. // Examples: -// screw(screwsize=3,screwlen=10,headsize=6,headlen=3, anchor="countersunk"); -// screw(screwsize=3,screwlen=10,headsize=6,headlen=3, anchor="base"); -// Example(FlatSpin): Standard Anchors -// screw(screwsize=3,screwlen=10,headsize=6,headlen=3) +// generic_screw(screwsize=3,screwlen=10,headsize=6,headlen=3, anchor="countersunk"); +// generic_screw(screwsize=3,screwlen=10,headsize=6,headlen=3, anchor="base"); +// Example(FlatSpin,VPD=75): Standard Anchors +// generic_screw(screwsize=3,screwlen=10,headsize=6,headlen=3) // show_anchors(5, custom=false); -// Example(FlatSpin): Standard Anchors -// show_internal_anchors() -// screw(screwsize=3,screwlen=10,headsize=6,headlen=3) +// Example(FlatSpin,VPD=55): Custom Named Anchors +// expose_anchors() +// generic_screw(screwsize=3,screwlen=10,headsize=6,headlen=3) // show_anchors(5, std=false); -module screw( +module generic_screw( screwsize=3, screwlen=10, headsize=6, @@ -469,11 +467,11 @@ module screw( // metric_bolt(headtype="hex", size=10, l=15, phillips="#2"); // Example: Hex Head with Torx // metric_bolt(headtype="hex", size=10, l=15, torx=50); -// Example(FlatSpin): Standard Anchors +// Example(FlatSpin,VPD=100): Standard Anchors // metric_bolt(headtype="oval", size=10, l=15, shank=5, details=true, phillips="#2") // show_anchors(5, custom=false); -// Example(FlatSpin): Custom Anchors -// show_internal_anchors(0.125) +// Example(FlatSpin,VPD=100): Custom Named Anchors +// expose_anchors(0.125) // metric_bolt(headtype="oval", size=10, l=15, shank=5, details=true, phillips="#2") // show_anchors(5, std=false); module metric_bolt( diff --git a/modular_hose.scad b/modular_hose.scad new file mode 100644 index 00000000..365bab31 --- /dev/null +++ b/modular_hose.scad @@ -0,0 +1,221 @@ +////////////////////////////////////////////////////////////////////////// +// LibFile: modular_hose.scad +// Modular hose segment and attachment ends. +// Includes: +// include +// include +////////////////////////////////////////////////////////////////////////// + +// Section: Modular Hose Parts + +_small_end = [ + turtle([ + "left", 90-38.5, // 1/4" hose + "arcsteps", 12, + "arcleft", 6.38493, 62.15, + "arcsteps", 4, + "arcleft", .5, 90+38.5-62.15, + "move", .76, + "left", 67.5, + "move", .47, + "left", 90-67.5, + "move", 4.165, + "right", 30, + "move", 2.1 + ], + state=[4.864,0]), + turtle([ // 1/2" hose + "left", 90-41, + "arcsteps", 16, + "arcleft", 10.7407, 64.27, + "arcsteps", 4, + "arcleft", .5, 90+41-64.27, + "move", .95-.4, + "left", 45, + "move", .4*sqrt(2), + "left",45, + "move", 7.643-.4, + "right", 30, + "move", 4.06 + ], + state=[8.1, 0]), + turtle([ // 3/4" hose + "left", 90-30.4, + "arcsteps", 16, + "arcleft", 13.99219,53, + "arcsteps", 4, + "arcleft", .47,90-53+30.4, + "move", .597, + "left", + "move", 9.908-1.905/tan(25) +3.81*cos(30), // Change to 25 deg angle + "right", 25, // to remove narrow point in wall + "move",1.905 /sin(25), + ], + state=[11.989,0]) + ]; + + +_big_end = [ + turtle([ // 1/4" hose + "left", 90-22, + "move", 6.5, + "left",.75, + "arcsteps", 8, + "arcleft", 6.5, 37.3, + "setdir",90, + "move", .21, + "right", + "move", 1.24, + "right", 45, + "move", .7835, + "right", 19, + "move", 1.05, + "setdir", -90, + "move", 1, + "right", 22, + "move", 8.76 + ], + state = [3.268,0]), + turtle([ // 1/2" hose + "left", + "right", 22, + "move", 9, + "arcsteps", 8, + "arcleft", 11, 36.5, + "setdir",90, + "move",2-1.366, + "right", + "move",.91, + "arcsteps", 4, + "arcright", 1.25, 90, + "move", 2.2, + "arcsteps", 8, + "arcright", 13, 22.4, + "move", 8.73 + ], + state=[6.42154, 0]), + turtle([ // 3/4" hose + "left", 90-22, + "move", 7.633, + "arcsteps", 16, + "arcleft", 13.77, 35.27, + "setdir", 90, + "move", 1.09, + "right", + "move",1.0177, + "right", 45, + "move", 1.009, + "right", 77.8-45, + "move", .3, + "arcright", 15.5, 34.2, + "move", 6.47 + ], + state=[9.90237,0]) + ]; + + +_hose_waist = [1.7698, 1.8251, 3.95998]; + +// Module: modular_hose() +// Usage: +// modular_hose(size, type, , , , , ) +// Description: +// Construct moduler hose segments or modular hose ends for connection to standard +// modular hose systems. The 1/4", 1/2" and 3/4" sizes are supported and you can +// produce just one end to make a mount or end attachment to a modular hose, +// or you can make modular hose segments. To make assembly possible with printed +// parts you can add clearances that make the ball end smaller and the socket end +// larger. These work by simply increasing the radius of the whole end by the specified +// amount. On a Prusa printer with PETG, a clearance of 0.05 allows the 3/4" hose parts to mate +// with standard modular hose or itself. A clearance of 0.05 to 0.1 allows the 1/2" parts to mate with +// standard hose, and with clearance 0 the 1/4" parts will mate with standard hose. Note that clearance values +// are different for the different sizes. You will have to experiment with your machine and materials. Small +// adjustments will change the stiffness of the connection. +// Arguments: +// size = size of modular hose part, must be 1/4, 1/2 or 3/4. +// type = type of part to make, either "segment", "socket" (or "big"), or "ball" (or "small") +// clearance = clearance to make assembly possible. Either a scalar to apply the same to both ends or a vector [small,large] to apply different clearances to the two ends. Default: 0 +// waist_len = size of central "waist" of the part. Default: standard length. +// Example: +// modular_hose(1/4,"segment"); +// right(25)modular_hose(1/2,"segment"); +// right(60)modular_hose(3/4,"segment"); +// Example: A mount point for modular hose +// cylinder(l=10, r=20) +// attach(TOP) modular_hose(1/2, "ball", waist_len=15); +// Example: Mounting plate for something at the end of the hose +// cuboid([50,50,5]) +// attach(TOP) modular_hose(3/4, "socket", waist_len=0); +function modular_hose(size, type, clearance=0, waist_len, anchor=BOTTOM, spin=0,orient=UP) = no_function("modular_hose"); +module modular_hose(size, type, clearance=0, waist_len, anchor=BOTTOM, spin=0,orient=UP) +{ + clearance = force_list(clearance,2); + ind = search([size],[1/4, 1/2, 3/4])[0]; + sbound = + assert(ind!=[], "Must specify size as 1/4, 1/2 or 3/4") + pointlist_bounds(_small_end[ind]); + bbound = pointlist_bounds(_big_end[ind]); + smallend = + assert(is_vector(clearance,2), "Clearance must be a scalar or length 2 vector") + move([-clearance[0],-sbound[0].y],p=_small_end[ind]); + bigend = move([clearance[1], -bbound[0].y], p=_big_end[ind]); + + midlength = first_defined([waist_len, _hose_waist[ind]]); + assert(midlength>=0,"midlength must be nonnegative"); + + goodtypes = ["small","big","segment","socket","ball"]; + shape = + assert(in_list(type,goodtypes), str("type must be one of ",goodtypes)) + type=="segment"? concat(back(midlength,p=smallend),yflip(p=bigend)) + : type=="small" || type=="ball" ? + concat(back(midlength,p=smallend), + [[select(smallend,-1).x,0],[ smallend[0].x,0]]) + : concat( back(midlength,p=bigend), + [[select(bigend,-1).x,0],[ bigend[0].x,0]]); + bounds = pointlist_bounds(shape); + center = mean(bounds); + attachable(anchor,spin,orient,l=bounds[1].y-bounds[0].y, r=bounds[1].x) + { + rotate_extrude(convexity=4) + polygon(fwd(center.y,p=shape)); + children(); + } +} + + +// Function: modular_hose_radius() +// Usage: +// r = modular_hose_radius(size, ); +// Description: +// Returns the inner (or outer) diameter of the waist section +// of the modular hose to enable hollowing out connecting channels. +// Note: diameter is accurate to about 1e-4. +// Arguments: +// size = size of hose part, must be 1/4, 1/2 or 3/4 +// outer = set to true to get the outer diameter. +// Example: +// $fn=64; +// back_half() +// diff("remove") +// cuboid(50){ +// attach(TOP) modular_hose(1/2, "ball"); +// position(TOP+RIGHT) y +// rot(181) +// xrot(90) +// rotate_extrude(angle=135) +// right(25) +// circle(r=modular_hose_radius(1/2)); +// } +function modular_hose_radius(size, outer=false) = + let( + ind = search([size],[1/4, 1/2, 3/4])[0] + ) + assert(ind!=[], "Must specify size as 1/4, 1/2 or 3/4") + let( + b = select(_big_end[ind], [0,-1]), + s = select(_small_end[ind], [0,-1]), + dd=echo(b=b)echo(s=s) + ) + outer ? b[1][0] : b[0][0]; + +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/mutators.scad b/mutators.scad index 80b9752a..a763e6db 100644 --- a/mutators.scad +++ b/mutators.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// -// LibFile: transforms.scad +// LibFile: mutators.scad // Functions and modules to mutate children in various ways. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -16,38 +14,65 @@ // Usage: // bounding_box() ... // Description: -// Returns an axis-aligned cube shape that exactly contains all the 3D children given. +// Returns the smallest axis-aligned square (or cube) shape that contains all the 2D (or 3D) +// children given. The module children() is supposed to be a 3d shape when planar=false and +// a 2d shape when planar=true otherwise the system will issue a warning of mixing dimension +// or scaling by 0. // Arguments: // excess = The amount that the bounding box should be larger than needed to bound the children, in each axis. -// Example: -// #bounding_box() { +// planar = If true, creates a 2D bounding rectangle. Is false, creates a 3D bounding cube. Default: false +// Example(3D): +// module shapes() { // translate([10,8,4]) cube(5); // translate([3,0,12]) cube(2); // } -// translate([10,8,4]) cube(5); -// translate([3,0,12]) cube(2); -module bounding_box(excess=0) { - xs = excess>.1? excess : 1; - // a 3D approx. of the children projection on X axis - module _xProjection() - linear_extrude(xs, center=true) +// #bounding_box() shapes(); +// shapes(); +// Example(2D): +// module shapes() { +// translate([10,8]) square(5); +// translate([3,0]) square(2); +// } +// #bounding_box(planar=true) shapes(); +// shapes(); +module bounding_box(excess=0, planar=false) { + // a 3d (or 2d when planar=true) approx. of the children projection on X axis + module _xProjection() { + if (planar) { projection() rotate([90,0,0]) - linear_extrude(xs, center=true) - projection() - hull() - children(); - - // a bounding box with an offset of 1 in all axis - module _oversize_bbox() { - minkowski() { - _xProjection() children(); // x axis - rotate(-90) _xProjection() rotate(90) children(); // y axis - rotate([0,-90,0]) _xProjection() rotate([0,90,0]) children(); // z axis + linear_extrude(1, center=true) + hull() + children(); + } else { + xs = excess<.1? 1: excess; + linear_extrude(xs, center=true) + projection() + rotate([90,0,0]) + linear_extrude(xs, center=true) + projection() + hull() + children(); } } - // offset children() (a cube) by -1 in all axis + // a bounding box with an offset of 1 in all axis + module _oversize_bbox() { + if (planar) { + minkowski() { + _xProjection() children(); // x axis + rotate(-90) _xProjection() rotate(90) children(); // y axis + } + } else { + minkowski() { + _xProjection() children(); // x axis + rotate(-90) _xProjection() rotate(90) children(); // y axis + rotate([0,-90,0]) _xProjection() rotate([0,90,0]) children(); // z axis + } + } + } + + // offsets a cube by `excess` module _shrink_cube() { intersection() { translate((1-excess)*[ 1, 1, 1]) children(); @@ -55,11 +80,15 @@ module bounding_box(excess=0) { } } - render(convexity=2) - if (excess>.1) { - _oversize_bbox() children(); + if(planar) { + offset(excess-1/2) _oversize_bbox() children(); } else { - _shrink_cube() _oversize_bbox() children(); + render(convexity=2) + if (excess>.1) { + _oversize_bbox() children(); + } else { + _shrink_cube() _oversize_bbox() children(); + } } } @@ -209,7 +238,6 @@ function left_half(_arg1=_undef, _arg2=_undef, _arg3=_undef, // right_half([s], [x]) ... // right_half(planar=true, [s], [x]) ... // -// // Description: // Slices an object at a vertical Y-Z cut plane, and masks away everything that is left of it. // @@ -218,7 +246,7 @@ function left_half(_arg1=_undef, _arg2=_undef, _arg3=_undef, // x = The X coordinate of the cut-plane. Default: 0 // planar = If true, this becomes a 2D operation. // -// Examples(FlatSpin): +// Examples(FlatSpin,VPD=175): // right_half() sphere(r=20); // right_half(x=-5) sphere(r=20); // Example(2D): @@ -260,7 +288,7 @@ function right_half(_arg1=_undef, _arg2=_undef, _arg3=_undef, // y = The Y coordinate of the cut-plane. Default: 0 // planar = If true, this becomes a 2D operation. // -// Examples(FlatSpin): +// Examples(FlatSpin,VPD=175): // front_half() sphere(r=20); // front_half(y=5) sphere(r=20); // Example(2D): @@ -376,7 +404,7 @@ function right_half(_arg1=_undef, _arg2=_undef, _arg3=_undef, // s = Mask size to use. Use a number larger than twice your object's largest axis. If you make this too large, OpenSCAD's preview rendering may be incorrect. Default: 10000 // z = The Z coordinate of the cut-plane. Default: 0 // -// Examples(Spin): +// Examples(Spin,VPD=175): // top_half() sphere(r=20); // top_half(z=5) sphere(r=20); module top_half(s=1000, z=0) @@ -399,9 +427,10 @@ function right_half(_arg1=_undef, _arg2=_undef, _arg3=_undef, ////////////////////////////////////////////////////////////////////// -// Section: Chain Mutators +// Section: Warp Mutators ////////////////////////////////////////////////////////////////////// + // Module: chain_hull() // // Usage: @@ -450,10 +479,74 @@ module chain_hull() } +// Module: path_extrude2d() +// Usage: +// path_extrude2d(path, ) {...} +// Description: +// Extrudes 2D children along the given 2D path, with optional rounded endcaps. +// Arguments: +// path = The 2D path to extrude the geometry along. +// caps = If true, caps each end of the path with a `rotate_extrude()`d copy of the children. This may interact oddly when given asymmetric profile children. +// Example: +// path = [ +// each right(50, p=arc(d=100,angle=[90,180])), +// each left(50, p=arc(d=100,angle=[0,-90])), +// ]; +// path_extrude2d(path,caps=false) { +// fwd(2.5) square([5,6],center=true); +// fwd(6) square([10,5],center=true); +// } +// Example: +// path_extrude2d(arc(d=100,angle=[180,270])) +// trapezoid(w1=10, w2=5, h=10, anchor=BACK); +// Example: +// include +// path = bezier_path([ +// [-50,0], [-25,50], [0,0], [50,0] +// ]); +// path_extrude2d(path, caps=false) +// trapezoid(w1=10, w2=1, h=5, anchor=BACK); +module path_extrude2d(path, caps=true) { + thin = 0.01; + path = deduplicate(path); + for (p=pair(path)) { + delt = p[1]-p[0]; + translate(p[0]) { + rot(from=BACK,to=delt) { + minkowski() { + cube([thin,norm(delt),thin], anchor=FRONT); + rotate([90,0,0]) linear_extrude(height=thin,center=true) children(); + } + } + } + } + for (t=triplet(path)) { + ang = vang(t[2]-t[1]) - vang(t[1]-t[0]); + delt = t[2] - t[1]; + translate(t[1]) { + minkowski() { + cube(thin,center=true); + if (ang >= 0) { + rotate(90-ang) + rot(from=LEFT,to=delt) + rotate_extrude(angle=ang+0.01) + right_half(planar=true) children(); + } else { + rotate(-90) + rot(from=RIGHT,to=delt) + rotate_extrude(angle=-ang+0.01) + left_half(planar=true) children(); + } + } + } + } + if (caps) { + move_copies([path[0],last(path)]) + rotate_extrude() + right_half(planar=true) children(); + } +} -////////////////////////////////////////////////////////////////////// -// Section: Warp Mutators -////////////////////////////////////////////////////////////////////// // Module: cylindrical_extrude() // Usage: @@ -516,31 +609,105 @@ module cylindrical_extrude(or, ir, od, id, size=1000, convexity=10, spin=0, orie // Section: Offset Mutators ////////////////////////////////////////////////////////////////////// -// Module: round3d() +// Module: minkowski_difference() // Usage: -// round3d(r) ... -// round3d(or) ... -// round3d(ir) ... -// round3d(or, ir) ... +// minkowski_difference() { base_shape(); diff_shape(); ... } // Description: -// Rounds arbitrary 3D objects. Giving `r` rounds all concave and convex corners. Giving just `ir` +// Takes a 3D base shape and one or more 3D diff shapes, carves out the diff shapes from the +// surface of the base shape, in a way complementary to how `minkowski()` unions shapes to the +// surface of its base shape. +// Arguments: +// planar = If true, performs minkowski difference in 2D. Default: false (3D) +// Example: +// minkowski_difference() { +// union() { +// cube([120,70,70], center=true); +// cube([70,120,70], center=true); +// cube([70,70,120], center=true); +// } +// sphere(r=10); +// } +module minkowski_difference(planar=false) { + difference() { + bounding_box(excess=0, planar=planar) children(0); + render(convexity=20) { + minkowski() { + difference() { + bounding_box(excess=1, planar=planar) children(0); + children(0); + } + for (i=[1:1:$children-1]) children(i); + } + } + } +} + + +// Module: round2d() +// Usage: +// round2d(r) ... +// round2d(or) ... +// round2d(ir) ... +// round2d(or, ir) ... +// Description: +// Rounds arbitrary 2D objects. Giving `r` rounds all concave and convex corners. Giving just `ir` // rounds just concave corners. Giving just `or` rounds convex corners. Giving both `ir` and `or` -// can let you round to different radii for concave and convex corners. The 3D object must not have -// any parts narrower than twice the `or` radius. Such parts will disappear. This is an *extremely* -// slow operation. I cannot emphasize enough just how slow it is. It uses `minkowski()` multiple times. -// Use this as a last resort. This is so slow that no example images will be rendered. +// can let you round to different radii for concave and convex corners. The 2D object must not have +// any parts narrower than twice the `or` radius. Such parts will disappear. // Arguments: // r = Radius to round all concave and convex corners to. // or = Radius to round only outside (convex) corners to. Use instead of `r`. // ir = Radius to round only inside (concave) corners to. Use instead of `r`. -module round3d(r, or, ir, size=100) +// Examples(2D): +// round2d(r=10) {square([40,100], center=true); square([100,40], center=true);} +// round2d(or=10) {square([40,100], center=true); square([100,40], center=true);} +// round2d(ir=10) {square([40,100], center=true); square([100,40], center=true);} +// round2d(or=16,ir=8) {square([40,100], center=true); square([100,40], center=true);} +module round2d(r, or, ir) { or = get_radius(r1=or, r=r, dflt=0); ir = get_radius(r1=ir, r=r, dflt=0); - offset3d(or, size=size) - offset3d(-ir-or, size=size) - offset3d(ir, size=size) + offset(or) offset(-ir-or) offset(delta=ir,chamfer=true) children(); +} + + +// Module: shell2d() +// Usage: +// shell2d(thickness, [or], [ir], [fill], [round]) +// Description: +// Creates a hollow shell from 2D children, with optional rounding. +// Arguments: +// thickness = Thickness of the shell. Positive to expand outward, negative to shrink inward, or a two-element list to do both. +// or = Radius to round corners on the outside of the shell. If given a list of 2 radii, [CONVEX,CONCAVE], specifies the radii for convex and concave corners separately. Default: 0 (no outside rounding) +// ir = Radius to round corners on the inside of the shell. If given a list of 2 radii, [CONVEX,CONCAVE], specifies the radii for convex and concave corners separately. Default: 0 (no inside rounding) +// Examples(2D): +// shell2d(10) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(-10) {square([40,100], center=true); square([100,40], center=true);} +// shell2d([-10,10]) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,or=10) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,ir=10) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,or=[10,0]) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,or=[0,10]) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,ir=[10,0]) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(10,ir=[0,10]) {square([40,100], center=true); square([100,40], center=true);} +// shell2d(8,or=[16,8],ir=[16,8]) {square([40,100], center=true); square([100,40], center=true);} +module shell2d(thickness, or=0, ir=0) +{ + thickness = is_num(thickness)? ( + thickness<0? [thickness,0] : [0,thickness] + ) : (thickness[0]>thickness[1])? ( + [thickness[1],thickness[0]] + ) : thickness; + orad = is_finite(or)? [or,or] : or; + irad = is_finite(ir)? [ir,ir] : ir; + difference() { + round2d(or=orad[0],ir=orad[1]) + offset(delta=thickness[1]) children(); + round2d(or=irad[1],ir=irad[0]) + offset(delta=thickness[0]) + children(); + } } @@ -583,101 +750,31 @@ module offset3d(r=1, size=100, convexity=10) { } -// Module: round2d() +// Module: round3d() // Usage: -// round2d(r) ... -// round2d(or) ... -// round2d(ir) ... -// round2d(or, ir) ... +// round3d(r) ... +// round3d(or) ... +// round3d(ir) ... +// round3d(or, ir) ... // Description: -// Rounds arbitrary 2D objects. Giving `r` rounds all concave and convex corners. Giving just `ir` +// Rounds arbitrary 3D objects. Giving `r` rounds all concave and convex corners. Giving just `ir` // rounds just concave corners. Giving just `or` rounds convex corners. Giving both `ir` and `or` -// can let you round to different radii for concave and convex corners. The 2D object must not have -// any parts narrower than twice the `or` radius. Such parts will disappear. +// can let you round to different radii for concave and convex corners. The 3D object must not have +// any parts narrower than twice the `or` radius. Such parts will disappear. This is an *extremely* +// slow operation. I cannot emphasize enough just how slow it is. It uses `minkowski()` multiple times. +// Use this as a last resort. This is so slow that no example images will be rendered. // Arguments: // r = Radius to round all concave and convex corners to. // or = Radius to round only outside (convex) corners to. Use instead of `r`. // ir = Radius to round only inside (concave) corners to. Use instead of `r`. -// Examples(2D): -// round2d(r=10) {square([40,100], center=true); square([100,40], center=true);} -// round2d(or=10) {square([40,100], center=true); square([100,40], center=true);} -// round2d(ir=10) {square([40,100], center=true); square([100,40], center=true);} -// round2d(or=16,ir=8) {square([40,100], center=true); square([100,40], center=true);} -module round2d(r, or, ir) +module round3d(r, or, ir, size=100) { or = get_radius(r1=or, r=r, dflt=0); ir = get_radius(r1=ir, r=r, dflt=0); - offset(or) offset(-ir-or) offset(delta=ir,chamfer=true) children(); -} - - -// Module: shell2d() -// Usage: -// shell2d(thickness, [or], [ir], [fill], [round]) -// Description: -// Creates a hollow shell from 2D children, with optional rounding. -// Arguments: -// thickness = Thickness of the shell. Positive to expand outward, negative to shrink inward, or a two-element list to do both. -// or = Radius to round convex corners/pointy bits on the outside of the shell. -// ir = Radius to round concave corners on the outside of the shell. -// round = Radius to round convex corners/pointy bits on the inside of the shell. -// fill = Radius to round concave corners on the inside of the shell. -// Examples(2D): -// shell2d(10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(-10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d([-10,10]) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(10,or=10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(10,ir=10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(10,round=10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(10,fill=10) {square([40,100], center=true); square([100,40], center=true);} -// shell2d(8,or=16,ir=8,round=16,fill=8) {square([40,100], center=true); square([100,40], center=true);} -module shell2d(thickness, or=0, ir=0, fill=0, round=0) -{ - thickness = is_num(thickness)? ( - thickness<0? [thickness,0] : [0,thickness] - ) : (thickness[0]>thickness[1])? ( - [thickness[1],thickness[0]] - ) : thickness; - difference() { - round2d(or=or,ir=ir) - offset(delta=thickness[1]) + offset3d(or, size=size) + offset3d(-ir-or, size=size) + offset3d(ir, size=size) children(); - round2d(or=fill,ir=round) - offset(delta=thickness[0]) - children(); - } -} - - -// Module: minkowski_difference() -// Usage: -// minkowski_difference() { base_shape(); diff_shape(); ... } -// Description: -// Takes a 3D base shape and one or more 3D diff shapes, carves out the diff shapes from the -// surface of the base shape, in a way complementary to how `minkowski()` unions shapes to the -// surface of its base shape. -// Example: -// minkowski_difference() { -// union() { -// cube([120,70,70], center=true); -// cube([70,120,70], center=true); -// cube([70,70,120], center=true); -// } -// sphere(r=10); -// } -module minkowski_difference() { - difference() { - bounding_box(excess=0) children(0); - render(convexity=10) { - minkowski() { - difference() { - bounding_box(excess=1) children(0); - children(0); - } - for (i=[1:1:$children-1]) children(i); - } - } - } } diff --git a/nema_steppers.scad b/nema_steppers.scad index 2244806a..d3851924 100644 --- a/nema_steppers.scad +++ b/nema_steppers.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: nema_steppers.scad // Masks and models for NEMA stepper motors. -// To use, add these lines to the top of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// diff --git a/partitions.scad b/partitions.scad index c46134c8..2e516a05 100644 --- a/partitions.scad +++ b/partitions.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: partitions.scad // Modules to help partition large objects into smaller parts that can be reassembled. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -130,7 +128,7 @@ module partition_cut_mask(l=100, h=100, cutsize=10, cutpath=undef, gap=0, spin=0 rot(from=UP,to=orient) { rotate(spin) { linear_extrude(height=h, convexity=10) { - stroke(path, width=$slop*2); + stroke(path, width=max(0.1, $slop*2)); } } } diff --git a/paths.scad b/paths.scad index fe14a50f..5d391c34 100644 --- a/paths.scad +++ b/paths.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: paths.scad // Support for polygons and paths. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -64,7 +62,8 @@ function is_closed_path(path, eps=EPSILON) = approx(path[0], path[len(path)-1], // close_path(path); // Description: // If a path's last point does not coincide with its first point, closes the path so it does. -function close_path(path, eps=EPSILON) = is_closed_path(path,eps=eps)? path : concat(path,[path[0]]); +function close_path(path, eps=EPSILON) = + is_closed_path(path,eps=eps)? path : concat(path,[path[0]]); // Function: cleanup_path() @@ -72,7 +71,8 @@ function close_path(path, eps=EPSILON) = is_closed_path(path,eps=eps)? path : co // cleanup_path(path); // Description: // If a path's last point coincides with its first point, deletes the last point in the path. -function cleanup_path(path, eps=EPSILON) = is_closed_path(path,eps=eps)? select(path,0,-2) : path; +function cleanup_path(path, eps=EPSILON) = + is_closed_path(path,eps=eps)? [for (i=[0:1:len(path)-2]) path[i]] : path; // Function: path_subselect() @@ -116,15 +116,15 @@ function path_subselect(path, s1, u1, s2, u2, closed=false) = function simplify_path(path, eps=EPSILON) = assert( is_path(path), "Invalid path." ) assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) - len(path)<=2 ? path - : let( - indices = [ 0, - for (i=[1:1:len(path)-2]) - if (!collinear(path[i-1],path[i],path[i+1], eps=eps)) i, - len(path)-1 - ] - ) - [for (i = indices) path[i] ]; + len(path)<=2 ? path : + let( + indices = [ + 0, + for (i=[1:1:len(path)-2]) + if (!collinear(path[i-1], path[i], path[i+1], eps=eps)) i, + len(path)-1 + ] + ) [for (i=indices) path[i]]; // Function: simplify_path_indexed() @@ -139,19 +139,21 @@ function simplify_path(path, eps=EPSILON) = // indices = A list of indices into `points` that forms a path. // eps = Largest angle variance allowed. Default: EPSILON (1-e9) degrees. function simplify_path_indexed(points, indices, eps=EPSILON) = - len(indices)<=2? indices - : let( - indices = concat( indices[0], - [for (i=[1:1:len(indices)-2]) - let( - i1 = indices[i-1], - i2 = indices[i], - i3 = indices[i+1] - ) - if (!collinear(points[i1],points[i2],points[i3], eps=eps)) indices[i]], - indices[len(indices)-1] ) - ) - indices; + len(indices)<=2? indices : + let( + indices = concat( + indices[0], + [ + for (i=[1:1:len(indices)-2]) let( + i1 = indices[i-1], + i2 = indices[i], + i3 = indices[i+1] + ) if (!collinear(points[i1], points[i2], points[i3], eps=eps)) + indices[i] + ], + indices[len(indices)-1] + ) + ) indices; // Function: path_length() @@ -180,9 +182,9 @@ function path_length(path,closed=false) = // closed = true if the path is closed. Default: false function path_segment_lengths(path, closed=false) = [ - for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), - if (closed) norm(path[0]-path[len(path)-1]) - ]; + for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), + if (closed) norm(path[0]-select(path,-1)) + ]; // Function: path_pos_from_start() @@ -353,22 +355,32 @@ function path_tangents(path, closed=false, uniform=true) = // norms = path_normals(path, , ); // Description: // Compute the normal vector to the input path. This vector is perpendicular to the -// path tangent and lies in the plane of the curve. When there are collinear points, -// the curve does not define a unique plane and the normal is not uniquely defined. +// path tangent and lies in the plane of the curve. For 3d paths we define the plane of the curve +// at path point i to be the plane defined by point i and its two neighbors. At the endpoints of open paths +// we use the three end points. The computed normal is the one lying in this plane and pointing to the +// right of the direction of the path. If points are collinear then the path does not define a unique plane +// and hence the (right pointing) normal is not uniquely defined. In this case the function issues an error. +// For 2d paths the plane is always defined so the normal fails to exist only +// when the derivative is zero (in the case of repeated points). function path_normals(path, tangents, closed=false) = - assert(is_path(path)) + assert(is_path(path,[2,3])) assert(is_bool(closed)) - let( tangents = default(tangents, path_tangents(path,closed)) ) - assert(is_path(tangents)) + let( + tangents = default(tangents, path_tangents(path,closed)), + dim=len(path[0]) + ) + assert(is_path(tangents) && len(tangents[0])==dim,"Dimensions of path and tangents must match") [ - for(i=idx(path)) let( - pts = i==0? (closed? select(path,-1,1) : select(path,0,2)) : - i==len(path)-1? (closed? select(path,i-1,i+1) : select(path,i-2,i)) : - select(path,i-1,i+1) - ) unit(cross( - cross(pts[1]-pts[0], pts[2]-pts[0]), - tangents[i] - )) + for(i=idx(path)) + let( + pts = i==0 ? (closed? select(path,-1,1) : select(path,0,2)) + : i==len(path)-1 ? (closed? select(path,i-1,i+1) : select(path,i-2,i)) + : select(path,i-1,i+1) + ) + dim == 2 ? [tangents[i].y,-tangents[i].x] + : let(v=cross(cross(pts[1]-pts[0], pts[2]-pts[0]),tangents[i])) + assert(norm(v)>EPSILON, "3D path contains collinear points") + v ]; @@ -407,6 +419,193 @@ function path_torsion(path, closed=false) = ]; +// Function: path_chamfer_and_rounding() +// Usage: +// path2 = path_chamfer_and_rounding(path, [closed], [chamfer], [rounding]); +// Description: +// Rounds or chamfers corners in the given path. +// Arguments: +// path = The path to chamfer and/or round. +// closed = If true, treat path like a closed polygon. Default: true +// chamfer = The length of the chamfer faces at the corners. If given as a list of numbers, gives individual chamfers for each corner, from first to last. Default: 0 (no chamfer) +// rounding = The rounding radius for the corners. If given as a list of numbers, gives individual radii for each corner, from first to last. Default: 0 (no rounding) +// Example(2D): Chamfering a Path +// path = star(5, step=2, d=100); +// path2 = path_chamfer_and_rounding(path, closed=true, chamfer=5); +// stroke(path2, closed=true); +// Example(2D): Per-Corner Chamfering +// path = star(5, step=2, d=100); +// chamfs = [for (i=[0:1:4]) each 3*[i,i]]; +// path2 = path_chamfer_and_rounding(path, closed=true, chamfer=chamfs); +// stroke(path2, closed=true); +// Example(2D): Rounding a Path +// path = star(5, step=2, d=100); +// path2 = path_chamfer_and_rounding(path, closed=true, rounding=5); +// stroke(path2, closed=true); +// Example(2D): Per-Corner Chamfering +// path = star(5, step=2, d=100); +// rs = [for (i=[0:1:4]) each 2*[i,i]]; +// path2 = path_chamfer_and_rounding(path, closed=true, rounding=rs); +// stroke(path2, closed=true); +// Example(2D): Mixing Chamfers and Roundings +// path = star(5, step=2, d=100); +// chamfs = [for (i=[0:4]) each [5,0]]; +// rs = [for (i=[0:4]) each [0,10]]; +// path2 = path_chamfer_and_rounding(path, closed=true, chamfer=chamfs, rounding=rs); +// stroke(path2, closed=true); +function path_chamfer_and_rounding(path, closed, chamfer, rounding) = + let ( + path = deduplicate(path,closed=true), + lp = len(path), + chamfer = is_undef(chamfer)? repeat(0,lp) : + is_vector(chamfer)? list_pad(chamfer,lp,0) : + is_num(chamfer)? repeat(chamfer,lp) : + assert(false, "Bad chamfer value."), + rounding = is_undef(rounding)? repeat(0,lp) : + is_vector(rounding)? list_pad(rounding,lp,0) : + is_num(rounding)? repeat(rounding,lp) : + assert(false, "Bad rounding value."), + corner_paths = [ + for (i=(closed? [0:1:lp-1] : [1:1:lp-2])) let( + p1 = select(path,i-1), + p2 = select(path,i), + p3 = select(path,i+1) + ) + chamfer[i] > 0? _corner_chamfer_path(p1, p2, p3, side=chamfer[i]) : + rounding[i] > 0? _corner_roundover_path(p1, p2, p3, r=rounding[i]) : + [p2] + ], + out = [ + if (!closed) path[0], + for (i=(closed? [0:1:lp-1] : [1:1:lp-2])) let( + p1 = select(path,i-1), + p2 = select(path,i), + crn1 = select(corner_paths,i-1), + crn2 = corner_paths[i], + l1 = norm(select(crn1,-1)-p1), + l2 = norm(crn2[0]-p2), + needed = l1 + l2, + seglen = norm(p2-p1), + check = assert(seglen >= needed, str("Path segment ",i," is too short to fulfill rounding/chamfering for the adjacent corners.")) + ) each crn2, + if (!closed) select(path,-1) + ] + ) deduplicate(out); + + +function _corner_chamfer_path(p1, p2, p3, dist1, dist2, side, angle) = + let( + v1 = unit(p1 - p2), + v2 = unit(p3 - p2), + n = vector_axis(v1,v2), + ang = vector_angle(v1,v2), + path = (is_num(dist1) && is_undef(dist2) && is_undef(side))? ( + // dist1 & optional angle + assert(dist1 > 0) + let(angle = default(angle,(180-ang)/2)) + assert(is_num(angle)) + assert(angle > 0 && angle < 180) + let( + pta = p2 + dist1*v1, + a3 = 180 - angle - ang + ) assert(a3>0, "Angle too extreme.") + let( + side = sin(angle) * dist1/sin(a3), + ptb = p2 + side*v2 + ) [pta, ptb] + ) : (is_undef(dist1) && is_num(dist2) && is_undef(side))? ( + // dist2 & optional angle + assert(dist2 > 0) + let(angle = default(angle,(180-ang)/2)) + assert(is_num(angle)) + assert(angle > 0 && angle < 180) + let( + ptb = p2 + dist2*v2, + a3 = 180 - angle - ang + ) assert(a3>0, "Angle too extreme.") + let( + side = sin(angle) * dist2/sin(a3), + pta = p2 + side*v1 + ) [pta, ptb] + ) : (is_undef(dist1) && is_undef(dist2) && is_num(side))? ( + // side & optional angle + assert(side > 0) + let(angle = default(angle,(180-ang)/2)) + assert(is_num(angle)) + assert(angle > 0 && angle < 180) + let( + a3 = 180 - angle - ang + ) assert(a3>0, "Angle too extreme.") + let( + dist1 = sin(a3) * side/sin(ang), + dist2 = sin(angle) * side/sin(ang), + pta = p2 + dist1*v1, + ptb = p2 + dist2*v2 + ) [pta, ptb] + ) : (is_num(dist1) && is_num(dist2) && is_undef(side) && is_undef(side))? ( + // dist1 & dist2 + assert(dist1 > 0) + assert(dist2 > 0) + let( + pta = p2 + dist1*v1, + ptb = p2 + dist2*v2 + ) [pta, ptb] + ) : ( + assert(false,"Bad arguments.") + ) + ) path; + + +function _corner_roundover_path(p1, p2, p3, r, d) = + let( + r = get_radius(r=r,d=d,dflt=undef), + res = circle_2tangents(p1, p2, p3, r=r, tangents=true), + cp = res[0], + n = res[1], + tp1 = res[2], + ang = res[4]+res[5], + steps = floor(segs(r)*ang/360+0.5), + step = ang / steps, + path = [for (i=[0:1:steps]) move(cp, p=rot(a=-i*step, v=n, p=tp1-cp))] + ) path; + + + +// Function: path_add_jitter() +// Topics: Paths +// See Also: jittered_poly(), subdivide_long_segments() +// Usage: +// jpath = path_add_jitter(path, , ); +// Description: +// Adds tiny jitter offsets to collinear points in the given path so that they +// are no longer collinear. This is useful for preserving subdivision on long +// straight segments, when making geometry with `polygon()`, for use with +// `linear_exrtrude()` with a `twist()`. +// Arguments: +// path = The path to add jitter to. +// dist = The amount to jitter points by. Default: 1/512 (0.00195) +// --- +// closed = If true, treat path like a closed polygon. Default: true +// Example: +// d = 100; h = 75; quadsize = 5; +// path = pentagon(d=d); +// spath = subdivide_long_segments(path, quadsize, closed=true); +// jpath = path_add_jitter(spath, closed=true); +// linear_extrude(height=h, twist=72, slices=h/quadsize) +// polygon(jpath); +function path_add_jitter(path, dist=1/512, closed=true) = + assert(is_path(path)) + assert(is_finite(dist)) + assert(is_bool(closed)) + [ + path[0], + for (i=idx(path,s=1,e=closed?-1:-2)) let( + n = line_normal([path[i-1],path[i]]) + ) path[i] + n * (collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), + if (!closed) last(path) + ]; + + // Function: path3d_spiral() // Description: // Returns a 3D spiral path. @@ -674,7 +873,7 @@ function assemble_a_path_from_fragments(fragments, rightmost=true, startfrag=0, [foundfrag, concat([path], remainder)] ) : let( fragend = select(foundfrag,-1), - hits = [for (i = idx(path,end=-2)) if(approx(path[i],fragend,eps=eps)) i] + hits = [for (i = idx(path,e=-2)) if(approx(path[i],fragend,eps=eps)) i] ) hits? ( let( // Found fragment intersects with initial path @@ -766,6 +965,31 @@ module modulated_circle(r, sines=[[1,1]], d) } +// Module: jittered_poly() +// Topics: Extrusions +// See Also: path_add_jitter(), subdivide_long_segments() +// Usage: +// jittered_poly(path, ); +// Description: +// Creates a 2D polygon shape from the given path in such a way that any extra +// collinear points are not stripped out in the way that `polygon()` normally does. +// This is useful for refining the mesh of a `linear_extrude()` with twist. +// Arguments: +// path = The path to add jitter to. +// dist = The amount to jitter points by. Default: 1/512 (0.00195) +// Example: +// d = 100; h = 75; quadsize = 5; +// path = pentagon(d=d); +// spath = subdivide_long_segments(path, quadsize, closed=true); +// linear_extrude(height=h, twist=72, slices=h/quadsize) +// jittered_poly(spath); +module jittered_poly(path, dist=1/512) { + polygon(path_add_jitter(path, dist, closed=true)); +} + + + + // Section: 3D Modules @@ -779,7 +1003,7 @@ module modulated_circle(r, sines=[[1,1]], d) // twist = number of degrees to twist the 2D shape over the entire extrusion length. // scale = scale multiplier for end of extrusion compared the start. // slices = Number of slices along the extrusion to break the extrusion into. Useful for refining `twist` extrusions. -// Example(FlatSpin): +// Example(FlatSpin,VPD=200,VPT=[0,0,15]): // extrude_from_to([0,0,0], [10,20,30], convexity=4, twist=360, scale=3.0, slices=40) { // xcopies(3) circle(3, $fn=32); // } @@ -800,14 +1024,16 @@ module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) { // Module: spiral_sweep() // Description: -// Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path +// Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path. // of a given radius, height and twist. // Arguments: -// path = Array of points of a polygon path, to be extruded. +// poly = Array of points of a polygon path, to be extruded. // h = height of the spiral to extrude along. // r = Radius of the spiral to extrude along. Default: 50 -// d = Diameter of the spiral to extrude along. // twist = number of degrees of rotation to spiral up along height. +// --- +// d = Diameter of the spiral to extrude along. +// higbee = Length to taper thread ends over. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -815,53 +1041,57 @@ module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) { // Example: // poly = [[-10,0], [-3,-5], [3,-5], [10,0], [0,-30]]; // spiral_sweep(poly, h=200, r=50, twist=1080, $fn=36); -module spiral_sweep(poly, h, r, twist=360, center, d, anchor, spin=0, orient=UP) { - r = get_radius(r=r, d=d, dflt=50); +module spiral_sweep(poly, h, r, twist=360, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, anchor, spin=0, orient=UP) { poly = path3d(poly); - pline_count = len(poly); - steps = ceil(segs(r)*(twist/360)); anchor = get_anchor(anchor,center,BOT,BOT); + r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50); + r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50); + sides = segs(max(r1,r2)); + steps = ceil(sides*(twist/360)); + higbee1 = first_defined([higbee1, higbee, 0]); + higbee2 = first_defined([higbee2, higbee, 0]); + higang1 = 360 * higbee1 / (2 * r1 * PI); + higang2 = 360 * higbee2 / (2 * r2 * PI); + higsteps1 = ceil(higang1/360*sides); + higsteps2 = ceil(higang2/360*sides); + assert(higang1 < twist/2); + assert(higang2 < twist/2); - poly_points = [ - for ( - p = [0:1:steps] - ) let ( - a = twist * (p/steps), - dx = r*cos(a), - dy = r*sin(a), - dz = h * (p/steps), - pts = apply_list( - poly, [ - affine3d_xrot(90), - affine3d_zrot(a), - affine3d_translate([dx, dy, dz-h/2]) - ] - ) - ) for (pt = pts) pt + function higsize(a) = lookup(a,[ + [-0.001, 0], + for (x=[0.125:0.125:1]) [ x*higang1, pow(x,1/2)], + for (x=[0.125:0.125:1]) [twist-x*higang2, pow(x,1/2)], + [twist+0.001, 0] + ]); + + us = [ + for (i=[0:higsteps1/10:higsteps1]) i, + for (i=[higsteps1+1:1:steps-higsteps2-1]) i, + for (i=[steps-higsteps2:higsteps2/10:steps]) i, + ]; + zang = atan2(r2-r1,h); + points = [ + for (p = us) let ( + u = p / steps, + a = twist * u, + hsc = higsize(a), + r = lerp(r1,r2,u), + mat = affine3d_zrot(a) * + affine3d_translate([r, 0, h * (u-0.5)]) * + affine3d_xrot(90) * + affine3d_skew_xz(xa=zang) * + affine3d_scale([hsc,lerp(hsc,1,0.25),1]), + pts = apply(mat, poly) + ) pts ]; - poly_faces = concat( - [[for (b = [0:1:pline_count-1]) b]], - [ - for ( - p = [0:1:steps-1], - b = [0:1:pline_count-1], - i = [0:1] - ) let ( - b2 = (b == pline_count-1)? 0 : b+1, - p0 = p * pline_count + b, - p1 = p * pline_count + b2, - p2 = (p+1) * pline_count + b2, - p3 = (p+1) * pline_count + b, - pt = (i==0)? [p0, p2, p1] : [p0, p3, p2] - ) pt - ], - [[for (b = [pline_count-1:-1:0]) b+(steps)*pline_count]] + vnf = vnf_vertex_array( + points, col_wrap=true, caps=true, + style=(abs(higbee1)+abs(higbee2))>0? "quincunx" : "alt" ); - tri_faces = triangulate_faces(poly_points, poly_faces); - attachable(anchor,spin,orient, r=r, l=h) { - polyhedron(points=poly_points, faces=tri_faces, convexity=10); + attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) { + vnf_polyhedron(vnf, convexity=2*twist/360); children(); } } @@ -875,7 +1105,7 @@ module spiral_sweep(poly, h, r, twist=360, center, d, anchor, spin=0, orient=UP) // path = array of points for the bezier path to extrude along. // convexity = maximum number of walls a ran can pass through. // clipsize = increase if artifacts are left. Default: 1000 -// Example(FlatSpin): +// Example(FlatSpin,VPD=600,VPT=[75,16,20]): // path = [ [0, 0, 0], [33, 33, 33], [66, 33, 40], [100, 0, 0], [150,0,0] ]; // path_extrude(path) circle(r=10, $fn=6); module path_extrude(path, convexity=10, clipsize=100) { @@ -991,14 +1221,14 @@ module path_extrude(path, convexity=10, clipsize=100) { // polygon(concat([[0,0]],wedge)); // path_spread(wedge,n=5,spacing=3) fwd(.1) rect([1,4],anchor=FRONT); // } -// Example(Spin): 3d example, with children rotated into the plane of the path +// Example(Spin,VPD=115): 3d example, with children rotated into the plane of the path // tilted_circle = lift_plane(regular_ngon(n=64, or=12), [0,0,0], [5,0,5], [0,2,3]); // path_sweep(regular_ngon(n=16,or=.1),tilted_circle); // path_spread(tilted_circle, n=15,closed=true) { // color("blue") cyl(h=3,r=.2, anchor=BOTTOM); // z-aligned cylinder // color("red") xcyl(h=10,r=.2, anchor=FRONT+LEFT); // x-aligned cylinder // } -// Example(Spin): 3d example, with rotate_children set to false +// Example(Spin,VPD=115): 3d example, with rotate_children set to false // tilted_circle = lift_plane(regular_ngon(n=64, or=12), [0,0,0], [5,0,5], [0,2,3]); // path_sweep(regular_ngon(n=16,or=.1),tilted_circle); // path_spread(tilted_circle, n=25,rotate_children=false,closed=true) { @@ -1025,9 +1255,9 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals sort([for(entry=ptlist) posmod(entry-listcenter,length)]) : [for(entry=ptlist) entry + length/2-listcenter ] ); - distOK = min(distances)>=0 && max(distances)<=length; + distOK = is_def(n) || (min(distances)>=0 && max(distances)<=length); assert(distOK,"Cannot fit all of the copies"); - cutlist = path_cut(path, distances, closed, direction=true); + cutlist = path_cut_points(path, distances, closed, direction=true); planar = len(path[0])==2; if (true) for(i=[0:1:len(cutlist)-1]) { $pos = cutlist[i][0]; @@ -1039,7 +1269,7 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals if(planar) { rot(from=[0,1],to=cutlist[i][3]) children(); } else { - multmatrix(affine2d_to_3d(transpose([cutlist[i][2],cross(cutlist[i][3],cutlist[i][2]), cutlist[i][3]]))) + multmatrix(affine3d_frame_map(x=cutlist[i][2], z=cutlist[i][3])) children(); } } else { @@ -1050,61 +1280,76 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals } -// Function: path_cut() +// Function: path_cut_points() // // Usage: -// path_cut(path, dists, [closed], [direction]) +// cuts = path_cut_points(path, dists, , ); // // Description: // Cuts a path at a list of distances from the first point in the path. Returns a list of the cut // points and indices of the next point in the path after that point. So for example, a return // value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after -// this point is path[5]. If the path is too short then path_cut returns undef. If you set -// `direction` to true then `path_cut` will also return the tangent vector to the path and a normal +// this point is path[5]. If the path is too short then path_cut_points returns undef. If you set +// `direction` to true then `path_cut_points` will also return the tangent vector to the path and a normal // vector to the path. It tries to find a normal vector that is coplanar to the path near the cut // point. If this fails it will return a normal vector parallel to the xy plane. The output with // direction vectors will be `[point, next_index, tangent, normal]`. +// . +// If you give the very last point of the path as a cut point then the returned index will be +// one larger than the last index (so it will not be a valid index). If you use the closed +// option then the returned index will be equal to the path length for cuts along the closing +// path segment, and if you give a point equal to the path length you will get an +// index of len(path)+1 for the index. // // Arguments: // path = path to cut // dists = distances where the path should be cut (a list) or a scalar single distance +// --- // closed = set to true if the curve is closed. Default: false // direction = set to true to return direction vectors. Default: false // // Example(NORENDER): // square=[[0,0],[1,0],[1,1],[0,1]]; -// path_cut(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] -// path_cut(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] -// path_cut(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] -// path_cut(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] -function path_cut(path, dists, closed=false, direction=false) = +// path_cut_points(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] +// path_cut_points(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] +// path_cut_points(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] +// path_cut_points(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] +function path_cut_points(path, dists, closed=false, direction=false) = let(long_enough = len(path) >= (closed ? 3 : 2)) assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points") - !is_list(dists)? path_cut(path, [dists],closed, direction)[0] : - let(cuts = _path_cut(path,dists,closed)) - !direction ? cuts : let( - dir = _path_cuts_dir(path, cuts, closed), - normals = _path_cuts_normals(path, cuts, dir, closed) - ) zip(cuts, array_group(dir,1), array_group(normals,1)); + is_num(dists) ? path_cut_points(path, [dists],closed, direction)[0] : + assert(is_vector(dists)) + assert(list_increasing(dists), "Cut distances must be an increasing list") + let(cuts = _path_cut_points(path,dists,closed)) + !direction + ? cuts + : let( + dir = _path_cuts_dir(path, cuts, closed), + normals = _path_cuts_normals(path, cuts, dir, closed) + ) + hstack(cuts, array_group(dir,1), array_group(normals,1)); // Main recursive path cut function -function _path_cut(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = +function _path_cut_points(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = dind == len(dists) ? result : let( - lastpt = len(result)>0? select(result,-1)[0] : [], - dpartial = len(result)==0? 0 : norm(lastpt-path[pind]), - nextpoint = dpartial > dists[dind]-dtotal? - [lerp(lastpt,path[pind], (dists[dind]-dtotal)/dpartial),pind] : - _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) - ) is_undef(nextpoint)? - concat(result, repeat(undef,len(dists)-dind)) : - _path_cut(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); + lastpt = len(result)==0? [] : select(result,-1)[0], // location of last cut point + dpartial = len(result)==0? 0 : norm(lastpt-select(path,pind)), // remaining length in segment + nextpoint = dists[dind] < dpartial+dtotal // Do we have enough length left on the current segment? + ? [lerp(lastpt,select(path,pind),(dists[dind]-dtotal)/dpartial),pind] + : _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) + ) + _path_cut_points(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); + // Search for a single cut point in the path function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) = - ind>=len(path)? undef : - ind==len(path)-1 && !closed? (dist dist ? + // If we get to the very end of the path (ind is last point or wraparound for closed case) then + // check if we are within epsilon of the final path point. If not we're out of path, so we fail + ind==len(path)-(closed?0:1) ? + assert(dist dist ? [lerp(path[ind],select(path,ind+1),dist/d), ind+1] : _path_cut_single(path, dist-d,closed, ind+1, eps); @@ -1138,18 +1383,67 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = zeros = path[0]*0, nextind = cuts[ind][1], nextpath = unit(select(path, nextind+1)-select(path, nextind),zeros), - thispath = unit(select(path, nextind) - path[nextind-1],zeros), - lastpath = unit(path[nextind-1] - select(path, nextind-2),zeros), + thispath = unit(select(path, nextind) - select(path,nextind-1),zeros), + lastpath = unit(select(path,nextind-1) - select(path, nextind-2),zeros), nextdir = nextind==len(path) && !closed? lastpath : - (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps)? - unit(nextpath+thispath) : - (nextind>1 || closed) && approx(cuts[ind][0],path[nextind-1],eps)? - unit(thispath+lastpath) : - thispath + (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps) + ? unit(nextpath+thispath) + : (nextind>1 || closed) && approx(cuts[ind][0],select(path,nextind-1),eps) + ? unit(thispath+lastpath) + : thispath ) nextdir ]; + +// Function: path_cut() +// Topics: Paths +// See Also: path_cut_points() +// Usage: +// path_list = path_cut(path, cutdist, ); +// Description: +// Given a list of distances in `cutdist`, cut the path into +// subpaths at those lengths, returning a list of paths. +// If the input path is closed then the final path will include the +// original starting point. The list of cut distances must be +// in ascending order. If you repeat a distance you will get an +// empty list in that position in the output. +// Arguments: +// path = The original path to split. +// cutdist = Distance or list of distances where path is cut +// closed = If true, treat the path as a closed polygon. +// Example(2D): +// path = circle(d=100); +// segs = path_cut(path, [50, 200], closed=true); +// rainbow(segs) stroke($item); +function path_cut(path,cutdist,closed) = + is_num(cutdist) ? path_cut(path,[cutdist],closed) : + assert(is_vector(cutdist)) + assert(select(cutdist,-1)0, "Cut distances must be strictly positive") + let( + cutlist = path_cut_points(path,cutdist,closed=closed), + cuts = len(cutlist) + ) + [ + [ each slice(path,0,cutlist[0][1]), + if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0] + ], + for(i=[0:1:cuts-2]) + cutlist[i][0]==cutlist[i+1][0] ? [] + : + [ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0], + each slice(path,cutlist[i][1], cutlist[i+1][1]), + if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0], + ], + [ + if (!approx(cutlist[cuts-1][0], select(path,cutlist[cuts-1][1]))) cutlist[cuts-1][0], + each select(path,cutlist[cuts-1][1],closed ? 0 : -1) + ] + ]; + + + // Input `data` is a list that sums to an integer. // Returns rounded version of input data so that every // entry is rounded to an integer and the sum is the same as @@ -1220,7 +1514,7 @@ function _sum_preserving_round(data, index=0) = // Example(2D): With `exact=false` you can also get extra points, here 20 instead of requested 18 // mypath = subdivide_path(pentagon(side=2), 18, exact=false); // move_copies(mypath)circle(r=.1,$fn=32); -// Example(FlatSpin): Three-dimensional paths also work +// Example(FlatSpin,VPD=15,VPT=[0,0,1.5]): Three-dimensional paths also work // mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); // move_copies(mypath)sphere(r=.1,$fn=32); function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = @@ -1310,7 +1604,7 @@ function resample_path(path, N, spacing, closed=false) = N = is_def(N) ? N : round(length/spacing) + (closed?0:1), spacing = length/(closed?N:N-1), // Note: worried about round-off error, so don't include distlist = list_range(closed?N:N-1,step=spacing), // last point when closed=false - cuts = path_cut(path, distlist, closed=closed) + cuts = path_cut_points(path, distlist, closed=closed) ) concat(subindex(cuts,0),closed?[]:[select(path,-1)]); // Then add last point here diff --git a/phillips_drive.scad b/phillips_drive.scad index 3288722f..9ab829ad 100644 --- a/phillips_drive.scad +++ b/phillips_drive.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: phillips_drive.scad // Phillips driver bits -// To use, add these lines to the top of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// diff --git a/polyhedra.scad b/polyhedra.scad index a4cf00e5..ac0d575f 100644 --- a/polyhedra.scad +++ b/polyhedra.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: polyhedra.scad // Useful platonic, archimedian, and catalan polyhedra. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -123,6 +121,7 @@ function _unique_groups(m) = [ // // Arguments: // name = Name of polyhedron to create. +// --- // index = Index to select from polyhedron list. Default: 0. // type = Type of polyhedron: "platonic", "archimedean", "catalan". // faces = Number of faces. @@ -216,7 +215,7 @@ function _unique_groups(m) = [ // color("red") sphere(r=.1); // color("green") sphere(r=.1); // } -// Example(FlatSpin): Difference the children from the polyhedron; children depend on $faceindex +// Example(FlatSpin,VPD=100): Difference the children from the polyhedron; children depend on $faceindex // difference(){ // regular_polyhedron("tetrahedron", side=25); // regular_polyhedron("tetrahedron", side=25,draw=false) @@ -228,7 +227,7 @@ function _unique_groups(m) = [ // cylinder(r=.1, h=.5); // right(2) regular_polyhedron(name="tetrahedron", anchor=UP, rotate_children=false) // cylinder(r=.1, h=.5); -// Example(FlatSpin,Med): Using `$face` you can have full control of the construction of your children. This example constructs the Great Icosahedron. +// Example(FlatSpin,Med,VPD=15): Using `$face` you can have full control of the construction of your children. This example constructs the Great Icosahedron. // module makestar(pts) { // Make a star from a point list // polygon( // [ @@ -566,6 +565,7 @@ _stellated_polyhedra_ = [ // // Arguments: // name = Name of polyhedron to create. +// --- // index = Index to select from polyhedron list. Default: 0. // type = Type of polyhedron: "platonic", "archimedean", "catalan". // faces = Number of faces. @@ -689,7 +689,7 @@ function regular_polyhedron_info( boundtable = [bounds[0], [0,0,0], bounds[1]], translation = [for(i=[0:2]) -boundtable[1+anchor[i]][i]], face_normals = rot(p=faces_normals_vertices[1], from=down_direction, to=[0,0,-1]), - side_length = scalefactor * entry[edgelen] + radius_scale = name=="trapezohedron" ? 1 : scalefactor * entry[edgelen] ) info == "fullentry" ? [ scaled_points, @@ -697,15 +697,15 @@ function regular_polyhedron_info( stellate ? faces : face_triangles, faces, face_normals, - side_length*entry[in_radius] + radius_scale*entry[in_radius] ] : info == "vnf" ? [move(translation,p=scaled_points), stellate ? faces : face_triangles] : info == "vertices" ? move(translation,p=scaled_points) : info == "faces" ? faces : info == "face normals" ? face_normals : - info == "in_radius" ? side_length * entry[in_radius] : - info == "mid_radius" ? side_length * entry[mid_radius] : - info == "out_radius" ? side_length * entry[out_radius] : + info == "in_radius" ? radius_scale * entry[in_radius] : + info == "mid_radius" ? radius_scale * entry[mid_radius] : + info == "out_radius" ? radius_scale * entry[out_radius] : info == "index set" ? indexlist : info == "face vertices" ? (stellate==false? entry[facevertices] : [3]) : info == "edge length" ? scalefactor * entry[edgelen] : @@ -736,8 +736,8 @@ function _trapezohedron(faces, r, side, longside, h, d) = parmcount = num_defined([r,side,longside,h]) ) assert(parmcount==2,"Must define exactly two of 'r', 'side', 'longside', and 'height'") - let( - separation = ( + let( + separation = ( // z distance between non-apex vertices that aren't in the same plane !is_undef(h) ? 2*h*sqr(tan(90/N)) : (!is_undef(r) && !is_undef(side))? sqrt(side*side+2*r*r*(cos(180/N)-1)) : (!is_undef(r) && !is_undef(longside))? 2 * sqrt(sqr(longside)-sqr(r)) / (1-sqr(tan(90/N))) * sqr(tan(90/N)) : @@ -755,7 +755,7 @@ function _trapezohedron(faces, r, side, longside, h, d) = top = [for(i=[0:1:N-1]) [r*cos(360/N*i), r*sin(360/N*i),separation/2]], bot = [for(i=[0:1:N-1]) [r*cos(180/N+360/N*i), r*sin(180/N+360/N*i),-separation/2]], vertices = concat([[0,0,h],[0,0,-h]],top,bot) - ) [ + ) [ "trapezohedron", "trapezohedron", faces, [4], !is_undef(side)? side : sqrt(sqr(separation)-2*r*(cos(180/N)-1)), // actual side length h*r/sqrt(r*r+sqr(h+separation/2)), // in_radius diff --git a/primitives.scad b/primitives.scad index 98d63678..de965798 100644 --- a/primitives.scad +++ b/primitives.scad @@ -2,10 +2,8 @@ // LibFile: primitives.scad // The basic built-in shapes, reworked to integrate better with // other BOSL2 library shapes and utilities. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// @@ -13,14 +11,19 @@ // Function&Module: square() -// Usage: -// square(size, [center]) +// Topics: Shapes (2D), Path Generators (2D) +// Usage: As a Built-in Module +// square(size,

); +// Usage: As a Function +// path = square(size,
); +// See Also: rect() // Description: // When called as the builtin module, creates a 2D square or rectangle of the given size. // When called as a function, returns a 2D path/list of points for a square/rectangle of the given size. // Arguments: // size = The size of the square to create. If given as a scalar, both X and Y will be the same size. // center = If given and true, overrides `anchor` to be `CENTER`. If given and false, overrides `anchor` to be `FRONT+LEFT`. +// --- // anchor = (Function only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = (Function only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example(2D): @@ -45,14 +48,19 @@ function square(size=1, center, anchor, spin=0) = // Function&Module: circle() -// Usage: -// circle(r|d) +// Topics: Shapes (2D), Path Generators (2D) +// Usage: As a Built-in Module +// circle(r|d=, ...); +// Usage: As a Function +// path = circle(r|d=, ...); +// See Also: oval() // Description: // When called as the builtin module, creates a 2D polygon that approximates a circle of the given size. // When called as a function, returns a 2D list of points (path) for a polygon that approximates a circle of the given size. // Arguments: // r = The radius of the circle to create. // d = The diameter of the circle to create. +// --- // anchor = (Function only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = (Function only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example(2D): By Radius @@ -74,10 +82,14 @@ function circle(r, d, anchor=CENTER, spin=0) = // Function&Module: cube() +// Topics: Shapes (3D), Attachable, VNF Generators // Usage: As Module -// cube(size, [center]); +// cube(size,
, ...); +// Usage: With Attachments +// cube(size,
, ...) { attachments } // Usage: As Function -// vnf = cube(size, [center]); +// vnf = cube(size,
, ...); +// See Also: cuboid(), prismoid() // Description: // Creates a 3D cubic object with support for anchoring and attachments. // This can be used as a drop-in replacement for the built-in `cube()` module. @@ -85,6 +97,7 @@ function circle(r, d, anchor=CENTER, spin=0) = // Arguments: // size = The size of the cube. // center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=ALLNEG`. +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -140,12 +153,16 @@ function cube(size=1, center, anchor, spin=0, orient=UP) = // Function&Module: cylinder() +// Topics: Shapes (3D), Attachable, VNF Generators // Usage: As Module -// cylinder(h, r|d, [center]); -// cylinder(h, r1/d1, r2/d2, [center]); +// cylinder(h, r=/d=, , ...); +// cylinder(h, r1/d1=, r2/d2=, , ...); +// Usage: With Attachments +// cylinder(h, r=/d=, ) {attachments} // Usage: As Function -// vnf = cylinder(h, r|d, [center]); -// vnf = cylinder(h, r1/d1, r2/d2, [center]); +// vnf = cylinder(h, r=/d=, , ...); +// vnf = cylinder(h, r1/d1=, r2/d2=, , ...); +// See Also: cyl() // Description: // Creates a 3D cylinder or conic object with support for anchoring and attachments. // This can be used as a drop-in replacement for the built-in `cylinder()` module. @@ -155,6 +172,7 @@ function cube(size=1, center, anchor, spin=0, orient=UP) = // r1 = The bottom radius of the cylinder. (Before orientation.) // r2 = The top radius of the cylinder. (Before orientation.) // center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=BOTTOM`. +// --- // d1 = The bottom diameter of the cylinder. (Before orientation.) // d2 = The top diameter of the cylinder. (Before orientation.) // r = The radius of the cylinder. @@ -232,16 +250,21 @@ function cylinder(h, r1, r2, center, l, r, d, d1, d2, anchor, spin=0, orient=UP) // Function&Module: sphere() +// Topics: Shapes (3D), Attachable, VNF Generators // Usage: As Module -// sphere(r|d, [circum], [style]) +// sphere(r|d=, , , ...); +// Usage: With Attachments +// sphere(r|d=, ...) { attachments } // Usage: As Function -// vnf = sphere(r|d, [circum], [style]) +// vnf = sphere(r|d=, , , ...); +// See Also: spheroid() // Description: // Creates a sphere object, with support for anchoring and attachments. // This is a drop-in replacement for the built-in `sphere()` module. // When called as a function, returns a [VNF](vnf.scad) for a sphere. // Arguments: // r = Radius of the sphere. +// --- // d = Diameter of the sphere. // circum = If true, the sphere is made large enough to circumscribe the sphere of the ideal side. Otherwise inscribes. Default: false (inscribes) // style = The style of the sphere's construction. One of "orig", "aligned", "stagger", "octa", or "icosa". Default: "orig" diff --git a/quaternions.scad b/quaternions.scad index e21801c1..eaf9e16e 100644 --- a/quaternions.scad +++ b/quaternions.scad @@ -1,10 +1,8 @@ /////////////////////////////////////////// // LibFile: quaternions.scad // Support for Quaternions. -// To use, add the following line to the beginning of your file: -// ``` +// Includes: // include -// ``` /////////////////////////////////////////// @@ -403,7 +401,7 @@ function Q_Angle(q1,q2) = // When called as a module, rotates all children by the rotation stored in quaternion `q`. // When called as a function with a `p` argument, rotates the point or list of points in `p` by the rotation stored in quaternion `q`. // When called as a function without a `p` argument, returns the affine3d rotation matrix for the rotation stored in quaternion `q`. -// Example(FlatSpin): +// Example(FlatSpin,VPD=225,VPT=[71,-26,16]): // module shape() translate([80,0,0]) cube([10,10,1]); // q = QuatXYZ([90,-15,-45]); // Qrot(q) shape(); @@ -469,7 +467,7 @@ function Q_Rotation(R) = _Qnorm( _Qset( [ (R[2][0]+R[0][2]), (R[1][2]+R[2][1]), 4*r ], (R[1][0]-R[0][1])) ) ; -// Function&Module: Q_Rotation_path(q1, n, [q2]) +// Function&Module: Q_Rotation_path() // Usage: As a function // path = Q_Rotation_path(q1, n, q2); // path = Q_Rotation_path(q1, n); diff --git a/queues.scad b/queues.scad index f2e2d000..0a496140 100644 --- a/queues.scad +++ b/queues.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: queues.scad // Queue data structure implementation. -// To use, add the following lines to the beginning of your file: -// ``` -// use -// use -// ``` +// Includes: +// include +// include ////////////////////////////////////////////////////////////////////// diff --git a/regions.scad b/regions.scad index acd66d98..b4befba3 100644 --- a/regions.scad +++ b/regions.scad @@ -1,10 +1,8 @@ ////////////////////////////////////////////////////////////////////// // LibFile: regions.scad // Regions and 2D boolean geometry -// To use, add the following lines to the beginning of your file: -// ``` -// use -// ``` +// Includes: +// include ////////////////////////////////////////////////////////////////////// @@ -60,35 +58,34 @@ module region(r) // Function: check_and_fix_path() // Usage: -// check_and_fix_path(path, [valid_dim], [closed]) +// check_and_fix_path(path, [valid_dim], [closed], [name]) // Description: // Checks that the input is a path. If it is a region with one component, converts it to a path. +// Note that arbitrary paths must have at least two points, but closed paths need at least 3 points. // valid_dim specfies the allowed dimension of the points in the path. -// If the path is closed, removed duplicate endpoint if present. +// If the path is closed, removes duplicate endpoint if present. // Arguments: // path = path to process // valid_dim = list of allowed dimensions for the points in the path, e.g. [2,3] to require 2 or 3 dimensional input. If left undefined do not perform this check. Default: undef // closed = set to true if the path is closed, which enables a check for endpoint duplication -function check_and_fix_path(path, valid_dim=undef, closed=false) = +// name = parameter name to use for reporting errors. Default: "path" +function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = let( - path = is_region(path)? ( - assert(len(path)==1,"Region supplied as path does not have exactly one component") - path[0] - ) : ( - assert(is_path(path), "Input is not a path") - path - ), - dim = array_dim(path) + path = + is_region(path)? + assert(len(path)==1,str("Region ",name," supplied as path does not have exactly one component")) + path[0] + : + assert(is_path(path), str("Input ",name," is not a path")) + path ) - assert(dim[0]>1,"Path must have at least 2 points") - assert(len(dim)==2,"Invalid path: path is either a list of scalars or a list of matrices") - assert(is_def(dim[1]), "Invalid path: entries in the path have variable length") - let(valid=is_undef(valid_dim) || in_list(dim[1],valid_dim)) + assert(len(path)>(closed?2:1),closed?str("Closed path ",name," must have at least 3 points") + :str("Path ",name," must have at least 2 points")) + let(valid=is_undef(valid_dim) || in_list(len(path[0]),force_list(valid_dim))) assert( valid, str( - "The points on the path have length ", - dim[1], " but length must be ", - len(valid_dim)==1? valid_dim[0] : str("one of ",valid_dim) + "Input ",name," must has dimension ", len(path[0])," but dimension must be ", + is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim ) ) closed && approx(path[0],select(path,-1))? slice(path,0,-2) : path; @@ -124,6 +121,76 @@ function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = ) pip==0? 0 : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); +// Function: polygons_equal() +// Usage: +// b = polygons_equal(poly1, poly2, ) +// Description: +// Returns true if the components of region1 and region2 are the same polygons +// within given epsilon tolerance. +// Arguments: +// poly1 = first polygon +// poly2 = second polygon +// eps = tolerance for comparison +// Example(NORENDER): +// polygons_equal(pentagon(r=4), +// rot(360/5, p=pentagon(r=4))); // returns true +// polygons_equal(pentagon(r=4), +// rot(90, p=pentagon(r=4))); // returns false +function polygons_equal(poly1, poly2, eps=EPSILON) = + let( + poly1 = cleanup_path(poly1), + poly2 = cleanup_path(poly2), + l1 = len(poly1), + l2 = len(poly2) + ) l1 != l2 ? false : + let( maybes = find_first_match(poly1[0], poly2, eps=eps, all=true) ) + maybes == []? false : + [for (i=maybes) if (__polygons_equal(poly1, poly2, eps, i)) 1] != []; + +function __polygons_equal(poly1, poly2, eps, st) = + max([for(d=poly1-select(poly2,st,st-1)) d*d])= len(polys)? false : + polygons_equal(poly, polys[i])? true : + __poly_in_polygons(poly, polys, i+1); + + +// Function: regions_equal() +// Usage: +// b = regions_equal(region1, region2, ) +// Description: +// Returns true if the components of region1 and region2 are the same polygons +// within given epsilon tolerance. +// Arguments: +// poly1 = first polygon +// poly2 = second polygon +// eps = tolerance for comparison +function regions_equal(region1, region2) = + assert(is_region(region1) && is_region(region2)) + len(region1) != len(region2)? false : + __regions_equal(region1, region2, 0); + +function __regions_equal(region1, region2, i) = + i >= len(region1)? true : + !poly_in_polygons(region1[i], region2)? false : + __regions_equal(region1, region2, i+1); + + // Function: region_path_crossings() // Usage: // region_path_crossings(path, region); @@ -369,7 +436,7 @@ function linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg for (path=rgn) let( p = cleanup_path(path), path = is_undef(maxseg)? p : [ - for (seg=pair_wrap(p)) each + for (seg=pair(p,true)) each let(steps=ceil(norm(seg.y-seg.x)/maxseg)) lerp(seg.x, seg.y, [0:1/steps:1-EPSILON]) ] @@ -382,7 +449,7 @@ function linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg for (pathnum = idx(rgn)) let( p = cleanup_path(rgn[pathnum]), path = is_undef(maxseg)? p : [ - for (seg=pair_wrap(p)) each + for (seg=pair(p,true)) each let(steps=ceil(norm(seg.y-seg.x)/maxseg)) lerp(seg.x, seg.y, [0:1/steps:1-EPSILON]) ], @@ -535,7 +602,7 @@ function _offset_region( difference(_acc, [ offset( paths[_i].y, - r=-r, delta=-delta, chamfer=chamfer, closed=closed, + r=u_mul(-1,r), delta=u_mul(-1,delta), chamfer=chamfer, closed=closed, maxstep=maxstep, check_valid=check_valid, quality=quality, return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces @@ -549,11 +616,14 @@ function _offset_region( // Function: offset() -// +// Usage: +// offsetpath = offset(path, [r|delta], [chamfer], [closed], [check_valid], [quality]) +// path_faces = offset(path, return_faces=true, [r|delta], [chamfer], [closed], [check_valid], [quality], [firstface_index], [flip_faces]) // Description: // Takes an input path and returns a path offset by the specified amount. As with the built-in // offset() module, you can use `r` to specify rounded offset and `delta` to specify offset with -// corners. Positive offsets shift the path to the left (relative to the direction of the path). +// corners. If you used `delta` you can set `chamfer` to true to get chamfers. +// Positive offsets shift the path to the left (relative to the direction of the path). // . // When offsets shrink the path, segments cross and become invalid. By default `offset()` checks // for this situation. To test validity the code checks that segments have distance larger than (r @@ -572,6 +642,7 @@ function _offset_region( // value is a list: [offset_path, face_list]. // Arguments: // path = the path to process. A list of 2d points. +// --- // r = offset radius. Distance to offset. Will round over corners. // delta = offset distance. Distance to offset with pointed corners. // chamfer = chamfer corners when you specify `delta`. Default: false @@ -645,7 +716,7 @@ function offset( maxstep=0.1, closed=false, check_valid=true, quality=1, return_faces=false, firstface_index=0, flip_faces=false -) = +) = is_region(path)? ( assert(!return_faces, "return_faces not supported for regions.") let( @@ -689,23 +760,21 @@ function offset( (len(sharpcorners)==2 && !closed) || all_defined(select(sharpcorners,closed?0:1,-1)) ) - assert(parallelcheck, "Path turns back on itself (180 deg turn)") + assert(parallelcheck, "Path contains sequential parallel segments (either 180 deg turn or 0 deg turn") let( // This is a boolean array that indicates whether a corner is an outside or inside corner // For outside corners, the newcorner is an extension (angle 0), for inside corners, it turns backward // If either side turns back it is an inside corner---must check both. // Outside corners can get rounded (if r is specified and there is space to round them) - outsidecorner = [ - for(i=[0:len(goodsegs)-1]) let( - prevseg=select(goodsegs,i-1) - ) ( - (goodsegs[i][1]-goodsegs[i][0]) * - (goodsegs[i][0]-sharpcorners[i]) > 0 - ) && ( - (prevseg[1]-prevseg[0]) * - (sharpcorners[i]-prevseg[1]) > 0 - ) - ], + outsidecorner = len(sharpcorners)==2 ? [false,false] + : + [for(i=[0:len(goodsegs)-1]) + let(prevseg=select(goodsegs,i-1)) + i==0 && !closed ? false // In open case first entry is bogus + : + (goodsegs[i][1]-goodsegs[i][0]) * (goodsegs[i][0]-sharpcorners[i]) > 0 + && (prevseg[1]-prevseg[0]) * (sharpcorners[i]-prevseg[1]) > 0 + ], steps = is_def(delta) ? [] : [ for(i=[0:len(goodsegs)-1]) r==0 ? 0 : diff --git a/rounding.scad b/rounding.scad index 1d0086c5..afebedb4 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1,22 +1,21 @@ -////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// // LibFile: rounding.scad // Routines to create rounded corners, with either circular rounding, // or continuous curvature rounding with no sudden curvature transitions. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// include include + // Section: Functions // Function: round_corners() // // Usage: -// round_corners(path, [method], [radius], [cut], [joint], [closed], [verbose]); +// rounded_path = round_corners(path, , , , , , ); // // Description: // Takes a 2D or 3D path as input and rounds each corner @@ -26,25 +25,29 @@ include // tactile "bump" where the curvature changes from flat to circular. // See https://hackernoon.com/apples-icons-have-that-shape-for-a-very-good-reason-720d4e7c8a14 // . -// You select the type of rounding using the `method` option, which should be `"smooth"` to +// You select the type of rounding using the `method` parameter, which should be `"smooth"` to // get continuous curvature rounding, `"circle"` to get circular rounding, or `"chamfer"` to get chamfers. The default is circle -// rounding. Each method has two options you can use to specify the amount of rounding. -// All of the rounding methods accept the cut option. This mode specifies the distance from the unrounded corner to the rounded tip, so how +// rounding. Each method accepts multiple options to specify the amount of rounding. +// . +// The `cut` parameter specifies the distance from the unrounded corner to the rounded tip, so how // much of the corner to "cut" off. This can be easier to understand than setting a circular radius, which can be // unexpectedly extreme when the corner is very sharp. It also allows a systematic specification of // corner treatments that are the same size for all three methods. // . -// For circular rounding you can also use the `radius` parameter, which sets a circular rounding -// radius. For chamfers and smooth rounding you can specify the `joint` parameter, which specifies the distance +// The `joint` parameter specifies the distance // away from the corner along the path where the roundover or chamfer should start. The figure below shows -// the cut and joint distances for a given roundover. +// the cut and joint distances for a given roundover. This parameter is good for ensuring that your roundover will +// fit on the polygon, since you can easily tell whether adjacent corner treatments will interfere. +// . +// For circular rounding you can also use the `radius` parameter, which sets a circular rounding +// radius. // . // The `"smooth"` method rounding also has a parameter that specifies how smooth the curvature match // is. This parameter, `k`, ranges from 0 to 1, with a default of 0.5. Larger values give a more // abrupt transition and smaller ones a more gradual transition. If you set the value much higher // than 0.8 the curvature changes abruptly enough that though it is theoretically continuous, it may // not be continuous in practice. If you set it very small then the transition is so gradual that -// the length of the roundover may be extremely long. +// the length of the roundover may be extremely long. // . // If you select curves that are too large to fit the function will fail with an error. You can set `verbose=true` to // get a message showing a list of scale factors you can apply to your rounding parameters so that the @@ -56,6 +59,10 @@ include // of the curve are not rounded. In this case you can specify a full list of points anyway, and the endpoint values are ignored, // or you can specify a list that has length len(path)-2, omitting the two dummy values. // . +// If your input path includes collinear points you must use a cut or radius value of zero for those "corners". You can +// choose a nonzero joint parameter when the collinear points form a 180 degree angle. This will cause extra points to be inserted. +// If the collinear points form a spike (0 degree angle) then round_corners will fail. +// . // Examples: // * `method="circle", radius=2`: // Rounds every point with circular, radius 2 roundover @@ -68,10 +75,11 @@ include // circular roundovers. For continuous curvature roundovers `$fs` and `$fn` are used and `$fa` is // ignored. Note that $fn is interpreted as the number of points on the roundover curve, which is // not equivalent to its meaning for rounding circles because roundovers are usually small fractions -// of a circular arc. When doing continuous curvature rounding be sure to use lots of segments or the effect -// will be hidden by the discretization. +// of a circular arc. As usual, $fn overrides $fs. When doing continuous curvature rounding be sure to use lots of segments or the effect +// will be hidden by the discretization. Note that if you use $fn with "smooth" then $fn points are added at each corner, even +// if the "corner" is flat, with collinear points, so this guarantees a specific output length. // -// Figure(2DMed): +// Figure(2D,Med): // h = 18; // w = 12.6; // example = [[0,0],[w,h],[2*w,0]]; @@ -86,6 +94,7 @@ include // Arguments: // path = list of 2d or 3d points defining the path to be rounded. // method = rounding method to use. Set to "chamfer" for chamfers, "circle" for circular rounding and "smooth" for continuous curvature 4th order bezier rounding. Default: "circle" +// --- // radius = rounding radius, only compatible with `method="circle"`. Can be a number or vector. // cut = rounding cut distance, compatible with all methods. Can be a number or vector. // joint = rounding joint distance, compatible with `method="chamfer"` and `method="smooth"`. Can be a number or vector. @@ -93,38 +102,38 @@ include // closed = if true treat the path as a closed polygon, otherwise treat it as open. Default: true. // verbose = if true display rounding scale factors that show how close roundovers are to overlapping. Default: false // -// Example(Med2D): Standard circular roundover with radius the same at every point. Compare results at the different corners. +// Example(2D,Med): Standard circular roundover with radius the same at every point. Compare results at the different corners. // $fn=36; // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // polygon(round_corners(shape, radius=1)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Circular roundover using the "cut" specification, the same at every corner. +// Example(2D,Med): Circular roundover using the "cut" specification, the same at every corner. // $fn=36; // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // polygon(round_corners(shape, cut=1)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Continous curvature roundover using "cut", still the same at every corner. The default smoothness parameter of 0.5 was too gradual for these roundovers to fit, but 0.7 works. +// Example(2D,Med): Continous curvature roundover using "cut", still the same at every corner. The default smoothness parameter of 0.5 was too gradual for these roundovers to fit, but 0.7 works. // $fn=36; // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // polygon(round_corners(shape, method="smooth", cut=1, k=0.7)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Continuous curvature roundover using "joint", for the last time the same at every corner. Notice how small the roundovers are. +// Example(2D,Med): Continuous curvature roundover using "joint", for the last time the same at every corner. Notice how small the roundovers are. // $fn=36; // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // polygon(round_corners(shape, method="smooth", joint=1, k=0.7)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Circular rounding, different at every corner, some corners left unrounded +// Example(2D,Med): Circular rounding, different at every corner, some corners left unrounded // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // radii = [1.8, 0, 2, 0.3, 1.2, 0]; // polygon(round_corners(shape, radius = radii,$fn=64)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Continuous curvature rounding, different at every corner, with varying smoothness parameters as well, and `$fs` set very small. Note that `$fa` is ignored here with method set to "smooth". +// Example(2D,Med): Continuous curvature rounding, different at every corner, with varying smoothness parameters as well, and `$fs` set very small. Note that `$fa` is ignored here with method set to "smooth". // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // cuts = [1.5,0,2,0.3, 1.2, 0]; // k = [0.6, 0.5, 0.5, 0.7, 0.3, 0.5]; // polygon(round_corners(shape, method="smooth", cut=cuts, k=k, $fs=0.1)); // color("red") down(.1) polygon(shape); -// Example(Med2D): Chamfers +// Example(2D,Med): Chamfers // $fn=36; // shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]]; // polygon(round_corners(shape, method="chamfer", cut=1)); @@ -144,12 +153,12 @@ include // translate([60,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.32, $fn=96)); // translate([0,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.7, $fn=96)); // } -// Example(Med2D): Rounding a path that is not closed in a three different ways. +// Example(2D,Med): Rounding a path that is not closed in a three different ways. // $fs=.1; // $fa=1; // zigzagx = [-10, 0, 10, 20, 29, 38, 46, 52, 59, 66, 72, 78, 83, 88, 92, 96, 99, 102, 112]; // zigzagy = concat([0], flatten(repeat([-10,10],8)), [-10,0]); -// zig = zip(zigzagx,zigzagy); +// zig = hstack(zigzagx,zigzagy); // stroke(zig,width=1); // Original shape // fwd(20) // Smooth size corners with a cut of 4 and curvature parameter 0.6 // stroke(round_corners(zig,cut=4, k=0.6, method="smooth", closed=false),width=1); @@ -158,7 +167,7 @@ include // // Smooth size corners with a circular arc and radius 1.5 (close to maximum possible) // fwd(60) // Note how the different points are cut back by different amounts // stroke(round_corners(zig,radius=1.5,closed=false),width=1); -// Example(FlatSpin): Rounding some random 3D paths +// Example(FlatSpin,VPD=42,VPT=[7.75,6.69,5.22]): Rounding some random 3D paths // $fn=36; // list1= [ // [2.887360, 4.03497, 6.372090], @@ -179,24 +188,45 @@ include // path_sweep(regular_ngon(n=36,or=.1),round_corners(list1,closed=false, method="smooth", cut = 0.65)); // right(6) // path_sweep(regular_ngon(n=36,or=.1),round_corners(list2,closed=false, method="circle", cut = 0.75)); -// Example(FlatSpin): Rounding a spiral with increased rounding along the length +// Example(3D,Med): Rounding a spiral with increased rounding along the length // // Construct a square spiral path in 3D // $fn=36; // square = [[0,0],[1,0],[1,1],[0,1]]; // spiral = flatten(repeat(concat(square,reverse(square)),5)); // Squares repeat 10 times, forward and backward -// squareind = [for(i=[0:9]) each [i,i,i,i]]; // Index of the square for each point +// squareind = [for(i=[0:9]) each [i,i,i,i]]; // Index of the square for each point // z = list_range(40)*.2+squareind; -// path3d = zip(spiral,z); // 3D spiral +// path3d = hstack(spiral,z); // 3D spiral // rounding = squareind/20; // // Setting k=1 means curvature won't be continuous, but curves are as round as possible // // Try changing the value to see the effect. // rpath = round_corners(path3d, joint=rounding, k=1, method="smooth", closed=false); // path_sweep( regular_ngon(n=36, or=.1), rpath); +// Example(2D): The rounding invocation that is commented out gives an error because the rounding parameters interfere with each other. The error message gives a list of factors that can help you fix this: [0.852094, 0.852094, 1.85457, 10.1529] +// $fn=64; +// path = [[0, 0],[10, 0],[20, 20],[30, -10]]; +// debug_polygon(path); +// //polygon(round_corners(path,cut = [1,3,1,1], method="circle")); +// Example(2D): The list of factors shows that the problem is in the first two rounding values, because the factors are smaller than one. If we multiply the first two parameters by 0.85 then the roundings fit. The verbose option gives us the same fit factors. +// $fn=64; +// path = [[0, 0],[10, 0],[20, 20],[30, -10]]; +// polygon(round_corners(path,cut = [0.85,3*0.85,1,1], method="circle", verbose=true)); +// Example(2D): From the fit factors we can see that rounding at vertices 2 and 3 could be increased a lot. Applying those factors we get this more rounded shape. The new fit factors show that we can still further increase the rounding parameters if we wish. +// $fn=64; +// path = [[0, 0],[10, 0],[20, 20],[30, -10]]; +// polygon(round_corners(path,cut = [0.85,3*0.85,2.13, 10.15], method="circle",verbose=true)); +// Example(2D): Using the `joint` parameter it's easier to understand whether your roundvers will fit. We can guarantee a fairly large roundover on any path by picking each one to use up half the segment distance along the shorter of its two segments: +// $fn=64; +// path = [[0, 0],[10, 0],[20, 20],[30, -10]]; +// path_len = path_segment_lengths(path,closed=true); +// halflen = [for(i=idx(path)) min(select(path_len,i-1,i))/2]; +// polygon(round_corners(path,joint = halflen, method="circle",verbose=true)); + +module round_corners(path, method="circle", radius, cut, joint, k, closed=true, verbose=false) {no_module();} function round_corners(path, method="circle", radius, cut, joint, k, closed=true, verbose=false) = assert(in_list(method,["circle", "smooth", "chamfer"]), "method must be one of \"circle\", \"smooth\" or \"chamfer\"") let( default_k = 0.5, - size=one_defined([radius, cut, joint], ["radius", "cut", "joint"]), + size=one_defined([radius, cut, joint], "radius,cut,joint"), path = is_region(path)? assert(len(path)==1, "Region supplied as path does not have exactly one component") path[0] : path, @@ -211,7 +241,6 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true assert(k_ok,method=="smooth" ? str("Input k must be a number or list with length ",len(path), closed?"":str(" or ",len(path)-2)) : "Input k is only allowed with method=\"smooth\"") assert(method=="circle" || measure!="radius", "radius parameter allowed only with method=\"circle\"") - assert(method!="circle" || measure!="joint", "joint parameter not allowed with method=\"circle\"") let( parm = is_num(size) ? repeat(size, len(path)) : len(size)=1,"Roundovers are too big for the path") + assert(min(scalefactors)>=1,str("Roundovers are too big for the path. If you multitply them by this vector they should fit: ",scalefactors)) [ for(i=[0:1:len(path)-1]) each (dk[i][0] == 0)? [path[i]] : @@ -308,7 +346,7 @@ function _bezcorner(points, parm) = ] : _smooth_bez_fill(points,parm), N = max(3,$fn>0 ?$fn : ceil(bezier_segment_length(P)/$fs)) ) - bezier_curve(P,N); + bezier_curve(P,N,endpoint=true); function _chamfcorner(points, parm) = let( @@ -320,19 +358,22 @@ function _chamfcorner(points, parm) = function _circlecorner(points, parm) = let( - angle = vector_angle(points)/2, - d = parm[0], - r = parm[1], - prev = unit(points[0]-points[1]), - next = unit(points[2]-points[1]), - center = r/sin(angle) * unit(prev+next)+points[1], - start = points[1]+prev*d, - end = points[1]+next*d + angle = vector_angle(points)/2, + d = parm[0], + r = parm[1], + prev = unit(points[0]-points[1]), + next = unit(points[2]-points[1]) + ) + approx(angle,90) ? [points[1]+prev*d, points[1]+next*d] : + let( + center = r/sin(angle) * unit(prev+next)+points[1], + start = points[1]+prev*d, + end = points[1]+next*d ) // 90-angle is half the angle of the circular arc - arc(max(3,(90-angle)/180*segs(r)), cp=center, points=[start,end]); + arc(max(3,ceil((90-angle)/180*segs(r))), cp=center, points=[start,end]); -// Used by offset_sweep and convex_offset_extrude: +// Used by offset_sweep and convex_offset_extrude. // Produce edge profile curve from the edge specification // z_dir is the direction multiplier (1 to build up, -1 to build down) function _rounding_offsets(edgespec,z_dir=1) = @@ -343,9 +384,11 @@ function _rounding_offsets(edgespec,z_dir=1) = r = struct_val(edgespec,"r"), cut = struct_val(edgespec,"cut"), k = struct_val(edgespec,"k"), - radius = in_list(edgetype,["circle","teardrop"])? - first_defined([cut/(sqrt(2)-1),r]) : - edgetype=="chamfer"? first_defined([sqrt(2)*cut,r]) : undef, + radius = in_list(edgetype,["circle","teardrop"]) + ? (is_def(cut) ? cut/(sqrt(2)-1) : r) + :edgetype=="chamfer" + ? (is_def(cut) ? sqrt(2)*cut : r) + : undef, chamf_angle = struct_val(edgespec, "angle"), cheight = struct_val(edgespec, "chamfer_height"), cwidth = struct_val(edgespec, "chamfer_width"), @@ -392,7 +435,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // Function: smooth_path() // Usage: -// smooth_path(path, [size|relsize], [tangents], [splinesteps], [closed], [uniform]) +// smoothed = smooth_path(path, , , , , ); // Description: // Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve // with `splinesteps` points. The cubic interpolation will pass through every input point on the path @@ -410,9 +453,10 @@ function _rounding_offsets(edgespec,z_dir=1) = // value is too large it will be rounded down. See also path_to_bezier(). // Arguments: // path = path to smooth -// size = absolute size specification for the curve, a number or vector +// tangents = tangents constraining curve direction at each point. Default: computed automatically +// --- // relsize = relative size specification for the curve, a number or vector. Default: 0.1 -// tangents = tangents constraining curve direction at each point +// size = absolute size specification for the curve, a number or vector // uniform = set to true to compute tangents with uniform=true. Default: false // closed = true if the curve is closed. Default: false. // Example(2D): Original path in green, smoothed path in yellow: @@ -444,7 +488,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // polygon(smooth_path(square(4),tangents=1.25*[[-2,-1], [-4,1], [1,2], [6,-1]],size=0.4,closed=true)); // Example(2D): Or you can give a different size for each segment // polygon(smooth_path(square(4),size = [.4, .05, 1, .3],closed=true)); -// Example(FlatSpin): Works on 3d paths as well +// Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths as well // path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]]; // stroke(smooth_path(path,relsize=.1),width=.3); // Example(2D): This shows the type of overshoot that can occur with uniform=true. You can produce overshoots like this if you supply a tangent that is difficult to connect to the adjacent points @@ -455,6 +499,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]]; // stroke(smooth_path(pts, uniform=false, relsize=0.1),width=.1); // color("red")move_copies(pts)circle(r=.15,$fn=12); +module smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) {no_module();} function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) = let ( bez = path_to_bezier(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=closed) @@ -473,7 +518,7 @@ function _scalar_to_vector(value,length,varname) = // Function: path_join() // Usage: -// path_join(paths, [joint], [k], [relocate], [closed] +// joined_path = path_join(paths, , , , ); // Description: // Connect a sequence of paths together into a single path with optional rounding // applied at the joints. By default the first path is taken as specified and subsequent paths are @@ -497,6 +542,7 @@ function _scalar_to_vector(value,length,varname) = // Arguments: // paths = list of paths to join // joint = joint distance, either a number, a pair (giving the previous and next joint distance) or a list of numbers and pairs. Default: 0 +// --- // k = curvature parameter, either a number or vector. Default: 0.5 // relocate = set to false to prevent paths from being arranged tail to head. Default: true // closed = set to true to round the junction between the last and first paths. Default: false @@ -562,7 +608,7 @@ function _scalar_to_vector(value,length,varname) = // Example(2D): With a single path with closed=true the start and end junction is rounded. // tri = regular_ngon(n=3, r=7); // stroke(path_join([tri], joint=3,closed=true,$fn=12),closed=true,width=.5); - +module path_join(paths,joint=0,k=0.5,relocate=true,closed=false) { no_module();} function path_join(paths,joint=0,k=0.5,relocate=true,closed=false)= assert(is_list(paths),"Input paths must be a list of paths") let( @@ -601,12 +647,12 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) d_next = is_vector(joint[i]) ? joint[i][1] : joint[i] ) assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1)) + assert(d_first, , , , ,...) +// Usage: most common function arguments. See Arguments list below for more. +// vnf = offset_sweep(path, , , , , ...) // Description: // Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. When invoked as a function returns a VNF; when invoked as a module produces geometry. -// You can specify a sequence of offsets values, or you can use several built-in offset profiles that are designed to provide end treatments such as roundovers. +// Using the `top` and/or `bottom` arguments you can specify a sequence of offsets values, or you can use several built-in offset profiles that +// provide end treatments such as roundovers. +// The height of the resulting object can be specified using the `height` argument, in which case `height` must be larger than the combined height +// of the end treatments. If you omit `height` then the object height will be the height of just the top and bottom end treatments. +// . // The path is shifted by `offset()` multiple times in sequence // to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade // from the successive shifts. If the result seems rough or strange try increasing the number of points you use for @@ -708,8 +757,9 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) // Arguments: // path = 2d path (list of points) to extrude // height / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments. -// top = rounding spec for the top end. // bottom = rounding spec for the bottom end +// top = rounding spec for the top end. +// --- // offset = default offset, `"round"` or `"delta"`. Default: `"round"` // steps = default step count. Default: 16 // quality = default quality. Default: 1 @@ -746,9 +796,9 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) // triangle = [[0,0],[10,0],[5,10]]; // offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=16,offset_maxstep=0.01); // Example: Here is the star chamfered at the top with a teardrop rounding at the bottom. Check out the rounded corners on the chamfer. Note that a very small value of `offset_maxstep` is needed to keep these round. Observe how the rounded star points vanish at the bottom in the teardrop: the number of vertices does not remain constant from layer to layer. -// 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,offset_maxstep=.1)); +// 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,offset_maxstep=.1)); // 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. // square = square(1); // rsquare = round_corners(square, method="smooth", cut=0.1, k=0.7, $fn=36); @@ -875,7 +925,7 @@ function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, offsetind+1, vertexcount+len(path), vertices=concat( vertices, - zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) + path3d(vertices_faces[0],offsets[offsetind][1]) ), faces=concat(faces, vertices_faces[1]) ) @@ -883,8 +933,9 @@ function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, function offset_sweep( - path, height, h, l, - top=[], bottom=[], + path, height, + bottom=[], top=[], + h, l, offset="round", r=0, steps=16, quality=1, check_valid=true, offset_maxstep=1, extra=0, @@ -932,10 +983,10 @@ function offset_sweep( bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"), top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"), - height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height), + height = one_defined([l,h,height], "l,h,height", dflt=u_add(bottom_height,top_height)), middle = height-bottom_height-top_height ) - assert(height>=0, "Height must be nonnegative") + assert(height>0, "Height must be positive") assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height, " top_height = ",top_height,") are too large for extrusion height (",height,")" ) @@ -952,7 +1003,7 @@ function offset_sweep( ), top_start_ind = len(vertices_faces_bot[0]), - initial_vertices_top = zip(path, repeat(middle,len(path))), + initial_vertices_top = path3d(path, middle), vertices_faces_top = _make_offset_polyhedron( path, move(p=offsets_top,[0,middle]), struct_val(top,"offset"), !clockwise, @@ -972,8 +1023,9 @@ function offset_sweep( concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)]; // Faces -module offset_sweep(path, height, h, l, - top=[], bottom=[], +module offset_sweep(path, height, + bottom=[], top=[], + h, l, offset="round", r=0, steps=16, quality=1, check_valid=true, offset_maxstep=1, extra=0, @@ -1157,6 +1209,15 @@ function os_mask(mask, out=false, extra,check_valid, quality, offset_maxstep, of // right(50) linear_extrude(height=7) star(5,r=22,ir=13); // convex_offset_extrude(bottom = os_chamfer(height=-2), top=os_chamfer(height=1), height=7) // star(5,r=22,ir=13); +function convex_offset_extrude( + height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45, + convexity=10, thickness = 1/1024 +) = no_function("convex_offset_extrude"); module convex_offset_extrude( height, h, l, top=[], bottom=[], @@ -1190,7 +1251,7 @@ module convex_offset_extrude( bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"); top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"); - height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height); + height = one_defined([l,h,height], "l,h,height", dflt=u_add(bottom_height,top_height)); assert(height>=0, "Height must be nonnegative"); middle = height-bottom_height-top_height; @@ -1247,8 +1308,11 @@ function _remove_undefined_vals(list) = // Function&Module: offset_stroke() -// Usage: -// offset_stroke(path, [width], [rounded], [chamfer], [start], [end], [check_valid], [quality], [maxstep], [closed]) +// Usage: as module +// offset_stroke(path, , , , , , , , , ); +// Usage: as function +// path = offset_stroke(path, , closed=false, , , , , , , ); +// region = offset_stroke(path, , closed=true, , , , , , , ); // Description: // Uses `offset()` to compute a stroke for the input path. Unlike `stroke`, the result does not need to be // centered on the input path. The corners can be rounded, pointed, or chamfered, and you can make the ends @@ -1291,6 +1355,7 @@ function _remove_undefined_vals(list) = // Arguments: // path = 2d path that defines the stroke // width = width of the stroke, a scalar or a vector of 2 values giving the offset from the path. Default: 1 +// --- // rounded = set to true to use rounded offsets, false to use sharp (delta) offsets. Default: true // chamfer = set to true to use chamfers when `rounded=false`. Default: false // start = end treatment for the start of the stroke. See above for details. Default: "flat" @@ -1488,7 +1553,7 @@ function _stroke_end(width,left, right, spec) = normal_dir = unit(normal_seg[1]-normal_seg[0]), width_dir = sign(width[0]-width[1]) ) - type == "round"? [arc(points=[right[0],normal_pt,left[0]],N=50),1,1] : + type == "round"? [arc(points=[right[0],normal_pt,left[0]],N=ceil(segs(width/2)/2)),1,1] : type == "pointed"? [[normal_pt],0,0] : type == "shifted_point"? ( let(shiftedcenter = center + width_dir * parallel_dir * struct_val(spec, "loc")) @@ -1532,8 +1597,8 @@ function _stroke_end(width,left, right, spec) = 90-vector_angle([newright[1],newright[0],newleft[0]])/2, jointleft = 8*cutleft/cos(leftangle)/(1+4*bez_k), jointright = 8*cutright/cos(rightangle)/(1+4*bez_k), - pathcutleft = path_cut(newleft,abs(jointleft)), - pathcutright = path_cut(newright,abs(jointright)), + pathcutleft = path_cut_points(newleft,abs(jointleft)), + pathcutright = path_cut_points(newright,abs(jointright)), leftdelete = intright? pathcutleft[1] : pathcutleft[1] + pathclip[1] -1, rightdelete = intright? pathcutright[1] + pathclip[1] -1 : pathcutright[1], leftcorner = line_intersection([pathcutleft[0], newleft[pathcutleft[1]]], [newright[0],newleft[0]]), @@ -1627,9 +1692,10 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // Function&Module: rounded_prism() -// Usage: -// rounded_prism(bottom, [top], joint_top, joint_bot, joint_sides, [k], [k_top], [k_bot], [k_sides], [splinesteps], [height|h|length|l], [debug], [convexity]) -// vnf = rounded_prism(bottom, [top], joint_top, joint_bot, joint_sides, [k], [k_top], [k_bot], [k_sides], [splinesteps], [height|h|length|l], [debug]) +// Usage: as a module +// rounded_prism(bottom, , , , , , , , , , , , ,...) ; +// Usage: as a function +// vnf = rounded_prism(bottom, , , , , , , , , , , ); // Description: // Construct a generalized prism with continuous curvature rounding. You supply the polygons for the top and bottom of the prism. The only // limitation is that joining the edges must produce a valid polyhedron with coplanar side faces. You specify the rounding by giving @@ -1661,10 +1727,11 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // Arguments: // bottom = 2d or 3d path describing bottom polygon // top = 2d or 3d path describing top polygon (must be the same dimension as bottom) +// --- // height/length/h/l = height of the shape when you give 2d bottom -// joint_top = rounding length for top (number or 2-vector) -// joint_bot = rounding length for bottom (number or 2-vector) -// joint_sides = rounding length for side edges, a number/2-vector or list of them +// joint_top = rounding length for top (number or 2-vector). Default: 0 +// joint_bot = rounding length for bottom (number or 2-vector). Default: 0 +// joint_sides = rounding length for side edges, a number/2-vector or list of them. Default: 0 // k = continuous curvature rounding parameter for all edges. Default: 0.5 // k_top = continuous curvature rounding parameter for top // k_bot = continuous curvature rounding parameter for bottom @@ -1724,7 +1791,7 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // Example: Sideways polygons: // rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)), joint_top=2, joint_bot=1, joint_sides=1); -module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides, +module rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot, k_top, k_sides, k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false, anchor="origin",cp,spin=0, orient=UP, extent=false) { @@ -1743,7 +1810,7 @@ module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_to } -function rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides, k=0.5, splinesteps=16, +function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot, k_top, k_sides, k=0.5, splinesteps=16, h, length, l, height, debug=false) = assert(is_path(bottom) && len(bottom)>=3) assert(is_num(k) && k>=0 && k<=1, "Curvature parameter k must be in interval [0,1]") @@ -1752,7 +1819,7 @@ function rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_ k_top = default(k_top, k), k_bot = default(k_bot, k), k_sides = default(k_sides, k), - height = one_defined([h,l,height,length],["height","length","l","h"], required=false), + height = one_defined([h,l,height,length],"height,length,l,h", dflt=undef), shapedimok = (len(bottom[0])==3 && is_path(top,3)) || (len(bottom[0])==2 && (is_undef(top) || is_path(top,2))) ) assert(is_num(k_top) && k_top>=0 && k_top<=1, "Curvature parameter k_top must be in interval [0,1]") @@ -1768,7 +1835,7 @@ function rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_ len(top[0])==2 ? path3d(top,height/2) : top, bottom = len(bottom[0])==2 ? path3d(bottom,-height/2) : bottom, - jssingleok = (is_num(joint_sides) && joint_sides > 0) || (is_vector(joint_sides,2) && joint_sides[0]>=0 && joint_sides[1]>=0), + jssingleok = (is_num(joint_sides) && joint_sides >= 0) || (is_vector(joint_sides,2) && joint_sides[0]>=0 && joint_sides[1]>=0), jsvecok = is_list(joint_sides) && len(joint_sides)==N && []==[for(entry=joint_sides) if (!(is_num(entry) || is_vector(entry,2))) entry] ) assert(is_num(joint_top) || is_vector(joint_top,2)) @@ -2008,7 +2075,7 @@ function _circle_mask(r) = // Module: bent_cutout_mask() // Usage: -// bent_cutout_mask(r|radius, thickness, path) +// bent_cutout_mask(r|radius, thickness, path); // Description: // Creates a mask for cutting a round-edged hole out of a vertical cylindrical shell. The specified radius // is the center radius of the cylindrical shell. The path needs to be sampled finely enough @@ -2172,11 +2239,13 @@ function _circle_mask(r) = // bent_cutout_mask(diam/2-wall/2, wall+.1, subdivide_path(apply(back(10),slot(15, 29, 7)),250)); // } // } - -module bent_cutout_mask(r, thickness, path, convexity=10) +function bent_cutout_mask(r, thickness, path, radius, convexity=10) = no_function("bent_cutout_mask"); +module bent_cutout_mask(r, thickness, path, radius, convexity=10) { no_children($children); - assert(is_path(path,2),"Input path must be a 2d path") + r = get_radius(r1=r, r2=radius); + dummy=assert(is_def(r) && r>0,"Radius of the cylinder to bend around must be positive"); + assert(is_path(path,2),"Input path must be a 2d path"); assert(r-thickness>0, "Thickness too large for radius"); assert(thickness>0, "Thickness must be positive"); path = clockwise_polygon(path); diff --git a/screws.scad b/screws.scad index 03ba680f..27c6c5c0 100644 --- a/screws.scad +++ b/screws.scad @@ -1,11 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: screws.scad // Functions and modules for creating metric and UTS standard screws and nuts. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include // include -// ``` ////////////////////////////////////////////////////////////////////// include @@ -13,6 +11,9 @@ include include include +// Section: Generic Screw Creation + + /* http://mdmetric.com/thddata.htm#idx @@ -39,7 +40,7 @@ function _parse_screw_name(name) = let(val=str_num(type)) val == floor(val) && val>=0 && val<=12 ? str("#",type) : val ) - ["english", diam, thread, 25.4*length]; + ["english", diam, thread, u_mul(25.4,length)]; // drive can be "hex", "phillips", "slot", "torx", or "none" @@ -167,7 +168,7 @@ function screw_info(name, head, thread="coarse", drive, drive_size=undef, oversi is_def(type[3]) ? ["length",type[3]] : [], is_def(drive_info[1]) ? ["drive_size", drive_info[1]] : [], ["diameter", oversize+struct_val(screwdata,"diameter"), - "head_size", oversize+struct_val(screwdata,"head_size")] + "head_size", u_add(oversize,struct_val(screwdata,"head_size"))] ) ) struct_set(screwdata, over_ride); @@ -309,7 +310,9 @@ function _screw_info_english(diam, threadcount, head, thread, drive) = [2, [ 3, 1.5, undef, undef, undef]], ], entry = struct_val(UTS_socket, diam), - hexdepth = first_defined([entry[3], diam/2]), + hexdepth = is_def(entry[3]) ? entry[3] + : is_def(diam) ? diam/2 + : undef, drive_size = drive=="hex" ? [["drive_size",inch*entry[1]], ["drive_depth",inch*hexdepth]] : drive=="torx" ? [["drive_size",entry[2]],["drive_depth",inch*entry[4]]] : [] ) @@ -798,7 +801,7 @@ module screw_head(screw_info,details=false) { // Module: screw() // Usage: -// screw([name],[head],[thread],[drive],[drive_size], [length], [shank], [oversize], [tolerance], [spec], [details], [anchor], [anchor_head], [orient], [spin]) +// screw([name],[head],[thread],[drive],[drive_size], [length], [shank], [oversize], [tolerance], [$slop], [spec], [details], [anchor], [anchor_head], [orient], [spin]) // Description: // Create a screw. // . @@ -816,6 +819,8 @@ module screw_head(screw_info,details=false) { // tolerance "6g" specifies both pitch and crest diameter to be the same, // but they can be different, with a tolerance like "5g6g" specifies a pitch diameter tolerance of "5g" and a crest diameter tolerance of "6g". // Smaller numbers give a tighter tolerance. The default ISO tolerance is "6g". +// . +// The $slop argument gives an extra gap to account for printing overextrusion. It defaults to 0.2. // Arguments: // name = screw specification, e.g. "M5x1" or "#8-32" // head = head type (see list above). Default: none @@ -828,9 +833,10 @@ module screw_head(screw_info,details=false) { // shank = length of unthreaded portion of screw (in mm). Default: 0 // details = toggle some details in rendering. Default: false // tolerance = screw tolerance. Determines actual screw thread geometry based on nominal sizing. Default is "2A" for UTS and "6g" for ISO. +// $slop = add extra gap to account for printer overextrusion. Default: 0.2 // anchor = anchor relative to the shaft of the screw // anchor_head = anchor relative to the screw head -// Example: Selected UTS (English) screws +// Example(Med): Selected UTS (English) screws // $fn=32; // xdistribute(spacing=8){ // screw("#6", length=12); @@ -843,7 +849,7 @@ module screw_head(screw_info,details=false) { // screw("#6-24", head="socket",length=12); // Non-standard threading // screw("#6-32", drive="hex", drive_size=1.5, length=12); // } -// Example: A few examples of ISO (metric) screws +// Example(Med): A few examples of ISO (metric) screws // $fn=32; // xdistribute(spacing=8){ // screw("M3", head="flat small",length=12); @@ -855,7 +861,7 @@ module screw_head(screw_info,details=false) { // screw("M3", head="socket",length=12); // screw("M5", head="hex", length=12); // } -// Example: Demonstration of all head types for UTS screws (using pitch zero for fast preview) +// Example(Med): Demonstration of all head types for UTS screws (using pitch zero for fast preview) // xdistribute(spacing=15){ // ydistribute(spacing=15){ // screw("1/4", thread=0,length=8, anchor=TOP, head="none", drive="hex"); @@ -897,7 +903,7 @@ module screw_head(screw_info,details=false) { // screw("1/4", thread=0,length=8, anchor=TOP, head="flat undercut"); // } // } -// Example: Demonstration of all head types for metric screws without threading. +// Example(Med): Demonstration of all head types for metric screws without threading. // xdistribute(spacing=15){ // ydistribute(spacing=15){ // screw("M6x0", length=8, anchor=TOP, head="none", drive="hex"); @@ -949,21 +955,6 @@ module screw_head(screw_info,details=false) { // label("2") screw("1/4-20,5/8", head="hex",orient=DOWN,anchor_head=TOP,tolerance="2A"); // Standard // label("3") screw("1/4-20,5/8", head="hex",orient=DOWN,anchor_head=TOP,tolerance="3A"); // Tight // } -// Example: The three different UTS nut tolerances -// inch=25.4; -// module mark(number) -// { -// difference(){ -// children(); -// ycopies(n=number, spacing=1.5)right(.25*inch-2)up(8-.35)cyl(d=1, h=1); -// } -// } -// $fn=64; -// xdistribute(spacing=17){ -// mark(1) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="1B"); -// mark(2) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="2B"); -// mark(3) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="3B"); -// } // Example(2D): This example shows the gap between nut and bolt at the loosest tolerance for UTS. This gap is what enables the parts to mesh without binding and is part of the definition for standard metal hardware. // $slop=0; // $fn=32; @@ -1094,7 +1085,7 @@ function _ISO_thread_tolerance(diameter, pitch, internal=false, tolerance=undef) ], rangepts = [0.99, 1.4, 2.8, 5.6, 11.2, 22.4, 45, 90, 180, 300], - d_ind = floor(lookup(diameter,zip(rangepts,list_range(len(rangepts))))), + d_ind = floor(lookup(diameter,hstack(rangepts,list_range(len(rangepts))))), avgd = sqrt(rangepts[d_ind]* rangepts[d_ind+1]), T_d2_6 = 90*pow(P, 0.4)*pow(avgd,0.1), @@ -1302,12 +1293,16 @@ module _rod(spec, length, tolerance, orient=UP, spin=0, anchor=CENTER) // Module: nut() // Usage: -// nut([name],[thread],[oversize],[spec],[diameter],[thickness],[tolerance],[details]) +// nut([name],diameter, thickness,[thread],[oversize],[spec],[tolerance],[details],[$slop]) // Description: -// The name, thread and oversize parameters are described under `screw_info()` +// Generates a hexagonal nut. +// The name, thread and oversize parameters are described under `screw_info()`. As for screws, +// you can give the specification in `spec` and then omit the name. The diameter is the flat-to-flat +// size of the nut produced. // . // The tolerance determines the actual thread sizing based on the -// nominal size. For UTS threads it is either "1B", "2B" or "3B", in +// nominal size. +// For UTS threads the tolerance is either "1B", "2B" or "3B", in // order of increasing tightness. The default tolerance is "2B", which // is the general standard for manufactured nuts. For ISO the tolerance // has the form of a number and letter. The letter specifies the "fundamental deviation", also called the "tolerance position", the gap @@ -1315,16 +1310,44 @@ module _rod(spec, length, tolerance, orient=UP, spin=0, anchor=CENTER) // he loosest and "H" means no gap. The number specifies the allowed // range (variability) of the thread heights. Smaller numbers give tigher tolerances. It must be a value from // 4-8, so an allowed (loose) tolerance is "7G". The default ISO tolerance is "6H". +// . +// The $slop parameter determines extra gaps left to account for printing overextrusion. It defaults to 0.2. // Arguments: // name = screw specification, e.g. "M5x1" or "#8-32" +// diameter = outside diameter of nut (flat to flat dimension) +// thickness = thickness of nut (in mm) +// --- // thread = thread type or specification. Default: "coarse" // oversize = amount to increase screw diameter for clearance holes. Default: 0 // spec = screw specification from `screw_info()`. If you specify this you can omit all the preceeding parameters. -// thickness = thickness of bolt (in mm) // details = toggle some details in rendering. Default: false // tolerance = nut tolerance. Determines actual nut thread geometry based on nominal sizing. Default is "2B" for UTS and "6H" for ISO. -module nut(name, thread="coarse", oversize=0, spec, diameter, thickness, tolerance=undef, details=true, anchor=BOTTOM,spin=0, orient=UP) +// $slop = extra space left to account for printing over-extrusion. Default: 0.2 +// Example: A metric and UTS nut +// inch=25.4; +// nut("3/8", 5/8*inch, 1/4*inch); +// right(25) +// nut("M8", 16, 6); +// Example: The three different UTS nut tolerances +// inch=25.4; +// module mark(number) +// { +// difference(){ +// children(); +// ycopies(n=number, spacing=1.5)right(.25*inch-2)up(8-.35)cyl(d=1, h=1); +// } +// } +// $fn=64; +// xdistribute(spacing=17){ +// mark(1) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="1B"); +// mark(2) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="2B"); +// mark(3) nut("1/4-20", thickness=8, diameter=0.5*inch,tolerance="3B"); +// } +module nut(name, diameter, thickness, thread="coarse", oversize=0, spec, tolerance=undef, + details=true, anchor=BOTTOM,spin=0, orient=UP) { + assert(is_num(diameter) && diameter>0); + assert(is_num(thickness) && thickness>0); spec = is_def(spec) ? spec : screw_info(name, thread=thread, oversize=oversize); threadspec = thread_specification(spec, internal=true, tolerance=tolerance); echo(threadspec=threadspec,"for nut threads"); diff --git a/scripts/docs_gen.py b/scripts/docs_gen.py deleted file mode 100755 index d8849a97..00000000 --- a/scripts/docs_gen.py +++ /dev/null @@ -1,882 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import print_function - -import os -import re -import sys -import math -import random -import hashlib -import filecmp -import dbm.gnu -import os.path -import platform -import argparse -import subprocess - -from PIL import Image, ImageChops - - -if platform.system() == "Darwin": - OPENSCAD = "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD" - GIT = "git" -else: - OPENSCAD = "openscad" - GIT = "git" - - -def image_compare(file1, file2): - img1 = Image.open(file1) - img2 = Image.open(file2) - if img1.size != img2.size or img1.getbands() != img2.getbands(): - return False - diff = ImageChops.difference(img1, img2).histogram() - sq = (value * (i % 256) ** 2 for i, value in enumerate(diff)) - sum_squares = sum(sq) - rms = math.sqrt(sum_squares / float(img1.size[0] * img1.size[1])) - return rms<2 - - -def image_resize(infile, outfile, newsize=(320,240)): - im = Image.open(infile) - im.thumbnail(newsize, Image.ANTIALIAS) - im.save(outfile) - - -def make_animated_gif(imgfiles, outfile, size): - imgs = [] - for file in imgfiles: - img = Image.open(file) - img.thumbnail(size, Image.ANTIALIAS) - imgs.append(img) - imgs[0].save( - outfile, - save_all=True, - append_images=imgs[1:], - duration=250, - loop=0 - ) - -def git_checkout(filename): - # Pull previous committed image from git, if it exists. - gitcmd = [GIT, "checkout", filename] - p = subprocess.Popen(gitcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) - err = p.stdout.read() - - -def run_openscad_script(libfile, infile, imgfile, imgsize=(320,240), eye=None, show_edges=False, render=False, test_only=False): - if test_only: - scadcmd = [ - OPENSCAD, - "-o", "foo.term", - "--hardwarnings" - ] - else: - scadcmd = [ - OPENSCAD, - "-o", imgfile, - "--imgsize={},{}".format(imgsize[0]*2, imgsize[1]*2), - "--hardwarnings", - "--projection=o", - "--autocenter", - "--viewall" - ] - if eye is not None: - scadcmd.extend(["--camera", eye+",0,0,0"]) - if show_edges: - scadcmd.extend(["--view=axes,scales,edges"]) - else: - scadcmd.extend(["--view=axes,scales"]) - if render: # Force render - scadcmd.extend(["--render", ""]) - scadcmd.append(infile) - with open(infile, "r") as f: - script = "".join(f.readlines()); - p = subprocess.Popen(scadcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) - (stdoutdata, stderrdata) = p.communicate(None) - res = p.returncode - if test_only and os.path.isfile("foo.term"): - os.unlink("foo.term") - if res != 0 or b"ERROR:" in stderrdata or b"TRACE:" in stderrdata: - print("\n\n{}".format(stderrdata.decode('utf-8'))) - print("////////////////////////////////////////////////////") - print("// {}: {} for {}".format(libfile, infile, imgfile)) - print("////////////////////////////////////////////////////") - print(script) - print("////////////////////////////////////////////////////") - print("") - with open("FAILED.scad", "w") as f: - print("////////////////////////////////////////////////////", file=f) - print("// {}: {} for {}".format(libfile, infile, imgfile), file=f) - print("////////////////////////////////////////////////////", file=f) - print(script, file=f) - print("////////////////////////////////////////////////////", file=f) - print("", file=f) - sys.exit(-1) - return imgfile - - -class ImageProcessing(object): - def __init__(self): - self.examples = [] - self.commoncode = [] - self.imgroot = "" - self.keep_scripts = False - self.force = False - self.test_only = False - - def set_keep_scripts(self, x): - self.keep_scripts = x - - def add_image(self, libfile, imgfile, code, extype): - self.examples.append((libfile, imgfile, code, extype)) - - def set_commoncode(self, code): - self.commoncode = code - - def process_examples(self, imgroot, force=False, test_only=False): - self.imgroot = imgroot - self.force = force - self.test_only = test_only - self.hashes = {} - with dbm.gnu.open("examples_hashes.gdbm", "c") as db: - for libfile, imgfile, code, extype in self.examples: - self.gen_example_image(db, libfile, imgfile, code, extype) - for key, hash in self.hashes.items(): - db[key] = hash - - def gen_example_image(self, db, libfile, imgfile, code, extype): - if extype == "NORENDER": - return - - print(" {}".format(imgfile), end='') - sys.stdout.flush() - - test_only = self.test_only - scriptfile = "tmp_{0}.scad".format(imgfile.replace(".", "_")) - targimgfile = self.imgroot + imgfile - newimgfile = self.imgroot + "_new_" + imgfile - - # Pull previous committed image from git, if it exists. - git_checkout(targimgfile) - - m = hashlib.sha256() - m.update(extype.encode("utf8")) - for line in code: - m.update(line.encode("utf8")) - hash = m.digest() - key = "{0} - {1}".format(libfile, imgfile) - if key in db and db[key] == hash and not self.force: - print("") - return - - stdlibs = ["std.scad", "debug.scad"] - script = "" - for lib in stdlibs: - script += "include \n" % lib - if libfile not in stdlibs: - script += "include \n" % libfile - for line in self.commoncode: - script += line+"\n" - for line in code: - script += line+"\n" - - with open(scriptfile, "w") as f: - f.write(script) - - if "Huge" in extype: - imgsize = (800, 600) - elif "Big" in extype: - imgsize = (640, 480) - elif "Med" in extype or "distribute" in script or "show_anchors" in script: - imgsize = (480, 360) - else: # Small - imgsize = (320, 240) - - show_edges = "Edges" in extype - render = "FR" in extype - - tmpimgs = [] - if "Spin" in extype and not test_only: - for ang in range(0,359,10): - tmpimgfile = "{0}tmp_{2}_{1}.png".format(self.imgroot, ang, imgfile.replace(".", "_")) - arad = ang * math.pi / 180; - eye = "{0},{1},{2}".format( - 500*math.cos(arad), - 500*math.sin(arad), - 500 if "Flat" in extype else 500*math.sin(arad) - ) - run_openscad_script( - libfile, scriptfile, tmpimgfile, - imgsize=(imgsize[0]*2,imgsize[1]*2), - eye=eye, - show_edges=show_edges, - render=render, - test_only=test_only - ) - tmpimgs.append(tmpimgfile) - print(".", end='') - sys.stdout.flush() - else: - tmpimgfile = self.imgroot + "tmp_" + imgfile - eye = "0,0,500" if "2D" in extype else None - run_openscad_script( - libfile, scriptfile, tmpimgfile, - imgsize=(imgsize[0]*2,imgsize[1]*2), - eye=eye, - show_edges=show_edges, - render=render, - test_only=test_only - ) - tmpimgs.append(tmpimgfile) - - if not self.keep_scripts: - os.unlink(scriptfile) - - if not test_only: - if len(tmpimgs) == 1: - image_resize(tmpimgfile, newimgfile, imgsize) - os.unlink(tmpimgs.pop(0)) - else: - make_animated_gif(tmpimgs, newimgfile, size=imgsize) - for tmpimg in tmpimgs: - os.unlink(tmpimg) - - print("") - - if not test_only: - # Time to compare image. - if not os.path.isfile(targimgfile): - print(" NEW IMAGE\n") - os.rename(newimgfile, targimgfile) - else: - if targimgfile.endswith(".gif"): - issame = filecmp.cmp(targimgfile, newimgfile, shallow=False) - else: - issame = image_compare(targimgfile, newimgfile); - if issame: - os.unlink(newimgfile) - else: - print(" UPDATED IMAGE\n") - os.unlink(targimgfile) - os.rename(newimgfile, targimgfile) - self.hashes[key] = hash - - -imgprc = ImageProcessing() - - -def get_header_link(name): - refpat = re.compile("[^a-z0-9_ -]") - return refpat.sub("", name.lower()).replace(" ", "-") - - -def toc_entry(name, indent, count=None): - lname = "{0}{1}".format( - ("%d. " % count) if count else "", - name - ) - ref = get_header_link(lname) - if name.endswith( (")", "}", "]") ): - name = "`" + name.replace("\\", "") + "`" - return "{0}{1} [{2}](#{3})".format( - indent, - ("%d." % count) if count else "-", - name, - ref - ) - - -def mkdn_esc(txt): - out = "" - quotpat = re.compile(r'([^`]*)(`[^`]*`)(.*$)'); - while txt: - m = quotpat.match(txt) - if m: - out += m.group(1).replace(r'_', r'\_').replace(r'&',r'&').replace(r'<', r'<').replace(r'>',r'>') - out += m.group(2) - txt = m.group(3) - else: - out += txt.replace(r'_', r'\_').replace(r'&',r'&').replace(r'<', r'<').replace(r'>',r'>') - txt = "" - return out - - -def get_comment_block(lines, prefix, blanks=1): - out = [] - blankcnt = 0 - indent = 0 - while lines: - if not lines[0].startswith(prefix+" "): - break - line = lines.pop(0)[len(prefix):] - if not indent: - while line.startswith(" "): - line = line[1:] - indent += 1 - else: - line = line[indent:] - if line == "": - blankcnt += 1 - if blankcnt >= blanks: - break - else: - blankcnt = 0 - if line.rstrip() == '.': - line = "\n" - out.append(line.rstrip()) - return (lines, out) - - -class LeafNode(object): - def __init__(self): - self.name = "" - self.leaftype = "" - self.status = "" - self.description = [] - self.usages = [] - self.arguments = [] - self.anchors = [] - self.side_effects = [] - self.figures = [] - self.examples = [] - - @classmethod - def match_line(cls, line, prefix): - if line.startswith(prefix + "Constant: "): - return True - if line.startswith(prefix + "Function: "): - return True - if line.startswith(prefix + "Function&Module: "): - return True - if line.startswith(prefix + "Module: "): - return True - return False - - def add_figure(self, title, code, figtype): - self.figures.append((title, code, figtype)) - - def add_example(self, title, code, extype): - self.examples.append((title, code, extype)) - - def parse_lines(self, lines, prefix): - blankcnt = 0 - expat = re.compile(r"^(Examples?)(\(([^\)]*)\))?: *(.*)$") - figpat = re.compile(r"^(Figures?)(\(([^\)]*)\))?: *(.*)$") - while lines: - if prefix and not lines[0].startswith(prefix.strip()): - break - line = lines.pop(0).rstrip() - if line.lstrip("/").strip() == "": - blankcnt += 1 - if blankcnt >= 2: - break - continue - blankcnt = 0 - - line = line[len(prefix):] - if line.startswith("Constant:"): - leaftype, title = line.split(":", 1) - self.name = title.strip() - self.leaftype = leaftype.strip() - continue - if line.startswith("Function&Module:"): - leaftype, title = line.split(":", 1) - self.name = title.strip() - self.leaftype = leaftype.strip() - continue - if line.startswith("Function:"): - leaftype, title = line.split(":", 1) - self.name = title.strip() - self.leaftype = leaftype.strip() - continue - if line.startswith("Module:"): - leaftype, title = line.split(":", 1) - self.name = title.strip() - self.leaftype = leaftype.strip() - continue - - if line.startswith("Status:"): - dummy, status = line.split(":", 1) - self.status = status.strip() - continue - if line.startswith("Usage:"): - dummy, title = line.split(":", 1) - title = title.strip() - lines, block = get_comment_block(lines, prefix) - if block == []: - print("Error: Usage header without any usage examples.") - print(line) - sys.exit(-2) - self.usages.append([title, block]) - continue - if line.startswith("Description:"): - dummy, desc = line.split(":", 1) - desc = desc.strip() - if desc: - self.description.append(desc) - lines, block = get_comment_block(lines, prefix) - self.description.extend(block) - continue - m = figpat.match(line) - if m: # Figure(TYPE): - plural = m.group(1) == "Figures" - figtype = m.group(3) - title = m.group(4) - lines, block = get_comment_block(lines, prefix) - if not figtype: - figtype = "3D" - if not plural: - self.add_figure(title, block, figtype) - else: - for line in block: - self.add_figure("", [line], figtype) - continue - if line.startswith("Arguments:"): - lines, block = get_comment_block(lines, prefix) - for line in block: - if "=" not in line: - print("Error in {}: Could not parse line in Argument block. Missing '='.".format(self.name)) - print("Line read was:") - print(line) - sys.exit(-2) - argname, argdesc = line.split("=", 1) - argname = argname.strip() - argdesc = argdesc.strip() - self.arguments.append([argname, argdesc]) - continue - if line.startswith("Extra Anchors:") or line.startswith("Anchors:"): - lines, block = get_comment_block(lines, prefix) - for line in block: - if "=" not in line: - print("Error: bad anchor line:") - print(line) - sys.exit(-2) - anchorname, anchordesc = line.split("=", 1) - anchorname = anchorname.strip() - anchordesc = anchordesc.strip() - self.anchors.append([anchorname, anchordesc]) - continue - if line.startswith("Side Effects:"): - lines, block = get_comment_block(lines, prefix) - self.side_effects.extend(block) - continue - - m = expat.match(line) - if m: # Example(TYPE): - plural = m.group(1) == "Examples" - extype = m.group(3) - title = m.group(4) - lines, block = get_comment_block(lines, prefix) - if not extype: - extype = "3D" if self.leaftype in ["Module", "Function&Module"] else "NORENDER" - if not plural: - self.add_example(title=title, code=block, extype=extype) - else: - for line in block: - self.add_example(title="", code=[line], extype=extype) - continue - - if ":" not in line: - print("Error in {}: Unrecognized block header. Missing colon?".format(self.name)) - else: - print("Error in {}: Unrecognized block header.".format(self.name)) - print("Line read was:") - print(line) - sys.exit(-2) - - return lines - - def gen_md(self, fileroot, imgroot): - out = [] - if self.name: - out.append("### " + mkdn_esc(self.name)) - out.append("**Type:** {0}".format(mkdn_esc(self.leaftype.replace("&","/")))) - out.append("") - if self.status: - out.append("**{0}**".format(mkdn_esc(self.status))) - out.append("") - for title, usages in self.usages: - if not title: - title = "" - out.append("**Usage:** {0}".format(mkdn_esc(title))) - for usage in usages: - out.append("- {0}".format(mkdn_esc(usage))) - out.append("") - if self.description: - out.append("**Description**:") - for line in self.description: - out.append(mkdn_esc(line)) - out.append("") - fignum = 0 - for title, excode, extype in self.figures: - fignum += 1 - extitle = "**Figure {0}**:".format(fignum) - if title: - extitle += " " + mkdn_esc(title) - san_name = re.sub(r"[^A-Za-z0-9_]", "", self.name) - imgfile = "{}_{}.{}".format( - san_name, - ("fig%d" % fignum), - "gif" if "Spin" in extype else "png" - ) - imgprc.add_image(fileroot+".scad", imgfile, excode, extype) - out.append(extitle) - out.append("") - out.append( - "![{0} Figure {1}]({2}{3})".format( - mkdn_esc(self.name), - fignum, - imgroot, - imgfile - ) - ) - out.append("") - if self.arguments: - out.append("Argument | What it does") - out.append("--------------- | ------------------------------") - for argname, argdesc in self.arguments: - argname = argname.replace(" / ", "` / `") - out.append( - "{0:15s} | {1}".format( - "`{0}`".format(argname), - mkdn_esc(argdesc) - ) - ) - out.append("") - if self.side_effects: - out.append("**Side Effects**:") - for sfx in self.side_effects: - out.append("- " + mkdn_esc(sfx)) - out.append("") - if self.anchors: - out.append("Anchor Name | Description") - out.append("--------------- | ------------------------------") - for anchorname, anchordesc in self.anchors: - anchorname = anchorname.replace(" / ", "` / `") - out.append( - "{0:15s} | {1}".format( - "`{0}`".format(anchorname), - mkdn_esc(anchordesc) - ) - ) - out.append("") - exnum = 0 - for title, excode, extype in self.examples: - exnum += 1 - if len(self.examples) < 2: - extitle = "**Example**:" - else: - extitle = "**Example {0}**:".format(exnum) - if title: - extitle += " " + mkdn_esc(title) - san_name = re.sub(r"[^A-Za-z0-9_]", "", self.name) - imgfile = "{}{}.{}".format( - san_name, - ("_%d" % exnum) if exnum > 1 else "", - "gif" if "Spin" in extype else "png" - ) - if "NORENDER" not in extype: - imgprc.add_image(fileroot+".scad", imgfile, excode, extype) - if "Hide" not in extype: - out.append(extitle) - out.append("") - for line in excode: - out.append(" " + line) - out.append("") - if "NORENDER" not in extype: - out.append( - "![{0} Example{1}]({2}{3})".format( - mkdn_esc(self.name), - (" %d" % exnum) if len(self.examples) > 1 else "", - imgroot, - imgfile - ) - ) - out.append("") - out.append("---") - out.append("") - return out - - -class Section(object): - fignum = 0 - def __init__(self): - self.name = "" - self.description = [] - self.leaf_nodes = [] - self.figures = [] - - @classmethod - def match_line(cls, line, prefix): - if line.startswith(prefix + "Section: "): - return True - return False - - def add_figure(self, figtitle, figcode, figtype): - self.figures.append((figtitle, figcode, figtype)) - - def parse_lines(self, lines, prefix): - line = lines.pop(0).rstrip() - dummy, title = line.split(": ", 1) - self.name = title.strip() - lines, block = get_comment_block(lines, prefix, blanks=2) - self.description.extend(block) - blankcnt = 0 - figpat = re.compile(r"^(Figures?)(\(([^\)]*)\))?: *(.*)$") - while lines: - if prefix and not lines[0].startswith(prefix.strip()): - break - line = lines.pop(0).rstrip() - if line.lstrip("/").strip() == "": - blankcnt += 1 - if blankcnt >= 2: - break - continue - blankcnt = 0 - line = line[len(prefix):] - m = figpat.match(line) - if m: # Figures(TYPE): - plural = m.group(1) == "Figures" - figtype = m.group(3) - title = m.group(4) - lines, block = get_comment_block(lines, prefix) - if not figtype: - figtype = "3D" if self.figtype in ["Module", "Function&Module"] else "NORENDER" - if not plural: - self.add_figure(title, block, figtype) - else: - for line in block: - self.add_figure("", [line], figtype) - return lines - - def gen_md_toc(self, count): - indent="" - out = [] - if self.name: - out.append(toc_entry(self.name, indent, count=count)) - indent += " " - for node in self.leaf_nodes: - out.append(toc_entry(node.name, indent)) - out.append("") - return out - - def gen_md(self, count, fileroot, imgroot): - out = [] - if self.name: - out.append("# %d. %s" % (count, mkdn_esc(self.name))) - out.append("") - if self.description: - in_block = False - for line in self.description: - if line.startswith("```"): - in_block = not in_block - if in_block or line.startswith(" "): - out.append(line) - else: - out.append(mkdn_esc(line)) - out.append("") - for title, figcode, figtype in self.figures: - Section.fignum += 1 - figtitle = "**Figure {0}**:".format(Section.fignum) - if title: - figtitle += " " + mkdn_esc(title) - out.append(figtitle) - out.append("") - imgfile = "{}{}.{}".format( - "figure", - Section.fignum, - "gif" if "Spin" in figtype else "png" - ) - if figtype != "NORENDER": - out.append( - "![{0} Figure {1}]({2}{3})".format( - mkdn_esc(self.name), - Section.fignum, - imgroot, - imgfile - ) - ) - out.append("") - imgprc.add_image(fileroot+".scad", imgfile, figcode, figtype) - in_block = False - for node in self.leaf_nodes: - out += node.gen_md(fileroot, imgroot) - return out - - -class LibFile(object): - def __init__(self): - self.name = "" - self.description = [] - self.commoncode = [] - self.sections = [] - self.dep_sect = None - - def parse_lines(self, lines, prefix): - currsect = None - constpat = re.compile(r"^([A-Z_0-9][A-Z_0-9]*) *=.* // (.*$)") - while lines: - while lines and prefix and not lines[0].startswith(prefix.strip()): - line = lines.pop(0) - m = constpat.match(line) - if m: - if currsect == None: - currsect = Section() - self.sections.append(currsect) - node = LeafNode(); - node.extype = "Constant" - node.name = m.group(1).strip() - node.description.append(m.group(2).strip()) - currsect.leaf_nodes.append(node) - - # Check for LibFile header. - if lines and lines[0].startswith(prefix + "LibFile: "): - line = lines.pop(0).rstrip() - dummy, title = line.split(": ", 1) - self.name = title.strip() - lines, block = get_comment_block(lines, prefix, blanks=2) - self.description.extend(block) - - # Check for CommonCode header. - if lines and lines[0].startswith(prefix + "CommonCode:"): - lines.pop(0) - lines, block = get_comment_block(lines, prefix) - self.commoncode.extend(block) - - # Check for Section header. - if lines and Section.match_line(lines[0], prefix): - sect = Section() - lines = sect.parse_lines(lines, prefix) - self.sections.append(sect) - currsect = sect - - # Check for LeafNode. - if lines and LeafNode.match_line(lines[0], prefix): - node = LeafNode() - lines = node.parse_lines(lines, prefix) - deprecated = node.status.startswith("DEPRECATED") - if deprecated: - if self.dep_sect == None: - self.dep_sect = Section() - self.dep_sect.name = "Deprecations" - sect = self.dep_sect - else: - if currsect == None: - currsect = Section() - self.sections.append(currsect) - sect = currsect - sect.leaf_nodes.append(node) - if lines: - lines.pop(0) - return lines - - def gen_md(self, fileroot, imgroot): - imgprc.set_commoncode(self.commoncode) - out = [] - if self.name: - out.append("# Library File " + mkdn_esc(self.name)) - out.append("") - if self.description: - in_block = False - for line in self.description: - if line.startswith("```"): - in_block = not in_block - if in_block or line.startswith(" "): - out.append(line) - else: - out.append(mkdn_esc(line)) - out.append("") - in_block = False - if self.name or self.description: - out.append("---") - out.append("") - - if self.sections or self.dep_sect: - out.append("# Table of Contents") - out.append("") - cnt = 0 - for sect in self.sections: - cnt += 1 - out += sect.gen_md_toc(cnt) - if self.dep_sect: - cnt += 1 - out += self.dep_sect.gen_md_toc(cnt) - out.append("---") - out.append("") - - cnt = 0 - for sect in self.sections: - cnt += 1 - out += sect.gen_md(cnt, fileroot, imgroot) - if self.dep_sect: - cnt += 1 - out += self.dep_sect.gen_md(cnt, fileroot, imgroot) - return out - - -def processFile(infile, outfile=None, gen_imgs=False, test_only=False, imgroot="", prefix="", force=False): - if imgroot and not imgroot.endswith('/'): - imgroot += "/" - - libfile = LibFile() - with open(infile, "r") as f: - lines = f.readlines() - libfile.parse_lines(lines, prefix) - - if outfile == None: - f = sys.stdout - else: - f = open(outfile, "w") - - fileroot = os.path.splitext(os.path.basename(infile))[0] - outdata = libfile.gen_md(fileroot, imgroot) - for line in outdata: - print(line, file=f) - - if gen_imgs: - imgprc.process_examples(imgroot, force=force, test_only=test_only) - - if outfile: - f.close() - - -def main(): - parser = argparse.ArgumentParser(prog='docs_gen') - parser.add_argument('-t', '--test-only', action="store_true", - help="If given, don't generate images, but do try executing the scripts.") - parser.add_argument('-k', '--keep-scripts', action="store_true", - help="If given, don't delete the temporary image OpenSCAD scripts.") - parser.add_argument('-c', '--comments-only', action="store_true", - help='If given, only process lines that start with // comments.') - parser.add_argument('-f', '--force', action="store_true", - help='If given, force generation of images when the code is unchanged.') - parser.add_argument('-i', '--images', action="store_true", - help='If given, generate images for examples with OpenSCAD.') - parser.add_argument('-I', '--imgroot', default="", - help='The directory to put generated images in.') - parser.add_argument('-o', '--outfile', - help='Output file, if different from infile.') - parser.add_argument('infile', help='Input filename.') - args = parser.parse_args() - - imgprc.set_keep_scripts(args.keep_scripts) - processFile( - args.infile, - outfile=args.outfile, - gen_imgs=args.images, - test_only=args.test_only, - imgroot=args.imgroot, - prefix="// " if args.comments_only else "", - force=args.force - ) - - sys.exit(0) - - -if __name__ == "__main__": - main() - - -# vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/scripts/gencheat.sh b/scripts/gencheat.sh deleted file mode 100755 index e07a909a..00000000 --- a/scripts/gencheat.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash - - -function ucase -{ - echo "$1" | tr '[:lower:]' '[:upper:]' -} - -function lcase -{ - echo "$1" | tr '[:upper:]' '[:lower:]' -} - -function columnize -{ - cols=$2 - TMPFILE=$(mktemp -t $(basename $0).XXXXXX) || exit 1 - cat >>$TMPFILE - if [[ $(wc -l $TMPFILE | awk '{print $1}') -gt 1 ]] ; then - totcnt=$(wc -l $TMPFILE | awk '{print $1}') - maxrows=$((($totcnt+$cols-1)/$cols)) - maxcols=$cols - if [[ $maxcols -gt $totcnt ]] ; then - maxcols=$totcnt - fi - cnt=0 - hdrln1="| $(ucase $1) " - hdrln2='|:-----' - n=1 - while [[ $n -lt $maxcols ]] ; do - hdrln1+=' |  ' - hdrln2+=' |:------' - n=$(($n+1)) - done - hdrln1+=' |' - hdrln2+=' |' - n=0 - while [[ $n -lt $maxrows ]] ; do - lines[$n]="" - n=$(($n+1)) - done - col=0 - while IFS= read -r line; do - if [[ $col != 0 ]] ; then - lines[$cnt]+=" | " - fi - lines[$cnt]+="$line" - cnt=$(($cnt+1)) - if [[ $cnt = $maxrows ]] ; then - cnt=0 - col=$(($col+1)) - fi - done <$TMPFILE - rm -f $TMPFILE - - echo - echo $hdrln1 - echo $hdrln2 - n=0 - while [[ $n -lt $maxrows ]] ; do - echo "| ${lines[$n]} |" - n=$(($n+1)) - done - fi -} - -function mkconstindex -{ - sed 's/([^)]*)//g' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "[%s](%s#%s)\n", $3, $1, $3}' -} - -function mkconstindex2 -{ - sed 's/ *=.*$//' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "[%s](%s#%s)\n", $2, $1, $2}' -} - -function mkotherindex -{ - sed 's/([^)]*)//g' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "[%s()](%s#%s)\n", $3, $1, $3}' -} - -CHEAT_FILES=$(grep '^include' std.scad | sed 's/^.*<\([a-zA-Z0-9.]*\)>/\1/' | grep -v 'version.scad' | grep -v 'primitives.scad') - -( - echo '## Belfry OpenScad Library Cheat Sheet' - echo - echo '( [Alphabetic Index](Index) )' - echo - ( - grep -H '// Constant: ' $CHEAT_FILES | mkconstindex - grep -H '^[A-Z$][A-Z0-9_]* *=.*//' $CHEAT_FILES | mkconstindex2 - ) | sort -u | columnize 'Constants' 6 - for f in $CHEAT_FILES ; do - egrep -H '// Function: |// Function&Module: |// Module: ' $f | mkotherindex | columnize "[$f]($f)" 4 - echo - done -) > BOSL2.wiki/CheatSheet.md - diff --git a/scripts/genindex.sh b/scripts/genindex.sh deleted file mode 100755 index 51a90750..00000000 --- a/scripts/genindex.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - - -function ucase -{ - echo "$1" | tr '[:lower:]' '[:upper:]' -} - - -function lcase -{ - echo "$1" | tr '[:upper:]' '[:lower:]' -} - - -function alphaindex -{ - alpha="A B C D E F G H I J K L M N O P Q R S T U V W X Y Z" - TMPFILE=$(mktemp -t $(basename $0).XXXXXX) || exit 1 - sort -d -f >> $TMPFILE - for a in $alpha; do - echo -n "[$a](#$(lcase "$a")) " - done - echo - echo - for a in $alpha; do - links=$(cat $TMPFILE | grep -i "^- .[$(lcase "$a")]") - if [ "$links" != "" ]; then - echo "### $(ucase "$a")" - echo "$links" - echo - fi - done - rm -f $TMPFILE -} - - -function constlist -{ - sed 's/([^)]*)//g' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "- [%s](%s#%s) (in %s)\n", $3, $1, $3, $1}' -} - -function constlist2 -{ - sed 's/ *=.*$//' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "- [%s](%s#%s) (in %s)\n", $2, $1, $2, $1}' -} - - -function funclist -{ - sed 's/([^)]*)//g' | sed 's/[^a-zA-Z0-9_.:$]//g' | awk -F ':' '{printf "- [%s()](%s#%s) (in %s)\n", $3, $1, $3, $1}' -} - - -( - echo "## Belfry OpenScad Library Index" - ( - ( - grep 'Constant: ' *.scad | constlist - grep '^[A-Z]* *=.*//' *.scad | constlist2 - ) | sort -u - egrep 'Function: |Function&Module: |Module: ' *.scad | sort -u | funclist - ) | sort | alphaindex -) > BOSL2.wiki/Index.md - diff --git a/githooks/pre-commit b/scripts/increment_version.sh similarity index 94% rename from githooks/pre-commit rename to scripts/increment_version.sh index 28c01395..1109536a 100755 --- a/githooks/pre-commit +++ b/scripts/increment_version.sh @@ -13,5 +13,3 @@ echo "New Version: $major.$minor.$newrev" sed -i '' 's/^BOSL_VERSION = .*$/BOSL_VERSION = ['"$major,$minor,$newrev];/g" $VERFILE -exec git add version.scad - diff --git a/scripts/make_all_docs.sh b/scripts/make_all_docs.sh deleted file mode 100755 index ae203272..00000000 --- a/scripts/make_all_docs.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -FORCED="" -IMGGEN="" -TESTONLY="" -FILES="" -DISPMD="" -for opt in "$@" ; do - case $opt in - -f ) FORCED=$opt ;; - -i ) IMGGEN=$opt ;; - -t ) TESTONLY=$opt ;; - -d ) DISPMD=$opt ;; - -* ) echo "Unknown option $opt"; exit -1 ;; - * ) FILES="$FILES $opt" ;; - esac -done - -if [[ "$FILES" != "" ]]; then - PREVIEW_LIBS="$FILES" -else - PREVIEW_LIBS=$(git ls-files | grep '\.scad$' | grep -v / | grep -v -f .nodocsfiles) -fi - -dir="$(basename $PWD)" -if [ "$dir" = "BOSL2" ]; then - cd BOSL2.wiki -elif [ "$dir" != "BOSL2.wiki" ]; then - echo "Must run this script from the BOSL2 or BOSL2/BOSL2.wiki directories." - exit 1 -fi - -rm -f tmpscad*.scad -for lib in $PREVIEW_LIBS; do - lib="$(basename $lib .scad)" - mkdir -p images/$lib - if [ "$IMGGEN" != "" -a "$TESTONLY" != "" ]; then - rm -f images/$lib/*.png images/$lib/*.gif - fi - echo "$lib.scad" - ../scripts/docs_gen.py ../$lib.scad -o $lib.scad.md -c $IMGGEN $FORCED $TESTONLY -I images/$lib/ || exit 1 - if [ "$DISPMD" != "" ]; then - open -a Typora $lib.scad.md - fi -done - - diff --git a/scripts/make_tutorials.sh b/scripts/make_tutorials.sh index 9c0fee4d..b99fb659 100755 --- a/scripts/make_tutorials.sh +++ b/scripts/make_tutorials.sh @@ -15,7 +15,7 @@ done if [[ "$FILES" != "" ]]; then PREVIEW_LIBS="$FILES" else - PREVIEW_LIBS="Transforms Distributors Shapes2d Shapes3d Paths FractalTree" + PREVIEW_LIBS="Shapes2d Shapes3d Transforms Distributors Mutators Paths FractalTree" fi dir="$(basename $PWD)" diff --git a/scripts/purge_wiki_history.sh b/scripts/purge_wiki_history.sh new file mode 100755 index 00000000..5392677e --- /dev/null +++ b/scripts/purge_wiki_history.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [[ ! -d BOSL2.wiki/.git ]] ; then + echo "Must be run from the BOSL2 directory, with the BOSL2.wiki repo inside." + exit -1 +fi + +cd BOSL2.wiki +rm -rf .git +git init +git add . +git commit -m "Purged wiki history." +git remote add origin git@github.com:revarbat/BOSL2.wiki.git +git push -u --force origin master +cd .. + diff --git a/scripts/tutorial_gen.py b/scripts/tutorial_gen.py index 12bfb7ab..c2a57f88 100755 --- a/scripts/tutorial_gen.py +++ b/scripts/tutorial_gen.py @@ -3,246 +3,46 @@ from __future__ import print_function import os -import re import sys -import math -import random -import hashlib -import filecmp -import dbm.gnu import os.path -import platform import argparse -import subprocess -from PIL import Image, ImageChops +from openscad_docsgen.imagemanager import ImageManager -if platform.system() == "Darwin": - OPENSCAD = "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD" - GIT = "git" -else: - OPENSCAD = "openscad" - GIT = "git" +imgmgr = ImageManager() - -def image_compare(file1, file2): - img1 = Image.open(file1) - img2 = Image.open(file2) - if img1.size != img2.size or img1.getbands() != img2.getbands(): - return False - diff = ImageChops.difference(img1, img2).histogram() - sq = (value * (i % 256) ** 2 for i, value in enumerate(diff)) - sum_squares = sum(sq) - rms = math.sqrt(sum_squares / float(img1.size[0] * img1.size[1])) - return rms<10 - -def image_resize(infile, outfile, newsize=(320,240)): - im = Image.open(infile) - im.thumbnail(newsize, Image.ANTIALIAS) - im.save(outfile) - -def make_animated_gif(imgfiles, outfile, size): - imgs = [] - for file in imgfiles: - img = Image.open(file) - img.thumbnail(size, Image.ANTIALIAS) - imgs.append(img) - imgs[0].save( - outfile, - save_all=True, - append_images=imgs[1:], - duration=250, - loop=0 - ) - -def git_checkout(filename): - # Pull previous committed image from git, if it exists. - gitcmd = [GIT, "checkout", filename] - p = subprocess.Popen(gitcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) - err = p.stdout.read() - -def run_openscad_script(libfile, infile, imgfile, imgsize=(320,240), eye=None, show_edges=False, render=False): - scadcmd = [ - OPENSCAD, - "-o", imgfile, - "--imgsize={},{}".format(imgsize[0]*2, imgsize[1]*2), - "--hardwarnings", - "--projection=o", - "--autocenter", - "--viewall" - ] - if eye is not None: - scadcmd.extend(["--camera", eye+",0,0,0"]) - if show_edges: - scadcmd.extend(["--view=axes,scales,edges"]) - else: - scadcmd.extend(["--view=axes,scales"]) - if render: # Force render - scadcmd.extend(["--render", ""]) - scadcmd.append(infile) - with open(infile, "r") as f: - script = "".join(f.readlines()); - p = subprocess.Popen(scadcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) - (stdoutdata, stderrdata) = p.communicate(None) - res = p.returncode - if res != 0 or b"ERROR:" in stderrdata or b"TRACE:" in stderrdata: - print("\n\n{}".format(stderrdata.decode('utf-8'))) - print("////////////////////////////////////////////////////") - print("// {}: {} for {}".format(libfile, infile, imgfile)) - print("////////////////////////////////////////////////////") - print(script) - print("////////////////////////////////////////////////////") - print("") - with open("FAILED.scad", "w") as f: - print("////////////////////////////////////////////////////", file=f) - print("// {}: {} for {}".format(libfile, infile, imgfile), file=f) - print("////////////////////////////////////////////////////", file=f) - print(script, file=f) - print("////////////////////////////////////////////////////", file=f) - print("", file=f) - sys.exit(-1) - return imgfile +def img_started(req): + print(" {}... ".format(os.path.basename(req.src_file)), end='') + sys.stdout.flush() -class ImageProcessing(object): - def __init__(self): - self.examples = [] - self.commoncode = [] - self.imgroot = "" - self.keep_scripts = False - self.force = False - - def set_keep_scripts(self, x): - self.keep_scripts = x - - def add_image(self, libfile, imgfile, code, extype): - self.examples.append((libfile, imgfile, code, extype)) - - def set_commoncode(self, code): - self.commoncode = code - - def process_examples(self, imgroot, force=False): - self.imgroot = imgroot - self.force = force - self.hashes = {} - with dbm.gnu.open("examples_hashes.gdbm", "c") as db: - for libfile, imgfile, code, extype in self.examples: - self.gen_example_image(db, libfile, imgfile, code, extype) - for key, hash in self.hashes.items(): - db[key] = hash - - def gen_example_image(self, db, libfile, imgfile, code, extype): - if extype == "NORENDER": - return - - print(" {} ({})".format(imgfile,extype), end='') +def img_completed(req): + if req.success: + if req.status == "SKIP": + print() + else: + print(req.status) sys.stdout.flush() - - scriptfile = "tmp_{0}.scad".format(imgfile.replace(".", "_").replace("/","_")) - targimgfile = self.imgroot + imgfile - newimgfile = self.imgroot + "_new_" + imgfile - - # Pull previous committed image from git, if it exists. - git_checkout(targimgfile) - - m = hashlib.sha256() - m.update(extype.encode("utf8")) - for line in code: - m.update(line.encode("utf8")) - hash = m.digest() - key = "{0} - {1}".format(libfile, imgfile) - if key in db and db[key] == hash and not self.force: - print("") - return - - stdlibs = ["std.scad", "debug.scad"] - script = "" - for lib in stdlibs: - script += "include \n" % lib - for line in self.commoncode: - script += line+"\n" - for line in code: - script += line+"\n" - - with open(scriptfile, "w") as f: - f.write(script) - - if "Big" in extype: - imgsize = (640, 480) - elif "Med" in extype or "distribute" in script or "show_anchors" in script: - imgsize = (480, 360) - else: # Small - imgsize = (320, 240) - - show_edges = "Edges" in extype - render = "FR" in extype - - tmpimgs = [] - if "Spin" in extype: - for ang in range(0,359,10): - tmpimgfile = "{0}tmp_{2}_{1}.png".format(self.imgroot, ang, imgfile.replace(".", "_")) - arad = ang * math.pi / 180; - eye = "{0},{1},{2}".format( - 500*math.cos(arad), - 500*math.sin(arad), - 500 if "Flat" in extype else 500*math.sin(arad) - ) - run_openscad_script( - libfile, scriptfile, tmpimgfile, - imgsize=(imgsize[0]*2,imgsize[1]*2), - eye=eye, - show_edges=show_edges, - render=render - ) - tmpimgs.append(tmpimgfile) - print(".", end='') - sys.stdout.flush() - else: - tmpimgfile = self.imgroot + "tmp_" + imgfile - eye = "0,0,500" if "2D" in extype else None - run_openscad_script( - libfile, scriptfile, tmpimgfile, - imgsize=(imgsize[0]*2,imgsize[1]*2), - eye=eye, - show_edges=show_edges, - render=render - ) - tmpimgs.append(tmpimgfile) - - if not self.keep_scripts: - os.unlink(scriptfile) - - if len(tmpimgs) == 1: - image_resize(tmpimgfile, newimgfile, imgsize) - os.unlink(tmpimgs.pop(0)) - else: - make_animated_gif(tmpimgs, newimgfile, size=imgsize) - for tmpimg in tmpimgs: - os.unlink(tmpimg) - - print("") - - # Time to compare image. - if not os.path.isfile(targimgfile): - print(" NEW IMAGE\n") - os.rename(newimgfile, targimgfile) - else: - if targimgfile.endswith(".gif"): - issame = filecmp.cmp(targimgfile, newimgfile, shallow=False) - else: - issame = image_compare(targimgfile, newimgfile); - if issame: - os.unlink(newimgfile) - else: - print(" UPDATED IMAGE\n") - os.unlink(targimgfile) - os.rename(newimgfile, targimgfile) - self.hashes[key] = hash - - -imgprc = ImageProcessing() + return + out = "\n\n" + for line in req.echos: + out += line + "\n" + for line in req.warnings: + out += line + "\n" + for line in req.errors: + out += line + "\n" + out += "//////////////////////////////////////////////////////////////////////\n" + out += "// LibFile: {} Line: {} Image: {}\n".format( + req.src_file, req.src_line, os.path.basename(req.image_file) + ) + out += "//////////////////////////////////////////////////////////////////////\n" + for line in req.script_lines: + out += line + "\n" + out += "//////////////////////////////////////////////////////////////////////\n" + print(out, file=sys.stderr) + sys.exit(-1) def processFile(infile, outfile=None, imgroot=""): @@ -257,7 +57,9 @@ def processFile(infile, outfile=None, imgroot=""): in_script = False imgnum = 0 show_script = True + linenum = -1 for line in f.readlines(): + linenum += 1 line = line.rstrip("\n") if line.startswith("```openscad"): in_script = True; @@ -277,7 +79,12 @@ def processFile(infile, outfile=None, imgroot=""): if line == "```": in_script = False imgfile = "{}_{}.png".format(fileroot, imgnum) - imgprc.add_image(fileroot+".md", imgfile, script, extyp) + imgmgr.new_request( + fileroot+".md", linenum, + imgfile, script, extyp, + starting_cb=img_started, + completion_cb=img_completed + ) outdata.append("![Figure {}]({})".format(imgnum, imgroot + imgfile)) script = [] show_script = True @@ -301,10 +108,6 @@ def processFile(infile, outfile=None, imgroot=""): def main(): parser = argparse.ArgumentParser(prog='docs_gen') - parser.add_argument('-k', '--keep-scripts', action="store_true", - help="If given, don't delete the temporary image OpenSCAD scripts.") - parser.add_argument('-f', '--force', action="store_true", - help='If given, force generation of images when the code is unchanged.') parser.add_argument('-I', '--imgroot', default="", help='The directory to put generated images in.') parser.add_argument('-o', '--outfile', @@ -312,13 +115,12 @@ def main(): parser.add_argument('infile', help='Input filename.') args = parser.parse_args() - imgprc.set_keep_scripts(args.keep_scripts) processFile( args.infile, outfile=args.outfile, imgroot=args.imgroot ) - imgprc.process_examples(args.imgroot, force=args.force) + imgmgr.process_requests() sys.exit(0) diff --git a/shapes.scad b/shapes.scad index 31e35b1a..1a2ed217 100644 --- a/shapes.scad +++ b/shapes.scad @@ -1,18 +1,26 @@ ////////////////////////////////////////////////////////////////////// // LibFile: shapes.scad // Common useful shapes and structured objects. -// To use, add the following lines to the beginning of your file: -// ``` +// Includes: // include -// ``` ////////////////////////////////////////////////////////////////////// // Section: Cuboids - // Module: cuboid() // +// Usage: Standard Cubes +// cuboid(size, , , ); +// cuboid(size, p1=, ...); +// cuboid(p1=, p2=, ...); +// Usage: Chamfered Cubes +// cuboid(size, , , , , ...); +// Usage: Rounded Cubes +// cuboid(size, , , , , ...); +// Usage: Attaching children +// cuboid(size, , ...) ; +// // Description: // Creates a cube or cuboid object, with optional chamfering or rounding. // Negative chamfers and roundings can be applied to create external masks, @@ -20,6 +28,7 @@ // // Arguments: // size = The size of the cube. +// --- // chamfer = Size of chamfer, inset from sides. Default: No chamfering. // rounding = Radius of the edge rounding. Default: No rounding. // edges = Edges to chamfer/round. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. @@ -185,7 +194,8 @@ module cuboid( // Add multi-edge corners. if (trimcorners) { for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { - if (corner_edge_count(edges, [xa,ya,za]) > 1) { + ce = corner_edges(edges, [xa,ya,za]); + if (ce.x + ce.y > 1) { translate(vmul([xa,ya,za]/2, size+[ach-0.01,ach-0.01,-ach])) { cube([ach+0.01,ach+0.01,ach], center=true); } @@ -270,7 +280,8 @@ module cuboid( // Add multi-edge corners. if (trimcorners) { for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { - if (corner_edge_count(edges, [xa,ya,za]) > 1) { + ce = corner_edges(edges, [xa,ya,za]); + if (ce.x + ce.y > 1) { translate(vmul([xa,ya,za]/2, size+[ard-0.01,ard-0.01,-ard])) { cube([ard+0.01,ard+0.01,ard], center=true); } @@ -311,6 +322,19 @@ module cuboid( } } +function cuboid( + size=[1,1,1], + p1, p2, + chamfer, + rounding, + edges=EDGES_ALL, + except_edges=[], + trimcorners=true, + anchor=CENTER, + spin=0, + orient=UP +) = no_function("cuboid"); + // Section: Prismoids @@ -318,12 +342,19 @@ module cuboid( // Function&Module: prismoid() // -// Usage: As Module -// prismoid(size1, size2, h|l, [shift], [rounding], [chamfer]); -// prismoid(size1, size2, h|l, [shift], [rounding1], [rounding2], [chamfer1], [chamfer2]); +// Usage: Typical Prismoids +// prismoid(size1, size2, h|l, , ...); +// Usage: Attaching Children +// prismoid(size1, size2, h|l, , ...) ; +// Usage: Chamfered Prismoids +// prismoid(size1, size2, h|l, , ...); +// prismoid(size1, size2, h|l, , , ...); +// Usage: Rounded Prismoids +// prismoid(size1, size2, h|l, , ...); +// prismoid(size1, size2, h|l, , , ...); // Usage: As Function -// vnf = prismoid(size1, size2, h|l, [shift], [rounding], [chamfer]); -// vnf = prismoid(size1, size2, h|l, [shift], [rounding1], [rounding2], [chamfer1], [chamfer2]); +// vnf = prismoid(size1, size2, h|l, , , ); +// vnf = prismoid(size1, size2, h|l, , , , , ); // // Description: // Creates a rectangular prismoid shape with optional roundovers and chamfering. @@ -335,16 +366,17 @@ module cuboid( // ``` // // Arguments: -// size1 = [width, length] of the axis-negative end of the prism. -// size2 = [width, length] of the axis-positive end of the prism. +// size1 = [width, length] of the bottom end of the prism. +// size2 = [width, length] of the top end of the prism. // h|l = Height of the prism. -// shift = [X,Y] amount to shift the center of the top with respect to the center of the bottom. -// rounding = The roundover radius for the edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding) -// rounding1 = The roundover radius for the bottom corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. -// rounding2 = The roundover radius for the top corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. -// chamfer = The chamfer size for the edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no chamfer) -// chamfer1 = The chamfer size for the bottom corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. -// chamfer2 = The chamfer size for the top corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// shift = [X,Y] amount to shift the center of the top end with respect to the center of the bottom end. +// --- +// rounding = The roundover radius for the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding) +// rounding1 = The roundover radius for the bottom of the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// rounding2 = The roundover radius for the top of the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// chamfer = The chamfer size for the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no chamfer) +// chamfer1 = The chamfer size for the bottom of the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// chamfer2 = The chamfer size for the top of the vertical-ish edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -363,7 +395,7 @@ module cuboid( // prismoid(size1=[15,5], size2=[30,20], h=20); // Example: Right Prism // prismoid(size1=[30,60], size2=[0,60], shift=[-15,0], h=30); -// Example(FlatSpin): Shifting/Skewing +// Example(FlatSpin,VPD=160,VPT=[0,0,10]): Shifting/Skewing // prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]); // Example: Rounding // include @@ -390,7 +422,7 @@ module cuboid( // chamfer1=[0,5,0,10], chamfer2=[5,0,10,0], // rounding1=[5,0,10,0], rounding2=[0,5,0,10] // ); -// Example(Spin): Standard Connectors +// Example(Spin,VPD=160,VPT=[0,0,10]): Standard Connectors // prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]) // show_anchors(); module prismoid( @@ -487,16 +519,206 @@ function prismoid( ) reorient(anchor,spin,orient, size=[s1.x,s1.y,h], size2=s2, shift=shift, p=vnf); +// Module: rect_tube() +// Usage: Typical Rectangular Tubes +// rect_tube(h, size, isize,
, ); +// rect_tube(h, size, wall=, ); +// rect_tube(h, isize=, wall=, ); +// Usage: Tapering Rectangular Tubes +// rect_tube(h, size1=, size2=, wall=, ...); +// rect_tube(h, isize1=, isize2=, wall=, ...); +// rect_tube(h, size1=, size2=, isize1=, isize2=, ...); +// Usage: Chamfered +// rect_tube(h, size, isize, chamfer=, ...); +// rect_tube(h, size, isize, chamfer1=, chamfer2= ...); +// rect_tube(h, size, isize, ichamfer=, ...); +// rect_tube(h, size, isize, ichamfer1=, ichamfer2= ...); +// rect_tube(h, size, isize, chamfer=, ichamfer=, ...); +// Usage: Rounded +// rect_tube(h, size, isize, rounding=, ...); +// rect_tube(h, size, isize, rounding1=, rounding2= ...); +// rect_tube(h, size, isize, irounding=, ...); +// rect_tube(h, size, isize, irounding1=, irounding2= ...); +// rect_tube(h, size, isize, rounding=, irounding=, ...); +// Usage: Attaching Children +// rect_tube(h, size, isize, ...) ; +// +// Description: +// Creates a rectangular or prismoid tube with optional roundovers and/or chamfers. +// You can only round or chamfer the vertical(ish) edges. For those edges, you can +// specify rounding and/or chamferring per-edge, and for top and bottom, inside and +// outside separately. +// Note: if using chamfers or rounding, you **must** also include the hull.scad file: +// ``` +// include +// ``` +// Arguments: +// h|l = The height or length of the rectangular tube. Default: 1 +// size = The outer [X,Y] size of the rectangular tube. +// isize = The inner [X,Y] size of the rectangular tube. +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP`. +// shift = [X,Y] amount to shift the center of the top end with respect to the center of the bottom end. +// --- +// wall = The thickness of the rectangular tube wall. +// size1 = The [X,Y] size of the outside of the bottom of the rectangular tube. +// size2 = The [X,Y] size of the outside of the top of the rectangular tube. +// isize1 = The [X,Y] size of the inside of the bottom of the rectangular tube. +// isize2 = The [X,Y] size of the inside of the top of the rectangular tube. +// rounding = The roundover radius for the outside edges of the rectangular tube. +// rounding1 = The roundover radius for the outside bottom corner of the rectangular tube. +// rounding2 = The roundover radius for the outside top corner of the rectangular tube. +// chamfer = The chamfer size for the outside edges of the rectangular tube. +// chamfer1 = The chamfer size for the outside bottom corner of the rectangular tube. +// chamfer2 = The chamfer size for the outside top corner of the rectangular tube. +// irounding = The roundover radius for the inside edges of the rectangular tube. Default: Same as `rounding` +// irounding1 = The roundover radius for the inside bottom corner of the rectangular tube. +// irounding2 = The roundover radius for the inside top corner of the rectangular tube. +// ichamfer = The chamfer size for the inside edges of the rectangular tube. Default: Same as `chamfer` +// ichamfer1 = The chamfer size for the inside bottom corner of the rectangular tube. +// ichamfer2 = The chamfer size for the inside top corner of the rectangular tube. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `BOTTOM` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Examples: +// rect_tube(size=50, wall=5, h=30); +// rect_tube(size=[100,60], wall=5, h=30); +// rect_tube(isize=[60,80], wall=5, h=30); +// rect_tube(size=[100,60], isize=[90,50], h=30); +// rect_tube(size1=[100,60], size2=[70,40], wall=5, h=30); +// rect_tube(size1=[100,60], size2=[70,40], isize1=[40,20], isize2=[65,35], h=15); +// Example: Outer Rounding Only +// include +// rect_tube(size=100, wall=5, rounding=10, irounding=0, h=30); +// Example: Outer Chamfer Only +// include +// rect_tube(size=100, wall=5, chamfer=5, ichamfer=0, h=30); +// Example: Outer Rounding, Inner Chamfer +// include +// rect_tube(size=100, wall=5, rounding=10, ichamfer=8, h=30); +// Example: Inner Rounding, Outer Chamfer +// include +// rect_tube(size=100, wall=5, chamfer=10, irounding=8, h=30); +// Example: Gradiant Rounding +// include +// rect_tube(size1=100, size2=80, wall=5, rounding1=10, rounding2=0, irounding1=8, irounding2=0, h=30); +// Example: Per Corner Rounding +// include +// rect_tube(size=100, wall=10, rounding=[0,5,10,15], irounding=0, h=30); +// Example: Per Corner Chamfer +// include +// rect_tube(size=100, wall=10, chamfer=[0,5,10,15], ichamfer=0, h=30); +// Example: Mixing Chamfer and Rounding +// include +// rect_tube(size=100, wall=10, chamfer=[0,5,0,10], ichamfer=0, rounding=[5,0,10,0], irounding=0, h=30); +// Example: Really Mixing It Up +// include +// rect_tube( +// size1=[100,80], size2=[80,60], +// isize1=[50,30], isize2=[70,50], h=20, +// chamfer1=[0,5,0,10], ichamfer1=[0,3,0,8], +// chamfer2=[5,0,10,0], ichamfer2=[3,0,8,0], +// rounding1=[5,0,10,0], irounding1=[3,0,8,0], +// rounding2=[0,5,0,10], irounding2=[0,3,0,8] +// ); +module rect_tube( + h, size, isize, center, shift=[0,0], + wall, size1, size2, isize1, isize2, + rounding=0, rounding1, rounding2, + irounding=0, irounding1, irounding2, + chamfer=0, chamfer1, chamfer2, + ichamfer=0, ichamfer1, ichamfer2, + anchor, spin=0, orient=UP, + l +) { + h = one_defined([h,l],"h,l"); + assert(is_num(h), "l or h argument required."); + assert(is_vector(shift,2)); + s1 = is_num(size1)? [size1, size1] : + is_vector(size1,2)? size1 : + is_num(size)? [size, size] : + is_vector(size,2)? size : + undef; + s2 = is_num(size2)? [size2, size2] : + is_vector(size2,2)? size2 : + is_num(size)? [size, size] : + is_vector(size,2)? size : + undef; + is1 = is_num(isize1)? [isize1, isize1] : + is_vector(isize1,2)? isize1 : + is_num(isize)? [isize, isize] : + is_vector(isize,2)? isize : + undef; + is2 = is_num(isize2)? [isize2, isize2] : + is_vector(isize2,2)? isize2 : + is_num(isize)? [isize, isize] : + is_vector(isize,2)? isize : + undef; + size1 = is_def(s1)? s1 : + (is_def(wall) && is_def(is1))? (is1+2*[wall,wall]) : + undef; + size2 = is_def(s2)? s2 : + (is_def(wall) && is_def(is2))? (is2+2*[wall,wall]) : + undef; + isize1 = is_def(is1)? is1 : + (is_def(wall) && is_def(s1))? (s1-2*[wall,wall]) : + undef; + isize2 = is_def(is2)? is2 : + (is_def(wall) && is_def(s2))? (s2-2*[wall,wall]) : + undef; + assert(wall==undef || is_num(wall)); + assert(size1!=undef, "Bad size/size1 argument."); + assert(size2!=undef, "Bad size/size2 argument."); + assert(isize1!=undef, "Bad isize/isize1 argument."); + assert(isize2!=undef, "Bad isize/isize2 argument."); + assert(isize1.x < size1.x, "Inner size is larger than outer size."); + assert(isize1.y < size1.y, "Inner size is larger than outer size."); + assert(isize2.x < size2.x, "Inner size is larger than outer size."); + assert(isize2.y < size2.y, "Inner size is larger than outer size."); + anchor = get_anchor(anchor, center, BOT, BOT); + attachable(anchor,spin,orient, size=[each size1, h], size2=size2, shift=shift) { + diff("_H_o_L_e_") + prismoid( + size1, size2, h=h, shift=shift, + rounding=rounding, rounding1=rounding1, rounding2=rounding2, + chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, + anchor=CTR + ) { + children(); + tags("_H_o_L_e_") prismoid( + isize1, isize2, h=h+0.05, shift=shift, + rounding=irounding, rounding1=irounding1, rounding2=irounding2, + chamfer=ichamfer, chamfer1=ichamfer1, chamfer2=ichamfer2, + anchor=CTR + ); + } + children(); + } +} + +function rect_tube( + h, size, isize, center, shift=[0,0], + wall, size1, size2, isize1, isize2, + rounding=0, rounding1, rounding2, + irounding=0, irounding1, irounding2, + chamfer=0, chamfer1, chamfer2, + ichamfer=0, ichamfer1, ichamfer2, + anchor, spin=0, orient=UP, + l +) = no_function("rect_tube"); + + // Module: right_triangle() // // Usage: -// right_triangle(size, [center]); +// right_triangle(size,
); // // Description: // Creates a 3D right triangular prism with the hypotenuse in the X+Y+ quadrant. // // Arguments: // size = [width, thickness, height] +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP`. +// --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `ALLNEG` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -522,6 +744,9 @@ module right_triangle(size=[1, 1, 1], center, anchor, spin=0, orient=UP) } +function right_triangle(size=[1,1,1], center, anchor, spin=0, orient=UP) = + no_function("right_triangle"); + // Section: Cylindroids @@ -529,32 +754,34 @@ module right_triangle(size=[1, 1, 1], center, anchor, spin=0, orient=UP) // Module: cyl() // // Description: -// Creates cylinders in various anchors and orientations, -// with optional rounding and chamfers. You can use `r` and `l` -// interchangably, and all variants allow specifying size -// by either `r`|`d`, or `r1`|`d1` and `r2`|`d2`. -// Note that that chamfers and rounding cannot cross the -// midpoint of the cylinder's length. +// Creates cylinders in various anchorings and orientations, with optional rounding and chamfers. +// You can use `h` and `l` interchangably, and all variants allow specifying size by either `r`|`d`, +// or `r1`|`d1` and `r2`|`d2`. Note: the chamfers and rounding cannot be cumulatively longer than +// the cylinder's length. // // Usage: Normal Cylinders -// cyl(l|h, r|d, [circum], [realign], [center]); -// cyl(l|h, r1|d1, r2/d2, [circum], [realign], [center]); +// cyl(l|h, r,
, , ); +// cyl(l|h, d=, ...); +// cyl(l|h, r1=, r2=, ...); +// cyl(l|h, d1=, d2=, ...); // // Usage: Chamferred Cylinders -// cyl(l|h, r|d, chamfer, [chamfang], [from_end], [circum], [realign], [center]); -// cyl(l|h, r|d, chamfer1, [chamfang1], [from_end], [circum], [realign], [center]); -// cyl(l|h, r|d, chamfer2, [chamfang2], [from_end], [circum], [realign], [center]); -// cyl(l|h, r|d, chamfer1, chamfer2, [chamfang1], [chamfang2], [from_end], [circum], [realign], [center]); +// cyl(l|h, r|d, chamfer=, , , ...); +// cyl(l|h, r|d, chamfer1=, , , ...); +// cyl(l|h, r|d, chamfer2=, , , ...); +// cyl(l|h, r|d, chamfer1=, chamfer2=, , , , ...); // // Usage: Rounded End Cylinders -// cyl(l|h, r|d, rounding, [circum], [realign], [center]); -// cyl(l|h, r|d, rounding1, [circum], [realign], [center]); -// cyl(l|h, r|d, rounding2, [circum], [realign], [center]); -// cyl(l|h, r|d, rounding1, rounding2, [circum], [realign], [center]); +// cyl(l|h, r|d, rounding=, ...); +// cyl(l|h, r|d, rounding1=, ...); +// cyl(l|h, r|d, rounding2=, ...); +// cyl(l|h, r|d, rounding1=, rounding2=, ...); // // Arguments: -// l / h = Length of cylinder along oriented axis. (Default: 1.0) -// r = Radius of cylinder. +// l / h = Length of cylinder along oriented axis. Default: 1 +// r = Radius of cylinder. Default: 1 +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=DOWN`. +// --- // r1 = Radius of the negative (X-, Y-, Z-) end of cylinder. // r2 = Radius of the positive (X+, Y+, Z+) end of cylinder. // d = Diameter of cylinder. @@ -562,20 +789,19 @@ module right_triangle(size=[1, 1, 1], center, anchor, spin=0, orient=UP) // d2 = Diameter of the positive (X+, Y+, Z+) end of cylinder. // circum = If true, cylinder should circumscribe the circle of the given size. Otherwise inscribes. Default: `false` // chamfer = The size of the chamfers on the ends of the cylinder. Default: none. -// chamfer1 = The size of the chamfer on the axis-negative end of the cylinder. Default: none. -// chamfer2 = The size of the chamfer on the axis-positive end of the cylinder. Default: none. +// chamfer1 = The size of the chamfer on the bottom end of the cylinder. Default: none. +// chamfer2 = The size of the chamfer on the top end of the cylinder. Default: none. // chamfang = The angle in degrees of the chamfers on the ends of the cylinder. -// chamfang1 = The angle in degrees of the chamfer on the axis-negative end of the cylinder. -// chamfang2 = The angle in degrees of the chamfer on the axis-positive end of the cylinder. +// chamfang1 = The angle in degrees of the chamfer on the bottom end of the cylinder. +// chamfang2 = The angle in degrees of the chamfer on the top end of the cylinder. // from_end = If true, chamfer is measured from the end of the cylinder, instead of inset from the edge. Default: `false`. // rounding = The radius of the rounding on the ends of the cylinder. Default: none. -// rounding1 = The radius of the rounding on the axis-negative end of the cylinder. -// rounding2 = The radius of the rounding on the axis-positive end of the cylinder. +// rounding1 = The radius of the rounding on the bottom end of the cylinder. +// rounding2 = The radius of the rounding on the top end of the cylinder. // realign = If true, rotate the cylinder by half the angle of one face. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=DOWN`. // // Example: By Radius // xdistribute(30) { @@ -624,32 +850,34 @@ module right_triangle(size=[1, 1, 1], center, anchor, spin=0, orient=UP) // } // module cyl( - l=undef, h=undef, - r=undef, r1=undef, r2=undef, - d=undef, d1=undef, d2=undef, - chamfer=undef, chamfer1=undef, chamfer2=undef, - chamfang=undef, chamfang1=undef, chamfang2=undef, - rounding=undef, rounding1=undef, rounding2=undef, + h, r, center, + l, r1, r2, + d, d1, d2, + chamfer, chamfer1, chamfer2, + chamfang, chamfang1, chamfang2, + rounding, rounding1, rounding2, circum=false, realign=false, from_end=false, - center, anchor, spin=0, orient=UP + anchor, spin=0, orient=UP ) { - r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=1); - r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=1); l = first_defined([l, h, 1]); - sides = segs(max(r1,r2)); + _r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=1); + _r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=1); + sides = segs(max(_r1,_r2)); sc = circum? 1/cos(180/sides) : 1; + r1=_r1*sc; + r2=_r2*sc; phi = atan2(l, r2-r1); anchor = get_anchor(anchor,center,BOT,CENTER); attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { zrot(realign? 180/sides : 0) { if (!any_defined([chamfer, chamfer1, chamfer2, rounding, rounding1, rounding2])) { - cylinder(h=l, r1=r1*sc, r2=r2*sc, center=true, $fn=sides); + cylinder(h=l, r1=r1, r2=r2, center=true, $fn=sides); } else { vang = atan2(l, r1-r2)/2; chang1 = 90-first_defined([chamfang1, chamfang, vang]); chang2 = 90-first_defined([chamfang2, chamfang, 90-vang]); - cham1 = first_defined([chamfer1, chamfer]) * (from_end? 1 : tan(chang1)); - cham2 = first_defined([chamfer2, chamfer]) * (from_end? 1 : tan(chang2)); + cham1 = u_mul(first_defined([chamfer1, chamfer]) , (from_end? 1 : tan(chang1))); + cham2 = u_mul(first_defined([chamfer2, chamfer]) , (from_end? 1 : tan(chang2))); fil1 = first_defined([rounding1, rounding]); fil2 = first_defined([rounding2, rounding]); if (chamfer != undef) { @@ -727,13 +955,17 @@ module cyl( // Description: // Creates a cylinder oriented along the X axis. // -// Usage: -// xcyl(l|h, r|d, [anchor]); -// xcyl(l|h, r1|d1, r2|d2, [anchor]); +// Usage: Typical +// xcyl(l|h, r, ); +// xcyl(l|h, d=, ); +// xcyl(l|h, r1=|d1=, r2=|d2=, ); +// Usage: Attaching Children +// xcyl(l|h, r, ) ; // // Arguments: -// l / h = Length of cylinder along oriented axis. (Default: `1.0`) -// r = Radius of cylinder. +// l / h = Length of cylinder along oriented axis. Default: 1 +// r = Radius of cylinder. Default: 1 +// --- // r1 = Optional radius of left (X-) end of cylinder. // r2 = Optional radius of right (X+) end of cylinder. // d = Optional diameter of cylinder. (use instead of `r`) @@ -752,7 +984,7 @@ module cyl( // xcyl(l=35, d=20); // xcyl(l=35, d1=30, d2=10); // } -module xcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h=undef, anchor=CENTER) +module xcyl(h, r, d, r1, r2, d1, d2, l, anchor=CENTER) { r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=1); r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=1); @@ -770,13 +1002,17 @@ module xcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h // Description: // Creates a cylinder oriented along the Y axis. // -// Usage: -// ycyl(l|h, r|d, [anchor]); -// ycyl(l|h, r1|d1, r2|d2, [anchor]); +// Usage: Typical +// ycyl(l|h, r, ); +// ycyl(l|h, d=, ); +// ycyl(l|h, r1=|d1=, r2=|d2=, ); +// Usage: Attaching Children +// ycyl(l|h, r, ) ; // // Arguments: // l / h = Length of cylinder along oriented axis. (Default: `1.0`) // r = Radius of cylinder. +// --- // r1 = Radius of front (Y-) end of cone. // r2 = Radius of back (Y+) end of one. // d = Diameter of cylinder. @@ -795,7 +1031,7 @@ module xcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h // ycyl(l=35, d=20); // ycyl(l=35, d1=30, d2=10); // } -module ycyl(l, r, d, r1, r2, d1, d2, h, anchor=CENTER) +module ycyl(h, r, d, r1, r2, d1, d2, l, anchor=CENTER) { r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=1); r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=1); @@ -813,13 +1049,17 @@ module ycyl(l, r, d, r1, r2, d1, d2, h, anchor=CENTER) // Description: // Creates a cylinder oriented along the Z axis. // -// Usage: -// zcyl(l|h, r|d, [anchor]); -// zcyl(l|h, r1|d1, r2|d2, [anchor]); +// Usage: Typical +// zcyl(l|h, r, ); +// zcyl(l|h, d=, ); +// zcyl(l|h, r1=|d1=, r2=|d2=, ); +// Usage: Attaching Children +// zcyl(l|h, r, ) ; // // Arguments: // l / h = Length of cylinder along oriented axis. (Default: 1.0) // r = Radius of cylinder. +// --- // r1 = Radius of front (Y-) end of cone. // r2 = Radius of back (Y+) end of one. // d = Diameter of cylinder. @@ -838,7 +1078,7 @@ module ycyl(l, r, d, r1, r2, d1, d2, h, anchor=CENTER) // zcyl(l=35, d=20); // zcyl(l=35, d1=30, d2=10); // } -module zcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h=undef, anchor=CENTER) +module zcyl(h, r, d, r1, r2, d1, d2, l, anchor=CENTER) { cyl(l=l, h=h, r=r, r1=r1, r2=r2, d=d, d1=d1, d2=d2, orient=UP, anchor=anchor) children(); } @@ -850,27 +1090,32 @@ module zcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h // Description: // Makes a hollow tube with the given outer size and wall thickness. // -// Usage: -// tube(h|l, ir|id, wall, [realign]); -// tube(h|l, or|od, wall, [realign]); -// tube(h|l, ir|id, or|od, [realign]); -// tube(h|l, ir1|id1, ir2|id2, wall, [realign]); -// tube(h|l, or1|od1, or2|od2, wall, [realign]); -// tube(h|l, ir1|id1, ir2|id2, or1|od1, or2|od2, [realign]); +// Usage: Typical +// tube(h|l, or, ir,
, ); +// tube(h|l, or=|od=, ir=|id=, ...); +// tube(h|l, ir|id, wall, ...); +// tube(h|l, or|od, wall, ...); +// tube(h|l, ir1|id1, ir2|id2, wall, ...); +// tube(h|l, or1|od1, or2|od2, wall, ...); +// tube(h|l, ir1|id1, ir2|id2, or1|od1, or2|od2, ); +// Usage: Attaching Children +// tube(h|l, or, ir,
) ; // // Arguments: -// h|l = height of tube. (Default: 1) -// or = Outer radius of tube. -// or1 = Outer radius of bottom of tube. (Default: value of r) -// or2 = Outer radius of top of tube. (Default: value of r) +// h / l = height of tube. Default: 1 +// or = Outer radius of tube. Default: 1 +// ir = Inner radius of tube. +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=DOWN`. +// --- // od = Outer diameter of tube. +// id = Inner diameter of tube. +// wall = horizontal thickness of tube wall. Default 0.5 +// or1 = Outer radius of bottom of tube. Default: value of r) +// or2 = Outer radius of top of tube. Default: value of r) // od1 = Outer diameter of bottom of tube. // od2 = Outer diameter of top of tube. -// wall = horizontal thickness of tube wall. (Default 0.5) -// ir = Inner radius of tube. // ir1 = Inner radius of bottom of tube. // ir2 = Inner radius of top of tube. -// id = Inner diameter of tube. // id1 = Inner diameter of bottom of tube. // id2 = Inner diameter of top of tube. // realign = If true, rotate the tube by half the angle of one face. @@ -892,34 +1137,22 @@ module zcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h // Example: Standard Connectors // tube(h=30, or=40, wall=5) show_anchors(); module tube( - h, wall=undef, - r=undef, r1=undef, r2=undef, - d=undef, d1=undef, d2=undef, - or=undef, or1=undef, or2=undef, - od=undef, od1=undef, od2=undef, - ir=undef, id=undef, ir1=undef, - ir2=undef, id1=undef, id2=undef, - anchor, spin=0, orient=UP, - center, realign=false, l + h, or, ir, center, + od, id, wall, + or1, or2, od1, od2, + ir1, ir2, id1, id2, + realign=false, l, + anchor, spin=0, orient=UP ) { - function safe_add(x,wall) = is_undef(x)? undef : x+wall; h = first_defined([h,l,1]); - orr1 = get_radius( - r=first_defined([or1, r1, or, r]), - d=first_defined([od1, d1, od, d]), - dflt=undef - ); - orr2 = get_radius( - r=first_defined([or2, r2, or, r]), - d=first_defined([od2, d2, od, d]), - dflt=undef - ); + orr1 = get_radius(r1=or1, r=or, d1=od1, d=od, dflt=undef); + orr2 = get_radius(r1=or2, r=or, d1=od2, d=od, dflt=undef); irr1 = get_radius(r1=ir1, r=ir, d1=id1, d=id, dflt=undef); irr2 = get_radius(r1=ir2, r=ir, d1=id2, d=id, dflt=undef); - r1 = is_num(orr1)? orr1 : is_num(irr1)? irr1+wall : undef; - r2 = is_num(orr2)? orr2 : is_num(irr2)? irr2+wall : undef; - ir1 = is_num(irr1)? irr1 : is_num(orr1)? orr1-wall : undef; - ir2 = is_num(irr2)? irr2 : is_num(orr2)? orr2-wall : undef; + r1 = default(orr1, u_add(irr1,wall)); + r2 = default(orr2, u_add(irr2,wall)); + ir1 = default(irr1, u_sub(orr1,wall)); + ir2 = default(irr2, u_sub(orr2,wall)); assert(ir1 <= r1, "Inner radius is larger than outer radius."); assert(ir2 <= r2, "Inner radius is larger than outer radius."); sides = segs(max(r1,r2)); @@ -936,180 +1169,56 @@ module tube( } -// Module: rect_tube() -// Usage: -// rect_tube(size, wall, h, [center]); -// rect_tube(isize, wall, h, [center]); -// rect_tube(size, isize, h, [center]); -// rect_tube(size1, size2, wall, h, [center]); -// rect_tube(isize1, isize2, wall, h, [center]); -// rect_tube(size1, size2, isize1, isize2, h, [center]); -// Description: -// Creates a rectangular or prismoid tube with optional roundovers and/or chamfers. -// You can only round or chamfer the vertical(ish) edges. For those edges, you can -// specify rounding and/or chamferring per-edge, and for top and bottom, inside and -// outside separately. -// Note: if using chamfers or rounding, you **must** also include the hull.scad file: -// ``` -// include -// ``` -// Arguments: -// size = The outer [X,Y] size of the rectangular tube. -// isize = The inner [X,Y] size of the rectangular tube. -// h|l = The height or length of the rectangular tube. Default: 1 -// wall = The thickness of the rectangular tube wall. -// size1 = The [X,Y] side of the outside of the bottom of the rectangular tube. -// size2 = The [X,Y] side of the outside of the top of the rectangular tube. -// isize1 = The [X,Y] side of the inside of the bottom of the rectangular tube. -// isize2 = The [X,Y] side of the inside of the top of the rectangular tube. -// rounding = The roundover radius for the outside edges of the rectangular tube. -// rounding1 = The roundover radius for the outside bottom corner of the rectangular tube. -// rounding2 = The roundover radius for the outside top corner of the rectangular tube. -// chamfer = The chamfer size for the outside edges of the rectangular tube. -// chamfer1 = The chamfer size for the outside bottom corner of the rectangular tube. -// chamfer2 = The chamfer size for the outside top corner of the rectangular tube. -// irounding = The roundover radius for the inside edges of the rectangular tube. Default: Same as `rounding` -// irounding1 = The roundover radius for the inside bottom corner of the rectangular tube. -// irounding2 = The roundover radius for the inside top corner of the rectangular tube. -// ichamfer = The chamfer size for the inside edges of the rectangular tube. Default: Same as `chamfer` -// ichamfer1 = The chamfer size for the inside bottom corner of the rectangular tube. -// ichamfer2 = The chamfer size for the inside top corner of the rectangular tube. -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `BOTTOM` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Examples: -// rect_tube(size=50, wall=5, h=30); -// rect_tube(size=[100,60], wall=5, h=30); -// rect_tube(isize=[60,80], wall=5, h=30); -// rect_tube(size=[100,60], isize=[90,50], h=30); -// rect_tube(size1=[100,60], size2=[70,40], wall=5, h=30); -// rect_tube(size1=[100,60], size2=[70,40], isize1=[40,20], isize2=[65,35], h=15); -// Example: Outer Rounding Only -// include -// rect_tube(size=100, wall=5, rounding=10, irounding=0, h=30); -// Example: Outer Chamfer Only -// include -// rect_tube(size=100, wall=5, chamfer=5, ichamfer=0, h=30); -// Example: Outer Rounding, Inner Chamfer -// include -// rect_tube(size=100, wall=5, rounding=10, ichamfer=8, h=30); -// Example: Inner Rounding, Outer Chamfer -// include -// rect_tube(size=100, wall=5, chamfer=10, irounding=8, h=30); -// Example: Gradiant Rounding -// include -// rect_tube(size1=100, size2=80, wall=5, rounding1=10, rounding2=0, irounding1=8, irounding2=0, h=30); -// Example: Per Corner Rounding -// include -// rect_tube(size=100, wall=10, rounding=[0,5,10,15], irounding=0, h=30); -// Example: Per Corner Chamfer -// include -// rect_tube(size=100, wall=10, chamfer=[0,5,10,15], ichamfer=0, h=30); -// Example: Mixing Chamfer and Rounding -// include -// rect_tube(size=100, wall=10, chamfer=[0,5,0,10], ichamfer=0, rounding=[5,0,10,0], irounding=0, h=30); -// Example: Really Mixing It Up -// include -// rect_tube( -// size1=[100,80], size2=[80,60], -// isize1=[50,30], isize2=[70,50], h=20, -// chamfer1=[0,5,0,10], ichamfer1=[0,3,0,8], -// chamfer2=[5,0,10,0], ichamfer2=[3,0,8,0], -// rounding1=[5,0,10,0], irounding1=[3,0,8,0], -// rounding2=[0,5,0,10], irounding2=[0,3,0,8] -// ); -module rect_tube( - size, isize, - h, shift=[0,0], wall, - size1, size2, - isize1, isize2, - rounding=0, rounding1, rounding2, - irounding=0, irounding1, irounding2, - chamfer=0, chamfer1, chamfer2, - ichamfer=0, ichamfer1, ichamfer2, - anchor, spin=0, orient=UP, - center, l -) { - h = first_defined([h,l,1]); - assert(is_num(h), "l or h argument required."); - assert(is_vector(shift,2)); - s1 = is_num(size1)? [size1, size1] : - is_vector(size1,2)? size1 : - is_num(size)? [size, size] : - is_vector(size,2)? size : - undef; - s2 = is_num(size2)? [size2, size2] : - is_vector(size2,2)? size2 : - is_num(size)? [size, size] : - is_vector(size,2)? size : - undef; - is1 = is_num(isize1)? [isize1, isize1] : - is_vector(isize1,2)? isize1 : - is_num(isize)? [isize, isize] : - is_vector(isize,2)? isize : - undef; - is2 = is_num(isize2)? [isize2, isize2] : - is_vector(isize2,2)? isize2 : - is_num(isize)? [isize, isize] : - is_vector(isize,2)? isize : - undef; - size1 = is_def(s1)? s1 : - (is_def(wall) && is_def(is1))? (is1+2*[wall,wall]) : - undef; - size2 = is_def(s2)? s2 : - (is_def(wall) && is_def(is2))? (is2+2*[wall,wall]) : - undef; - isize1 = is_def(is1)? is1 : - (is_def(wall) && is_def(s1))? (s1-2*[wall,wall]) : - undef; - isize2 = is_def(is2)? is2 : - (is_def(wall) && is_def(s2))? (s2-2*[wall,wall]) : - undef; - assert(wall==undef || is_num(wall)); - assert(size1!=undef, "Bad size/size1 argument."); - assert(size2!=undef, "Bad size/size2 argument."); - assert(isize1!=undef, "Bad isize/isize1 argument."); - assert(isize2!=undef, "Bad isize/isize2 argument."); - assert(isize1.x < size1.x, "Inner size is larger than outer size."); - assert(isize1.y < size1.y, "Inner size is larger than outer size."); - assert(isize2.x < size2.x, "Inner size is larger than outer size."); - assert(isize2.y < size2.y, "Inner size is larger than outer size."); - anchor = get_anchor(anchor, center, BOT, BOT); - attachable(anchor,spin,orient, size=[each size1, h], size2=size2, shift=shift) { - diff("_H_o_L_e_") - prismoid( - size1, size2, h=h, shift=shift, - rounding=rounding, rounding1=rounding1, rounding2=rounding2, - chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, - anchor=CTR - ) { - children(); - tags("_H_o_L_e_") prismoid( - isize1, isize2, h=h+0.05, shift=shift, - rounding=irounding, rounding1=irounding1, rounding2=irounding2, - chamfer=ichamfer, chamfer1=ichamfer1, chamfer2=ichamfer2, - anchor=CTR - ); - } - children(); - } -} - - // Module: torus() // +// Usage: Typical +// torus(r_maj|d_maj, r_min|d_min,
, ...); +// torus(or|od, ir|id, ...); +// torus(r_maj|d_maj, or|od, ...); +// torus(r_maj|d_maj, ir|id, ...); +// torus(r_min|d_min, or|od, ...); +// torus(r_min|d_min, ir|id, ...); +// Usage: Attaching Children +// torus(or|od, ir|id, ...) ;; +// // Description: // Creates a torus shape. // -// Usage: -// torus(r|d, r2|d2); -// torus(or|od, ir|id); +// Figure(2D,Med): +// module text3d(t,size=8) text(text=t,size=size,font="Helvetica", halign="center",valign="center"); +// module dashcirc(r,start=0,angle=359.9,dashlen=5) let(step=360*dashlen/(2*r*PI)) for(a=[start:step:start+angle]) stroke(arc(r=r,start=a,angle=step/2)); +// r = 75; r2 = 30; +// down(r2+0.1) #torus(r_maj=r, r_min=r2, $fn=72); +// color("blue") linear_extrude(height=0.01) { +// dashcirc(r=r,start=15,angle=45); +// dashcirc(r=r-r2, start=90+15, angle=60); +// dashcirc(r=r+r2, start=180+45, angle=30); +// dashcirc(r=r+r2, start=15, angle=30); +// } +// rot(240) color("blue") linear_extrude(height=0.01) { +// stroke([[0,0],[r+r2,0]], endcaps="arrow2",width=2); +// right(r) fwd(9) rot(-240) text3d("or",size=10); +// } +// rot(135) color("blue") linear_extrude(height=0.01) { +// stroke([[0,0],[r-r2,0]], endcaps="arrow2",width=2); +// right((r-r2)/2) back(8) rot(-135) text3d("ir",size=10); +// } +// rot(45) color("blue") linear_extrude(height=0.01) { +// stroke([[0,0],[r,0]], endcaps="arrow2",width=2); +// right(r/2) back(8) text3d("r_maj",size=9); +// } +// rot(30) color("blue") linear_extrude(height=0.01) { +// stroke([[r,0],[r+r2,0]], endcaps="arrow2",width=2); +// right(r+r2/2) fwd(8) text3d("r_min",size=7); +// } // // Arguments: -// r = major radius of torus ring. (use with of 'r2', or 'd2') -// r2 = minor radius of torus ring. (use with of 'r', or 'd') -// d = major diameter of torus ring. (use with of 'r2', or 'd2') -// d2 = minor diameter of torus ring. (use with of 'r', or 'd') +// r_maj = major radius of torus ring. (use with 'r_min', or 'd_min') +// r_min = minor radius of torus ring. (use with 'r_maj', or 'd_maj') +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=DOWN`. +// --- +// d_maj = major diameter of torus ring. (use with 'r_min', or 'd_min') +// d_min = minor diameter of torus ring. (use with 'r_maj', or 'd_maj') // or = outer radius of the torus. (use with 'ir', or 'id') // ir = inside radius of the torus. (use with 'or', or 'od') // od = outer diameter of the torus. (use with 'ir' or 'id') @@ -1119,23 +1228,35 @@ module rect_tube( // // Example: // // These all produce the same torus. -// torus(r=22.5, r2=7.5); -// torus(d=45, d2=15); +// torus(r_maj=22.5, r_min=7.5); +// torus(d_maj=45, d_min=15); // torus(or=30, ir=15); // torus(od=60, id=30); +// torus(d_maj=45, id=30); +// torus(d_maj=45, od=60); +// torus(d_min=15, id=30); +// torus(d_min=15, od=60); // Example: Standard Connectors // torus(od=60, id=30) show_anchors(); module torus( - r=undef, d=undef, - r2=undef, d2=undef, - or=undef, od=undef, - ir=undef, id=undef, - center, anchor, spin=0, orient=UP + r_maj, r_min, center, + d_maj, d_min, + or, od, ir, id, + anchor, spin=0, orient=UP ) { - orr = get_radius(r=or, d=od, dflt=1.0); - irr = get_radius(r=ir, d=id, dflt=0.5); - majrad = get_radius(r=r, d=d, dflt=(orr+irr)/2); - minrad = get_radius(r=r2, d=d2, dflt=(orr-irr)/2); + _or = get_radius(r=or, d=od, dflt=undef); + _ir = get_radius(r=ir, d=id, dflt=undef); + _r_maj = get_radius(r=r_maj, d=d_maj, dflt=undef); + _r_min = get_radius(r=r_min, d=d_min, dflt=undef); + majrad = is_finite(_r_maj)? _r_maj : + is_finite(_ir) && is_finite(_or)? (_or + _ir)/2 : + is_finite(_ir) && is_finite(_r_min)? (_ir + _r_min) : + is_finite(_or) && is_finite(_r_min)? (_or - _r_min) : + assert(false, "Bad Parameters"); + minrad = is_finite(_r_min)? _r_min : + is_finite(_ir)? (majrad - _ir) : + is_finite(_or)? (_or - majrad) : + assert(false, "Bad Parameters"); anchor = get_anchor(anchor, center, BOT, CENTER); attachable(anchor,spin,orient, r=(majrad+minrad), l=minrad*2) { rotate_extrude(convexity=4) { @@ -1151,10 +1272,12 @@ module torus( // Function&Module: spheroid() -// Usage: As Module -// spheroid(r|d, [circum], [style]) +// Usage: Typical +// spheroid(r|d, ,