diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..798e666 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +variables: + GIT_SUBMODULE_STRATEGY: recursive + +stages: + - build + +debian_10: + stage: build + image: "registry.gitlab.com/gerbolyze/build-containers/debian:10" + script: + - "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + - "python3 setup.py install --user" + - "gerbolyze --help" + +ubuntu_2004: + stage: build + image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:20.04" + script: + - "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + - "python3 setup.py install --user" + - "gerbolyze --help" + +fedora_33: + stage: build + image: "registry.gitlab.com/gerbolyze/build-containers/fedora:33" + script: + - "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + - "python3 setup.py install --user" + - "gerbolyze --help" + +archlinux: + stage: build + image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest" + script: + - "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + - "python setup.py install --user" + - "gerbolyze --help" + diff --git a/.gitmodules b/.gitmodules index ea1cbfa..e42d3c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,18 @@ -[submodule "gerboweb/deploy/checkouts/pogojig"] - path = gerboweb/deploy/checkouts/pogojig - url = https://github.com/jaseg/pogojig.git -[submodule "gerboweb/deploy/library/ansible-collection"] - path = gerboweb/deploy/library/inwx-collection - url = https://github.com/inwx/ansible-collection +[submodule "upstream/cpp-base64"] + path = upstream/cpp-base64 + url = https://github.com/ReneNyffenegger/cpp-base64 +[submodule "upstream/voronoi"] + path = upstream/voronoi + url = https://github.com/JCash/voronoi +[submodule "upstream/poisson-disk-sampling"] + path = upstream/poisson-disk-sampling + url = https://github.com/thinks/poisson-disk-sampling +[submodule "upstream/argagg"] + path = upstream/argagg + url = https://github.com/vietjtnguyen/argagg +[submodule "upstream/CavalierContours"] + path = upstream/CavalierContours + url = https://github.com/jbuckmccready/CavalierContours +[submodule "upstream/subprocess.h"] + path = upstream/subprocess.h + url = https://github.com/sheredom/subprocess.h diff --git a/MANIFEST.in b/MANIFEST.in index 9561fb1..031eca7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,10 @@ -include README.rst +recursive-include svg-flatten * +recursive-include upstream * +recursive-exclude upstream/voronoi/test * +recursive-exclude upstream/subprocess.h/test * +recursive-exclude upstream/poisson-disk-sampling/thinks/poisson_disk_sampling/examples * +recursive-exclude upstream/poisson-disk-sampling/images * +recursive-exclude upstream/clipper-6.4.2/Documentation * +recursive-exclude upstream/CavalierContours tests/* examples/* +recursive-exclude upstream/argagg doc/* examples/* tests/* +recursive-exclude svg-flatten/build * diff --git a/README.rst b/README.rst index c937116..b042183 100644 --- a/README.rst +++ b/README.rst @@ -1,87 +1,568 @@ -Gerbolyze high-resolution image-to-PCB converter -================================================ +Gerbolyze high-fidelity SVG/PNG/JPG to PCB converter +==================================================== + +Gerbolyze renders SVG vector and PNG/JPG raster images into existing gerber PCB manufacturing files. +Vector data from SVG files is rendered losslessly *without* an intermediate rasterization/revectorization step. +Still, gerbolyze supports (almost) the full SVG 1.1 spec including complex, self-intersecting paths with holes, +patterns, dashes and transformations + +Raster images can either be vectorized through contour tracing (like gerbolyze v1.0 did) or they can be embedded using +high-resolution grayscale emulation while (mostly) guaranteeing trace/space design rules. + +.. figure:: pics/pcbway_sample_02_small.jpg + :width: 800px + + Drawing by `トーコ Toko `__ converted using Gerbolyze and printed at PCBWay. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample1.jpg Tooling for PCB art is quite limited in both open source and closed source ecosystems. Something as simple as putting a pretty picture on a PCB can be an extremely tedious task. Depending on the PCB tool used, various arcane incantations may be necessary and even modestly complex images will slow down most PCB tools to a crawl. -Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing bitmap files onto existing gerber -layers. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the industry-standard Altium -Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of thousands of primitives, -generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can finally be confident that -your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly anime silkscreen. +Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing SVG vector and PNG or JPG bitmap files +onto existing gerber layers. Gerbolyze processes any spec-compliant SVG and "gerbolyzes" SVG vector data into a Gerber +spec-compliant form. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the +industry-standard Altium Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of +thousands of primitives, generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can +finally be confident that your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly +anime silkscreen. + +.. image:: pics/process-overview.png + :width: 800px .. contents:: -Produce high-quality artistic PCBs in three easy steps! -------------------------------------------------------- +Tl;dr: Produce high-quality artistic PCBs in three easy steps! +-------------------------------------------------------------- Gerbolyze works in three steps. -1. Generate a scale-accurate preview of the finished PCB from your CAD tool's gerber output: +1. Generate a scale-accurate template of the finished PCB from your CAD tool's gerber output: .. code:: - $ gerbolyze render top my_gerber_dir preview.png + $ gerbolyze template --top template_top.svg [--bottom template_bottom.svg] my_gerber_dir -2. Load the resulting preview image into the GIMP or another image editing program. Use it as a guide to position scale - your artwork. Create a black-and-white image from your scaled artwork using GIMP's newsprint filter. Make sure most - details are larger than about 10px to ensure manufacturing goes smooth. +2. Load the resulting template image Inkscape_ or another SVG editing program. Put your artwork on the appropriate SVG + layer. Dark colors become filled gerber primitives, bright colors become unfilled primitives. You can directly put + raster images (PNG/JPG) into this SVG as well, just position and scale them like everything else. SVG clips work for + images, too. Masks are not supported. -3. Vectorize the resulting grayscale image drectly into the PCB's gerber files: +3. Vectorize the edited SVG template image drectly into the PCB's gerber files: .. code:: - $ gerbolyze vectorize top input_gerber_dir output_gerber_dir black_and_white_artwork.png + $ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir -Image preprocessing guide -------------------------- +Quick Start Installation +------------------------ -Nice black-and-white images can be generated from any grayscale image using the GIMP's newsprint filter. The -straight-forward pre-processing steps necessary for use by ``gerbolyze vectorize`` are as follows. +This will install gerbolyze and svg-flatten into a Python virtualenv and install usvg into your ``~/.cargo``. -1 Import a render of the board generated using ``gerbolyze render`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Note: + Right now (2020-02-07), ``pcb-tools-extension`` must be installed manually from the fork at: -``gerbolyze render`` will automatically scale the render such that ten pixels in the render correspond to 6mil on the -board, which is about the smallest detail most manufacturers can resolve on the silkscreen layer. You can control this -setting using the ``--fab-resolution`` and ``--oversampling`` options. Refer to ``gerbolyze --help`` for details. + ``pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git`` -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png + This fork contains fixes for compatibility issues with KiCAD nightlies that are still in the process of being + upstreamed. -2 Import your desired artwork +Debian +~~~~~~ + +Note: + Right now, debian stable ships with a rust that is so stable it can't even build half of usvg's dependencies. That's + why we yolo-install our own rust here. Sorry about that. I guess it'll work with the packaged rust on sid. + +.. code-block:: shell + + sudo apt install libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv + + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + source $HOME/.cargo/env + rustup install stable + rustup default stable + cargo install usvg + + pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git + pip3 install --user gerbolyze --no-binary gerbolyze + +Ubuntu +~~~~~~ + +.. code-block:: shell + + sudo apt install libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv cargo + cargo install usvg + + pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git + pip3 install --user gerbolyze --no-binary gerbolyze + + +Fedora +~~~~~~ + +.. code-block:: shell + + sudo dnf install python3 make clang opencv-devel pugixml-devel pango-devel cairo-devel rust cargo + cargo install usvg + + pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git + pip3 install --user gerbolyze --no-binary gerbolyze + +Arch +~~~~ + +.. code-block:: shell + + sudo pacman -S pugixml opencv pango cairo git python make clang rustup cargo pkgconf + + rustup install stable + rustup default stable + cargo install usvg + + pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git + pip3 install --user gerbolyze --no-binary gerbolyze + +macOS (via Homebrew) +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: shell + + # Tested on a fresh Mac OS 10.15.7 Catalina installation + + # Requires homebrew. To install, run: + # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n + # --> Now, restart the terminal app to load new $PATH from /etc/paths <-- + + brew install python3 rustup pugixml cairo pango opencv pkg-config + + rustup-init + cargo install usvg + + pip3 install git+https://git.jaseg.de/pcb-tools-extension.git + pip3 install gerbolyze --no-binary gerbolyze + +Build from source (any distro) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, install prerequisites like shown above. Then, + +.. code-block:: shell + + git clone --recurse-submodules https://git.jaseg.de/gerbolyze.git + cd gerbolyze + + pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git + python3 -m venv + source venv/bin/activate + python3 setup.py install + +Features +-------- + +Input on the left, output on the right. + +.. image:: pics/test_svg_readme_composited.png + :width: 800px + +* Almost full SVG 1.1 static spec coverage (!) + + * Paths with beziers, self-intersections and holes + * Strokes, even with dashes and markers + * Pattern fills and strokes + * Transformations and nested groups + * Proper text rendering with support for complex text layout (e.g. Arabic) + * elements via either built-in vectorizer or built-in halftone processor + * (some) CSS + +* Writes Gerber, SVG or KiCAD S-Expression (``.kicad_mod``) formats +* Can export from top/bottom SVGs to a whole gerber layer stack at once with filename autodetection +* Can export SVGs to ``.kicad_mod`` files like svg2mod (but with full SVG support) +* Beziers flattening with configurable tolerance using actual math! +* Polygon intersection removal +* Polygon hole removal (!) +* Optionally vector-compositing of output: convert black/white/transparent image to black/transparent image +* Renders SVG templates from input gerbers for accurate and easy scaling and positioning of artwork +* layer masking with offset (e.g. all silk within 1mm of soldermask) +* Can read gerbers from zip files + +Gerbolyze is the end-to-end "paste this svg into these gerbers" command that handles all layers on both board sides at +once. The heavy-duty computer geometry logic of gerbolyze is handled by the svg-flatten utility (``svg-flatten`` +directory). svg-flatten reads an SVG file and renders it into a variety of output formats. svg-flatten can be used like +a variant of the popular svg2mod that supports all of SVG and handles arbitrary input ```` elements. + +Algorithm Overview +------------------ + +This is the algorithm gerbolyze uses to process a stack of gerbers. + +* Map input files to semantic layers by their filenames +* For each layer: + + * load input gerber + * Pass mask layers through ``gerbv`` for conversion to SVG + * Pass mask layers SVG through ``svg-flatten --dilate`` + * Pass input SVG through ``svg-flatten --only-groups [layer]`` + * Overlay input gerber, mask and input svg + * Write result to output gerber + +This is the algorithm svg-flatten uses to process an SVG. + +* pass input SVG through usvg_ +* iterate depth-first through resulting SVG. + + * for groups: apply transforms and clip and recurse + * for images: Vectorize using selected vectorizer + * for paths: + + * flatten path using Cairo + * remove self-intersections using Clipper + * if stroke is set: process dash, then offset using Clipper + * apply pattern fills + * clip to clip-path + * remove holes using Clipper + +* for KiCAD S-Expression export: vector-composite results using CavalierContours: subtract each clear output primitive + from all previous dark output primitives + +Command-line usage +------------------ + +Generate SVG template from Gerber files: + +.. code-block:: shell + + gerbolyze template [options] [-t|--top top_side_output.svg] [-b|--bottom ...] input_dir_or.zip + +Render design from an SVG made with the template above into a set of gerber files: + +.. code-block:: shell + + gerbolyze paste [options] [-t|--top top_side_design.svg] [-b|--bottom ...] input_dir_or.zip output_dir + +Use svg-flatten to convert an SVG file into Gerber or flattened SVG: + +.. code-block:: shell + + svg-flatten [options] --format [gerber|svg] [input_file.svg] [output_file] + +Use svg-flatten to convert an SVG file into the given layer of a KiCAD S-Expression (``.kicad_mod``) file: + +.. code-block:: shell + + svg-flatten [options] --format kicad --sexp-layer F.SilkS --sexp-mod-name My_Module [input_file.svg] [output_file] + +Use svg-flatten to convert an SVG file into a ``.kicad_mod`` with SVG layers fed into separate KiCAD layers based on +their IDs like the popular ``svg2mod`` is doing: + +Note: + Right now, the input SVG's layers must have *ids* that match up KiCAD's s-exp layer names. Note that when you name + a layer in Inkscape that only sets a ``name`` attribute, but does not change the ID. In order to change the ID in + Inkscape, you have to use Inkscape's "object properties" context menu function. + + Also note that svg-flatten expects the layer names KiCAD uses in their S-Expression format. These are *different* to + the layer names KiCAD exposes in the UI (even though most of them match up!). + + For your convenience, there is an SVG template with all the right layer names and IDs located next to this README. + +.. code-block:: shell + + svg-flatten [options] --format kicad --sexp-mod-name My_Module [input_file.svg] [output_file] + +``gerbolyze template`` +~~~~~~~~~~~~~~~~~~~~~~ + +Usage: ``gerbolyze template [OPTIONS] INPUT`` + +Generate SVG template for gerbolyze paste from gerber files. + +INPUT may be a gerber file, directory of gerber files or zip file with gerber files + +Options: +******** +``-t, --top top_layer.svg`` + Top layer output file. + +``-b, --bottom bottom_layer.svg`` + Bottom layer output file. --top or --bottom may be given at once. If neither is given, autogenerate filenames. + +``--vector | --raster`` + Embed preview renders into output file as SVG vector graphics instead of rendering them to PNG bitmaps. The + resulting preview may slow down your SVG editor. + +``--raster-dpi FLOAT`` + DPI for rastering preview + +``--bbox TEXT`` + Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h] + mm output canvas with its bottom left corner at the given input gerber coördinates. + + +``gerbolyze paste`` +~~~~~~~~~~~~~~~~~~~ +(see `below `__) + +Usage: ``gerbolyze paste [OPTIONS] INPUT_GERBERS OUTPUT_GERBERS`` + +Render vector data and raster images from SVG file into gerbers. + +Options: +******** + +``-t, --top TEXT`` + Top side SVG overlay input file. At least one of this and ``--bottom`` should be given. + +``-b, --bottom TEXT`` + Bottom side SVG overlay input file. At least one of this and ``--top`` should be given. + +``--layer-top`` + Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk. + +``--layer-bottom`` + Bottom side SVG or PNG target layer. See ``--layer-top``. + +``--bbox TEXT`` + Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h] + mm output canvas with its bottom left corner at the given input gerber coördinates. This **must match the ``--bbox`` value given to + template**! + +``--subtract TEXT`` + Use user subtraction script from argument (see `below `_) + +``--no-subtract`` + Disable subtraction (see `below `_) + +``--dilate FLOAT`` + Default dilation for subtraction operations in mm (see `below `_) + +``--trace-space FLOAT`` + Passed through to svg-flatten, see `below `__. + +``--vectorizer TEXT`` + Passed through to svg-flatten, see `its description below `__. Also have a look at `the examples below `_. + +``--vectorizer-map TEXT`` + Passed through to svg-flatten, see `below `__. + +``--exclude-groups TEXT`` + Passed through to svg-flatten, see `below `__. + + +.. _subtraction_script: + +Subtraction scripts +******************* + +.. image:: pics/subtract_example.png + :width: 800px + +Subtraction scripts tell ``gerbolyze paste`` to remove an area around certain input layers to from an overlay layer. +When a input layer is given in the subtraction script, gerbolyze will dilate (extend outwards) everything on this input +layer and remove it from the target overlay layer. By default, Gerbolyze subtracts the mask layer from the silk layer to +make sure there are no silk primitives that overlap bare copper, and subtracts each input layer from its corresponding +overlay to make sure the two do not overlap. In the picture above you can see both at work: The overlay contains +halftone primitives all over the place. The subtraction script has cut out an area around all pads (mask layer) and all +existing silkscreen. You can turn off this behavior by passing ``--no-subtract`` or pass your own "script". + +The syntax of these scripts is: + +.. code-block:: + + {target layer} -= {source layer} {dilation} [; ...] + +The target layer must be ``out.{layer name}`` and the source layer ``in.{layer name}``. The layer names are gerbolyze's +internal layer names, i.e.: ``paste, silk, mask, copper, outline, drill`` + +The dilation value is optional, but can be a float with a leading ``+`` or ``-``. If given, before subtraction the +source layer's features will be extended by that many mm. If not given, the dilation defaults to the value given by +``--dilate`` if given or 0.1 mm otherwise. To disable dilation, simply pass ``+0`` here. + +Multiple commands can be separated by semicolons ``;`` or line breaks. + +The default subtraction script is: + +.. code-block:: + + out.silk -= in.mask + out.silk -= in.silk+0.5 + out.mask -= in.mask+0.5 + out.copper -= in.copper+0.5 + +``gerbolyze vectorize`` +~~~~~~~~~~~~~~~~~~~~~~~ + +``gerbolyze vectorize`` is a wrapper provided for compatibility with Gerbolyze version 1. It does nothing more than +internally call ``gerbolyze paste`` with some default arguments set. + +.. _svg_flatten: + +``svg-flatten`` +~~~~~~~~~~~~~~~ + +Usage: ``svg-flatten [OPTIONS]... [INPUT_FILE] [OUTPUT_FILE]`` + +Specify ``-`` for stdin/stdout. + +Options: +******** + +``-h, --help`` + Print help and exit + +``-v, --version`` + Print version and exit + +``-o, --format`` + Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression) + +``-p, --precision`` + Number of decimal places use for exported coordinates (gerber: 1-9, SVG: >=0). Note that not all gerber viewers are + happy with too many digits. 5 or 6 is a reasonable choice. + +``--clear-color`` + SVG color to use in SVG output for "clear" areas (default: white) + +``--dark-color`` + SVG color to use in SVG output for "dark" areas (default: black) + +``-f, --flip-gerber-polarity`` + Flip polarity of all output gerber primitives for --format gerber. + +``-d, --trace-space`` + Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm. + +``--no-header`` + Do not export output format header/footer, only export the primitives themselves + +``--flatten`` + Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level. + Potentially slow. This defaults to on when using KiCAD S-Exp export because KiCAD does not know polarity or colors. + +``--no-flatten`` + Disable automatic flattening for KiCAD S-Exp export + +``--dilate`` + Dilate output gerber primitives by this amount in mm. Used for masking out other layers. + +``-g, --only-groups`` + Comma-separated list of group IDs to export. + +``-b, --vectorizer`` + Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours, + dev-null. Have a look at `the examples below `_. + +``--vectorizer-map`` + Map from image element id to vectorizer. Overrides --vectorizer. Format: id1=vectorizer,id2=vectorizer,... + + You can use this to set a certain vectorizer for specific images, e.g. if you want to use both halftone + vectorization and contour tracing in the same SVG. Note that you can set an ```` element's SVG ID from within + Inkscape though the context menu's Object Properties tool. + +``--force-svg`` + Force SVG input irrespective of file name + +``--force-png`` + Force bitmap graphics input irrespective of file name + +``-s, --size`` + Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78 + +``--sexp-mod-name`` + Module name for KiCAD S-Exp output. This is a mandatory argument if using S-Exp output. + +``--sexp-layer`` + Layer for KiCAD S-Exp output. Defaults to auto-detect layers from SVG layer/top-level group IDs. If given, SVG + groups and layers are completely ignored and everything is simply vectorized into this layer, though you cna still + use ``-g`` for group selection. + +``-a, --preserve-aspect-ratio`` + Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG + preserveAspectRatio syntax. + +``--no-usvg`` + Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing) + +``--usvg-dpi`` + Passed through to usvg's --dpi, in case the input file has different ideas of DPI than usvg has. + +``--scale`` + Scale input svg lengths by this factor. + +``-e, --exclude-groups`` + Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups. + +.. _vectorization: + +Gerbolyze image vectorization +----------------------------- + +Gerbolyze has two built-in strategies to translate pixel images into vector images. One is its built-in halftone +processor that tries to approximate grayscale. The other is its built-in binary vectorizer that traces contours in +black-and-white images. Below are examples for the four options. + +The vectorizers can be used in isolation through ``svg-flatten`` with either an SVG input that contains an image or a +PNG/JPG input. + +The vectorizer can be controlled globally using the ``--vectorizer`` flag in both ``gerbolyze`` and ``svg-flatten``. It +can also be set on a per-image basis in both using ``--vectorizer-map [image svg id]=[option]["," ...]``. + +.. for f in vec_*.png; convert -background white -gravity center $f -resize 500x500 -extent 500x500 (basename -s .png $f)-square.png; end +.. for vec in hexgrid square poisson contours; convert vec_"$vec"_whole-square.png vec_"$vec"_detail-square.png -background transparent -splice 25x0+0+0 +append -chop 25x0+0+0 vec_"$vec"_composited.png; end + +``--vectorizer poisson-disc`` (the default) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: pics/vec_poisson_composited.png + :width: 800px + +``--vectorizer hex-grid`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: pics/vec_hexgrid_composited.png + :width: 800px + +``--vectorizer square-grid`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: pics/vec_square_composited.png + :width: 800px + +``--vectorizer binary-contours`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: pics/vec_contours_composited.png + :width: 800px + +The binary contours vectorizer requires a black-and-white binary input image. As you can see, like every bitmap tracer +it will produce some artifacts. For artistic input this is usually not too bad as long as the input data is +high-resolution. Antialiased edges in the input image are not only OK, they may even help with an accurate +vectorization. + +GIMP halftone preprocessing guide +--------------------------------- + +Gerbolyze has its own built-in halftone processor, but you can also use the high-quality "newsprint" filter built into +GIMP_ instead if you like. This section will guide you through this. The PNG you get out of this can then be fed into +gerbolyze using ``--vectorizer binary-contours``. + +1 Import your desired artwork ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Though anime or manga pictures are highly recommended, you can use any image including photographs. Be careful to select a picture with comparatively low detail that remains recognizable at very low resolution. While working on a screen this is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/02import02.png +.. image:: screenshots/02import02.png + :width: 800px -3 Paste the artwork onto the render as a new layer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/03paste.png - -4 Scale, rotate and position the artwork to the desired size -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/04scale_cut.png - -For alignment it may help to set the artwork layer's mode in the layers dialog to ``overlay``, which makes the PCB -render layer below shine through more. If you can't set the layer's mode, make sure you have actually made a new layer -from the floating selection you get when pasting one image into another in the GIMP. - -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/05position.png - -5 Convert the image to grayscale +2 Convert the image to grayscale ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/06grayscale.png +.. image:: screenshots/06grayscale.png + :width: 800px -6 Fine-tune the image's contrast +3 Fine-tune the image's contrast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during @@ -92,9 +573,10 @@ dots that might be beyond your PCB manufacturer's maximum resolution. To control of the grayscale value curve as shown (exaggerated) in the picture below. These steps saturate very bright grays to white and very dark grays to black while preserving the values in the middle. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/08curve_cut.png +.. image:: screenshots/08curve_cut.png + :width: 800px -7 Retouch details +4 Retouch details ~~~~~~~~~~~~~~~~~ Therer might be small details that don't look right yet, such as the image's background color or small highlights that @@ -105,14 +587,16 @@ If you don't want the image's background to show up on the final PCB at all, jus Particularly on low-resolution source images it may make sense to apply a blur with a radius similar to the following newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/09retouch.png +.. image:: screenshots/09retouch.png + :width: 800px In the following example, I retouched the highlights in the hair of the character in the picture to make them completely white instead of light-gray, so they still stand out nicely in the finished picture. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/10retouched.png +.. image:: screenshots/10retouched.png + :width: 800px -8 Run the newsprint filter +5 Run the newsprint filter ~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, run the GIMP's newsprint filter, under filters, distorts, newsprint. @@ -123,32 +607,124 @@ with ``gerbolyze render`` default settings for good-quality silkscreen). In gene The second important setting is oversampling, which should be set to four or slightly higher. This improves the result of the edge reconstruction of ``gerbolyze vectorize``. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/11newsprint.png +.. image:: screenshots/11newsprint.png + :width: 800px The following are examples on the detail resulting from the newsprint filter. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/12newsprint.png +.. image:: screenshots/12newsprint.png + :width: 800px -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/13newsprint.png - -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14newsprint.png - -9 Export the image for use with ``gerbolyze vectorize`` +6 Export the image for use with ``gerbolyze vectorize`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this example. -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14result_cut.png +.. image:: screenshots/14result_cut.png + :width: 800px -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/15result_cut.png +.. image:: screenshots/15result_cut.png + :width: 800px -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/16result_cut.png +Manufacturing Considerations +---------------------------- + +The main consideration when designing artwork for PCB processes is the processes' trace/space design rule. The two +things you can do here is one, to be creative with graphical parts of the design and avoid extremely narrow lines, +wedges or other thin features that will not come out well. Number two is to keep detail in raster images several times +larger than the manufacturing processes native capability. For example, to target a trace/space design rule of 100 µm, +the smallest detail in embedded raster graphics should not be much below 1mm. + +Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still produce small +artifacts that violate these rules, their output should be close enough to satifsy board houses and close enough for the +result to look good. The way gerbolyze does this is to clip the halftone cell's values to zero whenevery they get too +small, and to forcefully split or merge two neighboring cells when they get too close. While this process introduces +slight steps at the top and bottom of grayscale response, for most inputs these are not noticeable. + +On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with these design +rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design here, so all that's on the +roadmap here is to eventually include a gerber-level design rule checker. + +As far as board houses go, I have made good experiences with the popular Chinese board houses. In my experience, JLC +will just produce whatever you send them with little fucks being given about design rule adherence or validity of the +input gerbers. This is great if you just want artistic circuit boards without much of a hassle, and you don't care if +they come out exactly as you imagined. The worst I've had happen was when an older version of gerbolyze generated +polygons with holes assuming standard fill-rule processing. The in the board house's online gerber viewer things looked +fine, and neither did they complain during file review. However, the resulting boards looked completely wrong because +all the dark halftones were missing. + +PCBWay on the other hand has a much more rigurous file review process. They will complain when you throw +illegal garbage gerbers at them, and they will helpfully guide you through your design rule violations. In this way you +get much more of a professional service from them and for designs that have to be functional their higher level of +scrutiny definitely is a good thing. For the design you saw in the first picture in this article, I ended up begging +them to just plot my files if it doesn't physically break their machines and to their credit, while they seemed unhappy +about it they did it and the result looks absolutely stunning. + +PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else (large boards, +multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of manufacturing options such as +flexible circuit boards, multi-layer boards, thick or thin substrates and high-temperature substrates. + +When in doubt about how your design is going to come out on the board, do not hesitate to contact your board house. Most +of the end customer-facing online PCB services have a number of different factories that do a number of different +fabrication processes for them depending on order parameters. Places like PCBWay have exceptional quality control and +good customer service, but that is mostly focused on the technical aspects of the PCB. If you rely on visual aspects +like silkscreen uniformity or solder mask color that is a strong no concern to everyone else in the electronics +industry, you may find significant variations between manufacturers or even between orders with the same manufacturer +and you may encounter challenges communicating your requirements. + +Limitations +----------- + +SVG raster features +~~~~~~~~~~~~~~~~~~~ + +Currently, SVG masks and filters are not supported. Though SVG is marketed as a "vector graphics format", these two +features are really raster primitives that all SVG viewers perform at the pixel level after rasterization. Since +supporting these would likely not end up looking like what you want, it is not a planned feature. If you need masks or +filters, simply export the relevant parts of the SVG as a PNG then include that in your template. + +Gerber pass-through +~~~~~~~~~~~~~~~~~~~ + +Since gerbolyze has to composite your input gerbers with its own output, it has to fully parse and re-serialize them. +gerbolyze uses pcb-tools_ and pcb-tools-extension_ for all its gerber parsing needs. Both seem well-written, but likely +not free of bugs. This means that in rare cases information may get lost during this round trip. Thus, *always* check +the output files for errors before submitting them to production. + +Gerbolyze is provided without any warranty, but still please open an issue or `send me an email +`__ if you find any errors or inconsistencies. + +Trace/Space design rule adherence +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While the grayscale halftone vectorizers do a reasonable job adhering to a given trace/space design rule, they can still +produce small parts of output that violate it. For the contour vectorizer as well as for all SVG primitives, you are +responsible for adhering to design rules yourself as there is no algorithm that gerboyze could use to "fix" its input. + +A design rule checker is planned as a future addition to gerbolyze, but is not yet part of it. If in doubt, talk to your +fab and consider doing a test run of your design before ordering assembled boards ;) Gallery ------- -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample2.jpg +.. image:: pics/sample3.jpg + :width: 400px -.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample3.jpg +Licensing +--------- +This tool is licensed under the rather radical AGPLv3 license. Briefly, this means that you have to provide users of a +webapp using this tool in the backend with this tool's source. + +I get that some people have issues with the AGPL. In case this license prevents you from using this software, please +send me `an email `__ and I can grant you an exception. I want this software to be useful to as +many people as possible and I wouldn't want the license to be a hurdle to anyone. OTOH I see a danger of some cheap +board house just integrating a fork into their webpage without providing their changes back upstream, and I want to +avoid that so the default license is still AGPL. + +.. _usvg: https://github.com/RazrFalcon/resvg +.. _Inkscape: https://inkscape.org/ +.. _pcb-tools: https://github.com/curtacircuitos/pcb-tools +.. _pcb-tools-extension: https://github.com/opiopan/pcb-tools-extension +.. _GIMP: https://gimp.org/ diff --git a/TODO b/TODO new file mode 100644 index 0000000..978a30b --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +[ ] Do not just return "error 255" if usvg is not installed +[ ] Straighten up svg-flatten input unit handling +[ ] split up python code into modules +[ ] Add backwards-compatible vectorize drop-in +[ ] Figure out handling of drill layers +[ ] Re-publish my own pcb-tools, pcb-tools-extension forks with actual maintenance +[ ] For pattern rendering: validate pattern origin aligns with what the svg spec expects +[ ] Invert SVG color interpretation (use saturation maybe? or sat * val?) diff --git a/Untitled_17-11-19_18-00-15.ipynb b/Untitled_17-11-19_18-00-15.ipynb deleted file mode 100644 index 59d3b5a..0000000 --- a/Untitled_17-11-19_18-00-15.ipynb +++ /dev/null @@ -1,394 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import cv2\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "img = cv2.imread('/home/user/toys/gerbimg/foo_coarse.png')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "SCALE_FACTOR = 4\n", - "\n", - "img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n", - "w, h = img_gray.shape\n", - "img_big = cv2.resize(img_gray, (w*SCALE_FACTOR, h*SCALE_FACTOR), cv2.INTER_CUBIC)\n", - "ret, img_thr = cv2.threshold(img_big, 127, 255, 0)\n", - "img_cont_out, contours, hierarchy = cv2.findContours(img_thr, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_KCOS)\n", - "img_cont = cv2.cvtColor(img_big, cv2.COLOR_GRAY2RGB)\n", - "conts = cv2.drawContours(img_cont, contours, -1, (0, 0, 255))" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.imshow(img_cont, aspect='auto')" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cv2.imwrite('/tmp/img_cont.png', img_cont)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Plot contour length histogram\n", - "--------------\n", - "\n", - "Use log-log for easier viewing as we're observing some sort of power law here as is common with random graphs. A funny fact about this distribution is that we can actually see the spot size having an effect on this. There are discrete peaks every 25 or 30 contour points that are caused by a single contour most commonly containing a discrete number of spots and every spot being in the same order of magnitude in size having roughly the same number of contour vertices." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\nBWqbRTIovufUH9ILdeec9uljBYxmzrQ2qroa9t/fpgztt5+1qdXVdgPb7zUNqufUP2e8lSvtGKl8\ndpg921YraKESqdIMQbbNqSSnlcDWMd/3Ab6O3cHzvNhFNJ4E7kl2sNgGUKS50l1O5ptv4M9/tkSy\nZQP/+xvqOf3+e/joIzjwwOi2bbaBL7+0dThbtLCGap99rLLfDjskPk5sz6lv0CB48UUYNcq+/+or\nq1L82GOpXmE0xt12syJLW2wR3f7++9ZbPHt2dNv69dZQ7ryzxRQ//zZs/nDjHj0sQZ0xI7rkT7FY\ntsw+PEyZAnvsEXY04YpPnG677bbwggmX2maRDIqfc/rxx7beOkR7TleuhN694a234Mgj7XPA5s1w\nxhkwYoQtGXP66db23nKL3bTu0iW45HTffe3zTNeudbf/+c8Wy0MPNX6Mk06Cv/61/s1skXQE2Tan\ncp9kCjDAOdfPOdcKOA27G/sD51yPmG+PB2YjkgXp9py++SYccQR06tTwfg0lpxMnwu6710362ra1\nBufryEfD8eOtcamsTH6ORMkpWAXgl1+2r4cfhsmTG441kcWLLaFbvLju9kWL6sfkx7HVVrlZFMm/\nM+xctOe02CyKDN687jrNuZUfqG0WyaD4ntMlS6I9kfE9p0uWWCJbXW2J65572meNtWttfz/h9KfP\nBJWcVlXB8uX1t69fn/oKAKtWNRyPSLY1mpx6nrcZuBz4NzALq/w3xzl3m3PumMhuVzrnZjrnpkX2\nPTdTAYvESrfn9I03onc+G9JQcho/39QXu9bpG29ASUny5HTzZjt+/N3OeEOH2jDc779vPOZY/nnj\nz19ZaQ2W32BCNDnt3j03k9MZM6LDloo1Oa2stA86ixfDv/4VdjSSC9Q2i2RWouR0q63scatWdZNT\nsOk4VVX2HECHDtEEcdOm6PPff9/wnNN0qvXW1MCKFfW3b9qU+lqpa9aEt1KASCKpDOvF87y3gB3i\ntt0a8/gmIKQ6o1LM0uk53bTJejSffLLxfRtLThMNlfGLIh1wAIwda9WAkyWnK1fauqqlpQ3H0bq1\nDbWZOhUOOqjxuH0NJaf+v/6801xPTmfOjA5l3XlnG5LsD58uFpWVNnT84ovh+uttWHNJSdhRSdjU\nNotkTnxBpKVL7YY41C+IBPYZo7o62q63bx+9EVxVZcmi50Wn0iSTas9pba19JUtOU7mpXVVlybCS\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import collections\n", - "import math\n", - "cnt = collections.Counter(len(c) for c in contours)\n", - "cnt_sorted = sorted(cnt.items())\n", - "n = 100\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 3))\n", - "ax1.plot([k for k, _ in cnt_sorted[:n]], [math.log10(k) for _, k in cnt_sorted[:n]])\n", - "ax2.plot([math.log10(k) for k, _ in cnt_sorted], [math.log10(k) for _, k in cnt_sorted])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Count interior and exterior contours, respectively\n", - "-------------------------\n", - "\n", - "The fourth element of the hierarchy tuples is the countour's parent. As we've told ```findContours``` to use the ```CCOMP``` retrieval node, we'll get $-1$ for all white contours and some positive index for all internal, black contours." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(7989, 1219)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.sum(hierarchy[:,:,3] == -1), np.sum(hierarchy[:,:,3] != -1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Build a dict to resolve parents to their children\n", - "-----\n", - "\n", - "Structure: \n", - "```python\n", - "{\n", - " int(parent_index):\n", - " (parent_contour_ndarray, [child_contour_ndarrays...])\n", - "}\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "parents_and_children = {}\n", - "\n", - "for i, (_1, _2, _3, parent) in enumerate(hierarchy[0]):\n", - " if parent == -1:\n", - " parents_and_children[i] = (contours[i], [])\n", - "\n", - "for i, (_1, _2, _3, parent) in enumerate(hierarchy[0]):\n", - " if parent != -1:\n", - " parent_cont, child_set = parents_and_children[parent]\n", - " child_set.append(contours[i])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "How many children (holes) does a contour usually have?\n", - "---" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import collections\n", - "import math\n", - "chld = collections.Counter(len(children) for parent, children in parents_and_children.values())\n", - "chld_sorted = sorted(chld.items())[:20]\n", - "plt.plot([k for k, _ in chld_sorted[:n]], [math.log10(k) for _, k in chld_sorted[:n]])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def write_csv(scale, filename):\n", - " with open(filename, 'w') as f:\n", - " print('\"Index\",\"X (mm)\",\"Y (mm)\",\"Arc Angle (Neg = CW)\"', file=f)\n", - " for parent, _children in parents_and_children.values():\n", - " for x, y in parent[:,0]:\n", - " print('\"{}\",\"{}\",\"{}\",\"\"'.format(i, scale*float(x), scale*float(y)), file=f)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def svg_format_curve(scale, curve):\n", - " mid = ('L{} {} '.format(x, y) for x, y in curve)\n", - " return ' '.join(['M{} {}'.format(*curve[0]), *mid, 'Z'])\n", - "\n", - "def write_svg(scale, filename):\n", - " with open(filename, 'w') as f:\n", - " f.write('')\n", - " for parent, children in parents_and_children.values():\n", - " f.write(''.format(' '.join(\n", - " svg_format_curve(scale, c[:,0]) for c in [parent, *children]\n", - " )))\n", - " f.write('')" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "write_svg(1, '/tmp/test.svg')" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[[3556, 7996]],\n", - "\n", - " [[3555, 7996]],\n", - "\n", - " [[3554, 7998]],\n", - "\n", - " [[3559, 7998]],\n", - "\n", - " [[3558, 7995]]], dtype=int32)" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "contours[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[((3556, 7996), (3555, 7996)), ((3555, 7996), (3554, 7998)), ((3554, 7998), (3559, 7998)), ((3559, 7998), (3558, 7995)), ((3558, 7995), (3556, 7996))]\n", - "[((3540, 7966), (3537, 7967)), ((3537, 7967), (3535, 7968)), ((3535, 7968), (3533, 7973)), ((3533, 7973), (3532, 7979)), ((3532, 7979), (3532, 7985)), ((3532, 7985), (3533, 7992)), ((3533, 7992), (3534, 7994)), ((3534, 7994), (3536, 7996)), ((3536, 7996), (3536, 7998)), ((3536, 7998), (3543, 7998)), ((3543, 7998), (3544, 7996)), ((3544, 7996), (3546, 7992)), ((3546, 7992), (3547, 7989)), ((3547, 7989), (3547, 7985)), ((3547, 7985), (3549, 7981)), ((3549, 7981), (3549, 7975)), ((3549, 7975), (3548, 7972)), ((3548, 7972), (3547, 7969)), ((3547, 7969), (3546, 7967)), ((3546, 7967), (3544, 7967)), ((3544, 7967), (3543, 7966)), ((3543, 7966), (3540, 7966))]\n", - "[((3459, 7966), (3458, 7971)), ((3458, 7971), (3460, 7971)), ((3460, 7971), (3462, 7969)), ((3462, 7969), (3459, 7966))]\n", - "[((3493, 7963), (3490, 7968)), ((3490, 7968), (3489, 7972)), ((3489, 7972), (3489, 7985)), ((3489, 7985), (3490, 7988)), ((3490, 7988), (3491, 7990)), ((3491, 7990), (3493, 7993)), ((3493, 7993), (3498, 7993)), ((3498, 7993), (3500, 7991)), ((3500, 7991), (3502, 7988)), ((3502, 7988), (3503, 7986)), ((3503, 7986), (3503, 7970)), ((3503, 7970), (3502, 7968)), ((3502, 7968), (3500, 7965)), ((3500, 7965), (3497, 7962)), ((3497, 7962), (3493, 7963))]\n", - "[((3474, 7928), (3471, 7931)), ((3471, 7931), (3470, 7933)), ((3470, 7933), (3469, 7936)), ((3469, 7936), (3469, 7944)), ((3469, 7944), (3470, 7948)), ((3470, 7948), (3476, 7953)), ((3476, 7953), (3479, 7950)), ((3479, 7950), (3480, 7948)), ((3480, 7948), (3481, 7943)), ((3481, 7943), (3481, 7936)), ((3481, 7936), (3480, 7932)), ((3480, 7932), (3479, 7930)), ((3479, 7930), (3477, 7928)), ((3477, 7928), (3474, 7928))]\n", - "[((3436, 7928), (3435, 7932)), ((3435, 7932), (3437, 7934)), ((3437, 7934), (3438, 7938)), ((3438, 7938), (3439, 7931)), ((3439, 7931), (3436, 7928))]\n", - "[((3514, 7923), (3512, 7925)), ((3512, 7925), (3511, 7927)), ((3511, 7927), (3510, 7929)), ((3510, 7929), (3509, 7934)), ((3509, 7934), (3509, 7943)), ((3509, 7943), (3510, 7948)), ((3510, 7948), (3511, 7950)), ((3511, 7950), (3513, 7952)), ((3513, 7952), (3513, 7953)), ((3513, 7953), (3516, 7954)), ((3516, 7954), (3518, 7953)), ((3518, 7953), (3521, 7950)), ((3521, 7950), (3523, 7947)), ((3523, 7947), (3524, 7940)), ((3524, 7940), (3524, 7931)), ((3524, 7931), (3523, 7928)), ((3523, 7928), (3522, 7926)), ((3522, 7926), (3521, 7924)), ((3521, 7924), (3514, 7923))]\n", - "[((3563, 7922), (3557, 7924)), ((3557, 7924), (3556, 7927)), ((3556, 7927), (3554, 7929)), ((3554, 7929), (3551, 7935)), ((3551, 7935), (3551, 7956)), ((3551, 7956), (3553, 7960)), ((3553, 7960), (3555, 7961)), ((3555, 7961), (3560, 7961)), ((3560, 7961), (3561, 7960)), ((3561, 7960), (3563, 7960)), ((3563, 7960), (3564, 7957)), ((3564, 7957), (3567, 7954)), ((3567, 7954), (3570, 7947)), ((3570, 7947), (3570, 7931)), ((3570, 7931), (3567, 7926)), ((3567, 7926), (3566, 7923)), ((3566, 7923), (3563, 7922))]\n", - "[((3493, 7890), (3492, 7892)), ((3492, 7892), (3491, 7895)), ((3491, 7895), (3491, 7904)), ((3491, 7904), (3492, 7907)), ((3492, 7907), (3493, 7909)), ((3493, 7909), (3494, 7910)), ((3494, 7910), (3496, 7911)), ((3496, 7911), (3500, 7910)), ((3500, 7910), (3501, 7908)), ((3501, 7908), (3503, 7903)), ((3503, 7903), (3502, 7896)), ((3502, 7896), (3500, 7892)), ((3500, 7892), (3498, 7890)), ((3498, 7890), (3493, 7890))]\n", - "[((3536, 7889), (3534, 7890)), ((3534, 7890), (3532, 7892)), ((3532, 7892), (3531, 7894)), ((3531, 7894), (3530, 7897)), ((3530, 7897), (3530, 7905)), ((3530, 7905), (3532, 7910)), ((3532, 7910), (3534, 7911)), ((3534, 7911), (3538, 7911)), ((3538, 7911), (3540, 7909)), ((3540, 7909), (3543, 7903)), ((3543, 7903), (3543, 7897)), ((3543, 7897), (3542, 7894)), ((3542, 7894), (3540, 7890)), ((3540, 7890), (3538, 7889)), ((3538, 7889), (3536, 7889))]\n" - ] - } - ], - "source": [ - "for contour, _children in list(parents_and_children.values())[:10]:\n", - " print([((a,b), (c,d)) for (a,b),(c,d) in zip(contour[:,0], np.vstack((contour[1:,0], contour[:1,0])))])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.4" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/gerbolyze.py b/gerbolyze.py deleted file mode 100755 index bf26a33..0000000 --- a/gerbolyze.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 - -import tempfile -import os.path as path -import os -import sys -import time -import shutil -import math - -import gerber -from gerber.render.cairo_backend import GerberCairoContext -import numpy as np -import cv2 -import enum -import tqdm - -def generate_mask( - outline, - target, - scale, - bounds, - debugimg, - status_print, - extend_overlay_r_mil, - subtract_gerber - ): - # Render all gerber layers whose features are to be excluded from the target image, such as board outline, the - # original silk layer and the solder paste layer to binary images. - with tempfile.TemporaryDirectory() as tmpdir: - img_file = path.join(tmpdir, 'target.png') - - status_print('Combining keepout composite') - fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0)) - ctx = GerberCairoContext(scale=scale) - status_print(' * outline') - ctx.render_layer(outline, settings=fg, bgsettings=bg, bounds=bounds) - status_print(' * target layer') - ctx.render_layer(target, settings=fg, bgsettings=bg, bounds=bounds) - for fn, sub in subtract_gerber: - status_print(' * extra layer', os.path.basename(fn)) - layer = gerber.loads(sub) - ctx.render_layer(layer, settings=fg, bgsettings=bg, bounds=bounds) - status_print('Rendering keepout composite') - ctx.dump(img_file) - - # Vertically flip exported image - original_img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :] - - f = 1 if outline.units == 'inch' else 25.4 - r = 1+2*max(1, int(extend_overlay_r_mil/1000 * f * scale)) - status_print('Expanding keepout composite by', r) - - # Extend image by a few pixels and flood-fill from (0, 0) to mask out the area outside the outermost outline - # This ensures no polygons are generated outside the board even for non-rectangular boards. - border = 10 - outh, outw = original_img.shape - extended_img = np.zeros((outh + 2*border, outw + 2*border), dtype=np.uint8) - extended_img[border:outh+border, border:outw+border] = original_img - debugimg(extended_img, 'outline') - cv2.floodFill(extended_img, None, (0, 0), (255,)) - original_img = extended_img[border:outh+border, border:outw+border] - debugimg(extended_img, 'flooded') - - # Dilate the white areas of the image using gaussian blur and threshold. Use these instead of primitive dilation - # here for their non-directionality. - target_img = cv2.blur(original_img, (r, r)) - _, target_img = cv2.threshold(target_img, 255//(1+r), 255, cv2.THRESH_BINARY) - return target_img - -def render_gerbers_to_image(*gerbers, scale, bounds=None): - with tempfile.TemporaryDirectory() as tmpdir: - img_file = path.join(tmpdir, 'target.png') - fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0)) - ctx = GerberCairoContext(scale=scale) - - for grb in gerbers: - ctx.render_layer(grb, settings=fg, bgsettings=bg, bounds=bounds) - - ctx.dump(img_file) - # Vertically flip exported image to align coordinate systems - return cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :] - -def pcb_area_mask(outline, scale, bounds): - # Merge layers to target mask - img = render_gerbers_to_image(outline, scale=scale, bounds=bounds) - # Extend - imgh, imgw = img.shape - img_ext = np.zeros(shape=(imgh+2, imgw+2), dtype=np.uint8) - img_ext[1:-1, 1:-1] = img - # Binarize - img_ext[img_ext < 128] = 0 - img_ext[img_ext >= 128] = 255 - # Flood-fill - cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white from top left corner (0,0) - img_ext_snap = img_ext.copy() - cv2.floodFill(img_ext, None, (0, 0), (0,)) # Flood-fill with black - cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white - return np.logical_xor(img_ext_snap, img_ext)[1:-1, 1:-1].astype(float) - -def generate_template( - silk, mask, copper, outline, drill, - image, - process_resolution:float=6, # mil - resolution_oversampling:float=10, # times - status_print=lambda *args:None - ): - - silk, mask, copper, outline, *drill = map(gerber.load_layer_data, [silk, mask, copper, outline, *drill]) - silk.layer_class = 'topsilk' - mask.layer_class = 'topmask' - copper.layer_class = 'top' - outline.layer_class = 'outline' - - - f = 1.0 if outline.cam_source.units == 'metric' else 25.4 - scale = (1000/process_resolution) / 25.4 * resolution_oversampling * f # dpmm - bounds = outline.cam_source.bounding_box - - # Create a new drawing context - ctx = GerberCairoContext(scale=scale) - - ctx.render_layer(outline, bounds=bounds) - ctx.render_layer(copper, bounds=bounds) - ctx.render_layer(mask, bounds=bounds) - ctx.render_layer(silk, bounds=bounds) - for dr in drill: - ctx.render_layer(dr, bounds=bounds) - ctx.dump(image) - -def paste_image( - target_gerber:str, - outline_gerber:str, - source_img:np.ndarray, - subtract_gerber:list=[], - extend_overlay_r_mil:float=6, - extend_picture_r_mil:float=2, - status_print=lambda *args:None, - debugdir:str=None): - - debugctr = 0 - def debugimg(img, name): - nonlocal debugctr - if debugdir: - cv2.imwrite(path.join(debugdir, '{:02d}{}.png'.format(debugctr, name)), img) - debugctr += 1 - - # Parse outline layer to get bounds of gerber file - status_print('Parsing outline gerber') - outline = gerber.loads(outline_gerber) - bounds = (minx, maxx), (miny, maxy) = outline.bounding_box - grbw, grbh = maxx - minx, maxy - miny - status_print(' * outline has offset {}, size {}'.format((minx, miny), (grbw, grbh))) - - # Parse target layer - status_print('Parsing target gerber') - target = gerber.loads(target_gerber) - (tminx, tmaxx), (tminy, tmaxy) = target.bounding_box - status_print(' * target layer has offset {}, size {}'.format((tminx, tminy), (tmaxx-tminx, tmaxy-tminy))) - - # Read source image - imgh, imgw = source_img.shape - scale = math.ceil(max(imgw/grbw, imgh/grbh)) # scale is in dpmm - status_print(' * source image has size {}, going for scale {}dpmm'.format((imgw, imgh), scale)) - - # Merge layers to target mask - target_img = generate_mask(outline, target, scale, bounds, debugimg, status_print, extend_overlay_r_mil, subtract_gerber) - - # Threshold source image. Ideally, the source image is already binary but in case it's not, or in case it's not - # exactly binary (having a few very dark or very light grays e.g. due to JPEG compression) we're thresholding here. - status_print('Thresholding source image') - qr = 1+2*max(1, int(extend_picture_r_mil/1000 * scale)) - source_img = source_img[::-1] - _, source_img = cv2.threshold(source_img, 127, 255, cv2.THRESH_BINARY) - debugimg(source_img, 'thresh') - - # Pad image to size of target layer images generated above. After this, `scale` applies to the padded image as well - # as the gerber renders. For padding, zoom or shrink the image to completely fit the gerber's rectangular bounding - # box. Center the image vertically or horizontally if it has a different aspect ratio. - status_print('Padding source image') - tgth, tgtw = target_img.shape - padded_img = np.zeros(shape=target_img.shape, dtype=source_img.dtype) - offx = int((minx-tminx if tminx < minx else 0)*scale) - offy = int((miny-tminy if tminy < miny else 0)*scale) - offx += int(grbw*scale - imgw) // 2 - offy += int(grbh*scale - imgh) // 2 - endx, endy = min(offx+imgw, tgtw), min(offy+imgh, tgth) - print('off', (offx, offy), 'end', (endx, endy), 'img', (imgw, imgh), 'tgt', (tgtw, tgth)) - padded_img[offy:endy, offx:endx] = source_img[:endy-offy, :endx-offx] - debugimg(padded_img, 'padded') - debugimg(target_img, 'target') - - # Mask out excluded gerber features (source silk, holes, solder mask etc.) from the target image - status_print('Masking source image') - out_img = (np.multiply((padded_img/255.0), (target_img/255.0) * -1 + 1) * 255).astype(np.uint8) - - debugimg(out_img, 'multiplied') - - # Calculate contours from masked target image and plot them to the target gerber context - status_print('Calculating contour lines') - plot_contours(out_img, - target, - offx=(minx, miny), - scale=scale, - status_print=lambda *args: status_print(' ', *args)) - - # Write target gerber context to disk - status_print('Generating output gerber') - from gerber.render import rs274x_backend - ctx = rs274x_backend.Rs274xContext(target.settings) - target.render(ctx) - out = ctx.dump().getvalue() - status_print('Done.') - return out - - -def plot_contours( - img:np.ndarray, - layer:gerber.rs274x.GerberFile, - offx:tuple, - scale:float, - debug=lambda *args:None, - status_print=lambda *args:None): - from gerber.primitives import Line, Region, Circle - imgh, imgw = img.shape - - # Extract contour hierarchy using OpenCV - status_print('Extracting contours') - # See https://stackoverflow.com/questions/48291581/how-to-use-cv2-findcontours-in-different-opencv-versions/48292371 - contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_KCOS)[-2:] - - aperture = list(layer.apertures)[0] if layer.apertures else Circle(None, 0.10) - - status_print('offx', offx, 'scale', scale) - - xbias, ybias = offx - def map(coord): - x, y = coord - return (x/scale + xbias, y/scale + ybias) - def contour_lines(c): - return [ Line(map(start), map(end), aperture, units=layer.settings.units) - for start, end in zip(c, np.vstack((c[1:], c[:1]))) ] - - done = [] - process_stack = [-1] - next_process_stack = [] - parents = [ (i, first_child != -1, parent) for i, (_1, _2, first_child, parent) in enumerate(hierarchy[0]) ] - is_dark = True - status_print('Converting contours to gerber primitives') - with tqdm.tqdm(total=len(contours)) as progress: - while len(done) != len(contours): - for i, has_children, parent in parents[:]: - if parent in process_stack: - contour = contours[i] - polarity = 'dark' if is_dark else 'clear' - debug('rendering {} with parent {} as {} with {} vertices'.format(i, parent, polarity, len(contour))) - debug('process_stack is', process_stack) - debug() - layer.primitives.append(Region(contour_lines(contour[:,0]), level_polarity=polarity, units=layer.settings.units)) - if has_children: - next_process_stack.append(i) - done.append(i) - parents.remove((i, has_children, parent)) - progress.update(1) - debug('skipping to next level') - process_stack, next_process_stack = next_process_stack, [] - is_dark = not is_dark - debug('done', done) - -# Utility foo -# =========== - -def find_gerber_in_dir(dir_path, extensions, exclude=''): - contents = os.listdir(dir_path) - exts = extensions.split('|') - excs = exclude.split('|') - for entry in contents: - if any(entry.lower().endswith(ext.lower()) for ext in exts) and not any(entry.lower().endswith(ex) for ex in excs if exclude): - lname = path.join(dir_path, entry) - if not path.isfile(lname): - continue - with open(lname, 'r') as f: - return lname, f.read() - - raise ValueError(f'Cannot find file with suffix {extensions} in dir {dir_path}') - -# Gerber file name extensions for Altium/Protel | KiCAD | Eagle -LAYER_SPEC = { - 'top': { - 'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc', - 'silk': '.gto|-F_SilkS.gbr|-F.SilkS.gbr|.plc|-F_Silkscreen.gbr', - 'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc', - 'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp', - 'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb', - }, - 'bottom': { - 'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms', - 'silk': '.gbo|-B_SilkS.gbr|-B.SilkS.gbr|.pls|-B_Silkscreen.gbr', - 'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts', - 'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol', - 'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb' - }, - } - -# Command line interface -# ====================== - -def process_gerbers(source, target, image, side, layer, exact, debugdir): - if not os.path.isdir(source): - raise ValueError(f'Given source "{source}" is not a directory.') - - # Load input files - source_img = cv2.imread(image, cv2.IMREAD_GRAYSCALE) - if source_img is None: - print(f'"{image}" is not a valid image file', file=sys.stderr) - sys.exit(1) - - tlayer, slayer = { - 'silk': ('silk', 'mask'), - 'mask': ('mask', 'silk'), - 'copper': ('copper', None) - }[layer] - - layers = LAYER_SPEC[side] - tname, tgrb = find_gerber_in_dir(source, layers[tlayer]) - print('Target layer file {}'.format(os.path.basename(tname))) - oname, ogrb = find_gerber_in_dir(source, layers['outline']) - print('Outline layer file {}'.format(os.path.basename(oname))) - subtract = [find_gerber_in_dir(source, layers[slayer])] if slayer and not exact else [] - - # Prepare output. Do this now to error out as early as possible if there's a problem. - if os.path.exists(target): - if os.path.isdir(target) and sorted(os.listdir(target)) == sorted(os.listdir(source)): - shutil.rmtree(target) - else: - print('Error: Target already exists and does not look like source. Please manually remove the target dir before proceeding.', file=sys.stderr) - sys.exit(1) - - # Generate output - out = paste_image(tgrb, ogrb, source_img, subtract, debugdir=debugdir, status_print=lambda *args: print(*args, flush=True)) - - shutil.copytree(source, target) - with open(os.path.join(target, os.path.basename(tname)), 'w') as f: - f.write(out) - -def render_preview(source, image, side, process_resolution, resolution_oversampling): - def load_layer(layer): - name, grb = find_gerber_in_dir(source, LAYER_SPEC[side][layer]) - print(f'{layer} layer file {os.path.basename(name)}') - return grb - - outline = load_layer('outline') - silk = load_layer('silk') - mask = load_layer('mask') - copper = load_layer('copper') - - try: - nm, npth = find_gerber_in_dir(source, '-npth.drl') - print(f'npth drill file {nm}') - except ValueError: - npth = None - nm, drill = find_gerber_in_dir(source, '.drl|.txt', exclude='-npth.drl') - print(f'drill file {nm}') - drill = ([npth] if npth else []) + [drill] - - generate_template( - silk, mask, copper, outline, drill, - image, - process_resolution=process_resolution, - resolution_oversampling=resolution_oversampling, - ) - diff --git a/gerbolyze b/gerbolyze/command_line.py similarity index 91% rename from gerbolyze rename to gerbolyze/command_line.py index fad68b4..e6c6d67 100755 --- a/gerbolyze +++ b/gerbolyze/command_line.py @@ -16,7 +16,6 @@ if __name__ == '__main__': vectorize_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side') vectorize_parser.add_argument('--layer', '-l', choices=['silk', 'mask', 'copper'], default='silk', help='Target layer on given side') - vectorize_parser.add_argument('--exact', '-x', action='store_true', default=False, help='Do not subtract existing features on other layers from overlay') vectorize_parser.add_argument('source', help='Source gerber directory') vectorize_parser.add_argument('target', help='Target gerber directory') @@ -30,7 +29,7 @@ if __name__ == '__main__': args = parser.parse_args() if args.command == 'vectorize': - gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.exact, args.debugdir) + gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.debugdir) else: # command == render gerbolyze.render_preview(args.source, args.image, args.side, args.fab_resolution, args.oversampling) diff --git a/gerbolyze/gerbolyze.py b/gerbolyze/gerbolyze.py new file mode 100755 index 0000000..369212c --- /dev/null +++ b/gerbolyze/gerbolyze.py @@ -0,0 +1,671 @@ +import tempfile +import os.path as path +from pathlib import Path +import textwrap +import subprocess +import functools +import os +import base64 +import re +import sys +import warnings +import shutil +from zipfile import ZipFile, is_zipfile + +from lxml import etree +import gerber +from gerber.render.cairo_backend import GerberCairoContext +import gerberex +import gerberex.rs274x +import numpy as np +import click +from slugify import slugify + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('-l', '--layer', type=click.Choice(['silk', 'mask', 'copper']), default='silk') +@click.option('-x', '--exact', flag_value=True) +@click.option('--trace-space', type=float, default=0.1, help='passed through to svg-flatten') +@click.argument('side', type=click.Choice(['top', 'bottom'])) +@click.argument('source') +@click.argument('target') +@click.argument('image') +@click.pass_context +def vectorize(ctx, side, layer, exact, source, target, image, trace_space): + """ Compatibility command for Gerbolyze version 1. + + This command is deprecated, please update your code to call "gerbolyze paste" instead. + """ + ctx.invoke(paste, + input_gerbers=source, + output_gerbers=target, + **{side: image, f'layer_{side}': layer}, + no_subtract=exact, + trace_space=trace_space, + vectorizer='binary-contours', + preserve_aspect_ratio='meet') + +@cli.command() +@click.argument('input_gerbers') +@click.argument('output_gerbers') +@click.option('-t', '--top', help='Top side SVG or PNG overlay') +@click.option('-b', '--bottom', help='Bottom side SVG or PNG overlay') +@click.option('--layer-top', help='Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk.') +@click.option('--layer-bottom', help='Bottom side SVG or PNG target layer. See --layer-top.') +@click.option('--bbox', help='Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR ' + '"x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber ' + 'coördinates. MUST MATCH --bbox GIVEN TO PREVIEW') +@click.option('--dilate', default=0.1, type=float, help='Default dilation for subtraction operations in mm') +@click.option('--curve-tolerance', type=float, help='Tolerance for curve flattening in mm') +@click.option('--no-subtract', 'no_subtract', flag_value=True, help='Disable subtraction') +@click.option('--subtract', help='Use user subtraction script from argument (see description above)') +@click.option('--trace-space', type=float, default=0.1, help='passed through to svg-flatten') +@click.option('--vectorizer', help='passed through to svg-flatten') +@click.option('--vectorizer-map', help='passed through to svg-flatten') +@click.option('--preserve-aspect-ratio', help='PNG/JPG files only: passed through to svg-flatten') +@click.option('--exclude-groups', help='passed through to svg-flatten') +def paste(input_gerbers, output_gerbers, + top, bottom, layer_top, layer_bottom, + bbox, + dilate, curve_tolerance, no_subtract, subtract, + preserve_aspect_ratio, + trace_space, vectorizer, vectorizer_map, exclude_groups): + """ Render vector data and raster images from SVG file into gerbers. """ + + if no_subtract: + subtract_map = {} + else: + subtract_map = parse_subtract_script(subtract, dilate) + + if not top and not bottom: + raise click.UsageError('Either --top or --bottom must be given') + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + output_gerbers = Path(output_gerbers) + input_gerbers = Path(input_gerbers) + source = unpack_if_necessary(input_gerbers, tmpdir) + matches = match_gerbers_in_dir(source) + + if input_gerbers.is_dir(): + # Create output dir if it does not exist yet + output_gerbers.mkdir(exist_ok=True) + + # In case output dir already existed, remove files we will overwrite + for in_file in source.iterdir(): + out_cand = output_gerbers / in_file.name + out_cand.unlink(missing_ok=True) + + for side, in_svg_or_png, target_layer in [ + ('top', top, layer_top), + ('bottom', bottom, layer_bottom)]: + + if not in_svg_or_png: + continue + + if Path(in_svg_or_png).suffix.lower() in ['.png', '.jpg'] and target_layer is None: + target_layer = 'silk' + + print() + print('#########################################') + print('processing side', side, 'infile', in_svg_or_png) + print('#########################################') + print() + + if not matches[side]: + warnings.warn(f'No input gerber files found for {side} side') + continue + + try: + units, layers = load_side(matches[side]) + except SystemError as e: + raise click.UsageError(e.args) + + print('loaded layers:', list(layers.keys())) + + bounds = get_bounds(bbox, layers) + print('bounds:', bounds) + + @functools.lru_cache() + def do_dilate(layer, amount): + print('dilating', layer, 'by', amount) + outfile = tmpdir / f'dilated-{layer}-{amount}.gbr' + dilate_gerber(layers, layer, amount, bbox, tmpdir, outfile, units, curve_tolerance) + gbr = gerberex.read(str(outfile)) + gbr.offset(bounds[0][0], bounds[1][0]) + return gbr + + for layer, input_files in layers.items(): + if layer == 'drill': + continue + + if target_layer is not None: + if layer != target_layer: + continue + + (in_grb_path, in_grb), = input_files + + print() + print('-----------------------------------------') + print('processing side', side, 'layer', layer) + print('-----------------------------------------') + print() + print('rendering layer', layer) + overlay_file = tmpdir / f'overlay-{side}-{layer}.gbr' + layer_arg = layer if target_layer is None else None # slightly confusing but trust me :) + svg_to_gerber(in_svg_or_png, overlay_file, layer_arg, + trace_space, vectorizer, vectorizer_map, exclude_groups, curve_tolerance, + bounds_for_png=bounds, preserve_aspect_ratio=preserve_aspect_ratio) + + overlay_grb = gerberex.read(str(overlay_file)) + if not overlay_grb.primitives: + print(f'Overlay layer {layer} does not contain anything. Skipping.', file=sys.stderr) + continue + + print('compositing') + comp = gerberex.GerberComposition() + # overlay on bottom + overlay_grb.offset(bounds[0][0], bounds[1][0]) + comp.merge(overlay_grb) + # dilated subtract layers on top of overlay + dilations = subtract_map.get(layer, []) + for d_layer, amount in dilations: + print('processing dilation', d_layer, amount) + dilated = do_dilate(d_layer, amount) + comp.merge(dilated) + # input on top of everything + comp.merge(gerberex.rs274x.GerberFile.from_gerber_file(in_grb.cam_source)) + + if input_gerbers.is_dir(): + this_out = output_gerbers / in_grb_path.name + else: + this_out = output_gerbers + print('dumping to', this_out) + comp.dump(this_out) + + if input_gerbers.is_dir(): + for in_file in source.iterdir(): + out_cand = output_gerbers / in_file.name + if not out_cand.is_file(): + print(f'Input file {in_file.name} remained unprocessed. Copying.', file=sys.stderr) + shutil.copy(in_file, out_cand) + +@cli.command() +@click.argument('input') +@click.option('-t' ,'--top', help='Top layer output file.') +@click.option('-b' ,'--bottom', help='Bottom layer output file. --top or --bottom may be given at once. If neither is given, autogenerate filenames.') +@click.option('--vector/--raster', help='Embed preview renders into output file as SVG vector graphics instead of rendering them to PNG bitmaps. The resulting preview may slow down your SVG editor.') +@click.option('--raster-dpi', type=float, default=300.0, help='DPI for rastering preview') +@click.option('--bbox', help='Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR ' + '"x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber ' + 'coördinates.') +def template(input, top, bottom, bbox, vector, raster_dpi): + ''' Generate SVG template for gerbolyze paste from gerber files. + + INPUT may be a gerber file, directory of gerber files or zip file with gerber files + ''' + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + source = Path(input) + + if not top and not bottom: # autogenerate two file names if neither --top nor --bottom are given + # /path/to/gerber/dir -> /path/to/gerber/dir.preview-{top|bottom}.svg + # /path/to/gerbers.zip -> /path/to/gerbers.zip.preview-{top|bottom}.svg + # /path/to/single/file.grb -> /path/to/single/file.grb.preview-{top|bottom}.svg + outfiles = { + 'top': source.parent / f'{source.name}.preview-top.svg', + 'bottom': source.parent / f'{source.name}.preview-top.svg' } + else: + outfiles = { + 'top': Path(top) if top else None, + 'bottom': Path(bottom) if bottom else None } + + source = unpack_if_necessary(source, tmpdir) + matches = match_gerbers_in_dir(source) + + for side in ('top', 'bottom'): + if not outfiles[side]: + continue + + if not matches[side]: + warnings.warn(f'No input gerber files found for {side} side') + continue + + try: + units, layers = load_side(matches[side]) + except SystemError as e: + raise click.UsageError(e.args) + + # cairo-svg uses a hardcoded dpi value of 72. pcb-tools does something weird, so we have to scale things + # here. + scale = 1/25.4 if units == 'metric' else 1.0 # pcb-tools gerber scale + + scale *= CAIRO_SVG_HARDCODED_DPI + if not vector: # adapt scale for png export + scale *= raster_dpi / CAIRO_SVG_HARDCODED_DPI + + bounds = get_bounds(bbox, layers) + ctx = GerberCairoContext(scale=scale) + for layer_name in LAYER_RENDER_ORDER: + for _path, to_render in layers.get(layer_name, ()): + ctx.render_layer(to_render, bounds=bounds) + + filetype = 'svg' if vector else 'png' + tmp_render = tmpdir / f'intermediate-{side}.{filetype}' + ctx.dump(str(tmp_render)) + + if vector: + with open(tmp_render, 'rb') as f: + svg_data = f.read() + + with open(outfiles[side], 'wb') as f: + f.write(create_template_from_svg(bounds, svg_data)) + + else: # raster + with open(tmp_render, 'rb') as f: + png_data = f.read() + + with open(outfiles[side], 'w') as f: + f.write(template_svg_for_png(bounds, png_data)) + +# Subtraction script handling +#============================ + +DEFAULT_SUB_SCRIPT = ''' +out.silk -= in.mask +out.silk -= in.silk+0.5 +out.mask -= in.mask+0.5 +out.copper -= in.copper+0.5 +''' + +def parse_subtract_script(script, default_dilation=0.1): + if script is None: + script = DEFAULT_SUB_SCRIPT + + subtract_script = {} + lines = script.replace(';', '\n').splitlines() + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + + line = line.lower() + line = re.sub('\s', '', line) + + # out.copper -= in.copper+0.1 + varname = r'([a-z]+\.[a-z]+)' + floatnum = r'([+-][.0-9]+)' + match = re.fullmatch(fr'{varname}-={varname}{floatnum}?', line) + if not match: + raise ValueError(f'Cannot parse line: {line}') + + out_var, in_var, dilation = match.groups() + if not out_var.startswith('out.') or not in_var.startswith('in.'): + raise ValueError('All left-hand side values must be outputs, right-hand side values must be inputs.') + + _out, _, out_layer = out_var.partition('.') + _in, _, in_layer = in_var.partition('.') + + dilation = float(dilation) if dilation else default_dilation + + subtract_script[out_layer] = subtract_script.get(out_layer, []) + [(in_layer, dilation)] + return subtract_script + +# Parameter parsing foo +#====================== + +def parse_bbox(bbox): + if not bbox: + return None + elems = [ int(elem) for elem in re.split('[,/ ]', bbox) ] + if len(elems) not in (2, 4): + raise click.BadParameter( + '--bbox must be either two floating-point values like: w,h or four like: x,y,w,h') + + elems = [ float(e) for e in elems ] + + if len(elems) == 2: + bounds = [0, 0, *elems] + else: + bounds = elems + + # now transform bounds to the format pcb-tools uses. Instead of (x, y, w, h) or even (x1, y1, x2, y2), that + # is ((x1, x2), (y1, y2) + + x, y, w, h = bounds + return ((x, x+w), (y, y+h)) + +def bounds_from_outline(layers): + ''' NOTE: When the user has not set explicit bounds, we automatically extract the design's bounding box from the + input gerber files. If a folder is used as input, we use the outline gerber and barf if we can't find one. If only a + single file is given, we simply use that file's bounding box + + We have to do things this way since gerber files do not have explicit bounds listed. + + Note that the bounding box extracted from the outline layer usually will be one outline layer stroke widht larger in + all directions than the finished board. + ''' + if 'outline' in layers: + outline_files = layers['outline'] + _path, grb = outline_files[0] + return calculate_apertureless_bounding_box(grb.cam_source) + + elif len(layers) == 1: + first_layer, *rest = layers.values() + first_file, *rest = first_layer + _path, grb = first_file + return grb.cam_source.bounding_box + + else: + raise click.UsageError('Cannot find an outline file and no --bbox given.') + +def get_bounds(bbox, layers): + bounds = parse_bbox(bbox) + if bounds: + return bounds + return bounds_from_outline(layers) + +# Utility foo +# =========== + +# Gerber file name extensions for Altium/Protel | KiCAD | Eagle +# Note that in case of KiCAD these extensions occassionally change without notice. If you discover that this list is not +# up to date, please know that it's not my fault and submit an issue or send me an email. +LAYER_SPEC = { + 'top': { + 'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc', + 'silk': '.gto|-F_Silkscreen.gbr|-F_SilkS.gbr|-F.SilkS.gbr|.plc', + 'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc', + 'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp', + 'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb', + 'drill': '.drl|.txt|-npth.drl', + }, + 'bottom': { + 'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms', + 'silk': '.gbo|-B_Silkscreen.gbr|-B_SilkS.gbr|-B.SilkS.gbr|.pls', + 'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts', + 'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol', + 'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb', + 'drill': '.drl|.txt|-npth.drl', + }, + } + +# Maps keys from LAYER_SPEC to pcb-tools layer classes (see pcb-tools'es gerber/layers.py) +LAYER_CLASSES = { + 'silk': 'topsilk', + 'mask': 'topmask', + 'paste': 'toppaste', + 'copper': 'top', + 'outline': 'outline', + 'drill': 'drill', + } + +LAYER_RENDER_ORDER = [ 'copper', 'mask', 'silk', 'paste', 'outline', 'drill' ] + +def match_gerbers_in_dir(path): + out = {} + for side, layers in LAYER_SPEC.items(): + out[side] = {} + for layer, match in layers.items(): + l = list(find_gerber_in_dir(path, match)) + if l: + out[side][layer] = l + return out + +def find_gerber_in_dir(path, extensions): + exts = extensions.split('|') + for entry in path.iterdir(): + if not entry.is_file(): + continue + + if any(entry.name.lower().endswith(suffix.lower()) for suffix in exts): + yield entry + +def calculate_apertureless_bounding_box(cam): + ''' pcb-tools'es default bounding box function returns the bounding box of the primitives including apertures (i.e. + line widths). For determining a board's size from the outline layer, we want the bounding box disregarding + apertures. + ''' + + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for prim in cam.primitives: + bounds = prim.bounding_box_no_aperture + min_x = min(bounds[0][0], min_x) + max_x = max(bounds[0][1], max_x) + + min_y = min(bounds[1][0], min_y) + max_y = max(bounds[1][1], max_y) + + return ((min_x, max_x), (min_y, max_y)) + +def unpack_if_necessary(source, tmpdir, dirname='input'): + """ Handle command-line input paths. If path points to a directory, return unchanged. If path points to a zip file, + unpack to a directory inside tmpdir and return that. If path points to a file that is not a zip, copy that file into + a subdir of tmpdir and return that subdir. """ + # If source is not a directory with gerber files (-> zip/single gerber), make it one + if not source.is_dir(): + tmp_indir = tmpdir / dirname + tmp_indir.mkdir() + + if source.suffix.lower() == '.zip' or is_zipfile(source): + with ZipFile(source) as f: + f.extractall(path=tmp_indir) + + else: # single input file + shutil.copy(source, tmp_indir) + + return tmp_indir + + else: + return source + +def load_side(side_matches): + """ Load all gerber files for one side returned by match_gerbers_in_dir. """ + def load(layer, path): + print('loading', layer, 'layer from:', path) + grb = gerber.load_layer(str(path)) + grb.layer_class = LAYER_CLASSES.get(layer, 'unknown') + return grb + + layers = { layer: [ (path, load(layer, path)) for path in files ] + for layer, files in side_matches.items() } + + for layer, elems in layers.items(): + if len(elems) > 1 and layer != 'drill': + raise SystemError(f'Multiple files found for layer {layer}: {", ".join(str(x) for x in side_matches[layer]) }') + + unitses = set(layer.cam_source.units for items in layers.values() for _path, layer in items) + if len(unitses) != 1: + # FIXME: we should ideally be able to deal with this. We'll have to figure out a way to update a + # GerberCairoContext's scale in between layers. + raise SystemError('Input gerber files mix metric and imperial units. Please fix your export.') + units, = unitses + + return units, layers + +# SVG export +#=========== + +DEFAULT_EXTRA_LAYERS = [ layer for layer in LAYER_RENDER_ORDER if layer != "drill" ] + +def template_layer(name): + return f'' + +def template_svg_for_png(bounds, png_data, extra_layers=DEFAULT_EXTRA_LAYERS): + (x1, x2), (y1, y2) = bounds + w_mm, h_mm = (x2 - x1), (y2 - y1) + + extra_layers = "\n ".join(template_layer(name) for name in extra_layers) + + # we set up the viewport such that document dimensions = document units = mm + template = f''' + + + + + + {extra_layers} + + ''' + return textwrap.dedent(template) + +# this is fixed, we cannot tell cairo-svg to use some other value. we just have to work around it. +CAIRO_SVG_HARDCODED_DPI = 72.0 +MM_PER_INCH = 25.4 + +def svg_pt_to_mm(pt_len, dpi=CAIRO_SVG_HARDCODED_DPI): + if pt_len.endswith('pt'): + pt_len = pt_len[:-2] + + return f'{float(pt_len) / dpi * MM_PER_INCH}mm' + +def create_template_from_svg(bounds, svg_data, extra_layers=DEFAULT_EXTRA_LAYERS): + svg = etree.fromstring(svg_data) + + # add inkscape namespaces + SVG_NS = '{http://www.w3.org/2000/svg}' + INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape' + SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' + # glObAL stAtE YaY + etree.register_namespace('inkscape', INKSCAPE_NS) + etree.register_namespace('sodipodi', SODIPODI_NS) + INKSCAPE_NS = '{'+INKSCAPE_NS+'}' + SODIPODI_NS = '{'+SODIPODI_NS+'}' + + # convert document units to mm + svg.set('width', svg_pt_to_mm(svg.get('width'))) + svg.set('height', svg_pt_to_mm(svg.get('height'))) + + # make original group an inkscape layer + orig_g = svg.find(SVG_NS+'g') + orig_g.set('id', 'g-preview') + orig_g.set(INKSCAPE_NS+'label', 'Preview') + orig_g.set(SODIPODI_NS+'insensitive', 'true') # lock group + orig_g.set('style', 'opacity:0.5') + + # add layers + for layer in extra_layers: + new_g = etree.SubElement(svg, SVG_NS+'g') + new_g.set('id', f'g-{slugify(layer)}') + new_g.set(INKSCAPE_NS+'label', layer) + new_g.set(INKSCAPE_NS+'groupmode', 'layer') + + return etree.tostring(svg) + +# SVG/gerber import +#================== + +def dilate_gerber(layers, layer_name, dilation, bbox, tmpdir, outfile, units, curve_tolerance): + if layer_name not in layers: + raise ValueError(f'Cannot dilate layer {layer_name}: layer not found in input dir') + + bounds = get_bounds(bbox, layers) + (x_min_mm, x_max_mm), (y_min_mm, y_max_mm) = bounds + + origin_x = x_min_mm / MM_PER_INCH + origin_y = y_min_mm / MM_PER_INCH + + width = (x_max_mm - x_min_mm) / MM_PER_INCH + height = (y_max_mm - y_min_mm) / MM_PER_INCH + + tmpfile = tmpdir / 'dilate-tmp.svg' + path, _gbr = layers[layer_name][0] + # NOTE: gerbv has an undocumented maximum length of 20 chars for the arguments to --origin and --window_inch + cmd = ['gerbv', '-x', 'svg', + '--border=0', + f'--origin={origin_x:.6f}x{origin_y:.6f}', f'--window_inch={width:.6f}x{height:.6f}', + '--foreground=#ffffff', + '-o', str(tmpfile), str(path)] + print('dilation cmd:', ' '.join(cmd)) + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # dilate & render back to gerber + # TODO: the scale parameter is a hack. ideally we would fix svg-flatten to handle input units correctly. + svg_to_gerber(tmpfile, outfile, dilate=-dilation*72.0/25.4, dpi=72, scale=25.4/72.0, curve_tolerance=curve_tolerance) + +def svg_to_gerber(infile, outfile, + layer=None, trace_space:'mm'=0.1, + vectorizer=None, vectorizer_map=None, + exclude_groups=None, + dilate=None, curve_tolerance=None, + dpi=None, scale=None, bounds_for_png=None, + preserve_aspect_ratio=None, + force_png=False, force_svg=False): + + infile = Path(infile) + + if 'SVG_FLATTEN' in os.environ: + candidates = [os.environ['SVG_FLATTEN']] + + else: + # By default, try three options: + candidates = [ + # somewhere in $PATH + 'svg-flatten', + # in user-local pip installation + Path.home() / '.local' / 'bin' / 'svg-flatten', + # next to our current python interpreter (e.g. in virtualenv + str(Path(sys.executable).parent / 'svg-flatten'), + # next to this python source file in the development repo + str(Path(__file__).parent.parent / 'svg-flatten' / 'build' / 'svg-flatten') ] + + args = [ '--format', 'gerber', + '--precision', '6', # intermediate file, use higher than necessary precision + '--trace-space', str(trace_space) ] + if layer: + args += ['--only-groups', f'g-{slugify(layer)}'] + if vectorizer: + args += ['--vectorizer', vectorizer] + if vectorizer_map: + args += ['--vectorizer-map', vectorizer_map] + if exclude_groups: + args += ['--exclude-groups', exclude_groups] + if dilate: + args += ['--dilate', str(dilate)] + if curve_tolerance is not None: + print('applying curve tolerance', curve_tolerance) + args += ['--curve-tolerance', str(curve_tolerance)] + if dpi: + args += ['--usvg-dpi', str(dpi)] + if scale: + args += ['--scale', str(scale)] + if force_png or (infile.suffix.lower() in ['.jpg', '.png'] and not force_svg): + (min_x, max_x), (min_y, max_y) = bounds_for_png + args += ['--size', f'{max_x - min_x}x{max_y - min_y}'] + if force_svg and force_png: + raise ValueError('both force_svg and force_png given') + if force_svg: + args += ['--force-svg'] + if force_png: + args += ['--force-png'] + if preserve_aspect_ratio: + args += ['--preserve-aspect-ratio', preserve_aspect_ratio] + + args += [str(infile), str(outfile)] + + print('svg-flatten args:', ' '.join(args)) + for candidate in candidates: + try: + res = subprocess.run([candidate, *args], check=True) + print('used svg-flatten:', candidate) + break + except FileNotFoundError: + continue + else: + raise SystemError('svg-flatten executable not found') + + +if __name__ == '__main__': + cli() diff --git a/gerboweb/deploy/.gitignore b/gerboweb/deploy/.gitignore deleted file mode 100644 index 136f960..0000000 --- a/gerboweb/deploy/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*_secret.txt -*_apikey.txt -playbook.retry -credentials.ini diff --git a/gerboweb/deploy/bootstrap_arch_container.yml b/gerboweb/deploy/bootstrap_arch_container.yml deleted file mode 100644 index e983f5c..0000000 --- a/gerboweb/deploy/bootstrap_arch_container.yml +++ /dev/null @@ -1,60 +0,0 @@ ---- -- name: Set local path facts - set_fact: - image: "/var/lib/machines/{{ container }}.img" - root: "/var/lib/machines/{{ container }}" - "{{container}}_root": "/var/lib/machines/{{ container }}" - -- name: Create container image file - command: truncate -s 4G "{{image}}" - args: - creates: "{{image}}" - register: create_container - -- name: Download arch bootstrap image - get_url: - url: http://mirror.rackspace.com/archlinux/iso/2020.03.01/archlinux-bootstrap-2020.03.01-x86_64.tar.gz - dest: /tmp/arch-bootstrap.tar.xz - checksum: sha256:49c7aa8718e48f5a4ec570624520fa50616ed3e044af101ec3aa16c155136f82 - when: create_container is changed - -- name: Create container image filesystem - filesystem: - dev: "{{image}}" - fstype: btrfs - -- name: Create container image fstab entry - mount: - src: "{{image}}" - path: "{{root}}" - state: mounted - fstype: btrfs - opts: loop - -- name: Unpack bootstrap image - unarchive: - remote_src: yes - src: /tmp/arch-bootstrap.tar.xz - dest: "{{root}}" - extra_opts: --strip-components=1 - creates: "{{root}}/etc" - -- name: Copy mirrorlist into container - copy: - src: mirrorlist - dest: "{{root}}/etc/pacman.d/mirrorlist" - -- name: Initialize container pacman keyring - shell: arch-chroot "{{root}}" pacman-key --init && arch-chroot "{{root}}" pacman-key --populate archlinux - args: - creates: "{{root}}/etc/pacman.d/gnupg" - -- name: Fixup pacman.conf for pacman to work in chroot without its own root fs - lineinfile: - path: "{{root}}/etc/pacman.conf" - regexp: '^CheckSpace' - line: '#CheckSpace' - -- name: Update container and install software - shell: arch-chroot "{{root}}" pacman -Syu --noconfirm - diff --git a/gerboweb/deploy/cgit-logo.png b/gerboweb/deploy/cgit-logo.png deleted file mode 100644 index f781fdd..0000000 Binary files a/gerboweb/deploy/cgit-logo.png and /dev/null differ diff --git a/gerboweb/deploy/cgitrc b/gerboweb/deploy/cgitrc deleted file mode 100644 index d77778b..0000000 --- a/gerboweb/deploy/cgitrc +++ /dev/null @@ -1,20 +0,0 @@ -css=/cgit.css -logo= /cgit.png - -enable-http-clone=1 -robots=noindex, nofollow -virtual-root=/ - -readme=:README.rst -about-filter=/usr/libexec/cgit/filters/about-formatting.sh - -enable-index-links=1 -enable-commit-grpah=1 -enable-log-filecount=1 -enable-log-linecount=1 -enable-git-config=1 - -source-filter=/usr/libexec/cgit/filters/syntax-highlighting.py - -project-list=/var/lib/gitolite3/projects.list -scan-path=/var/lib/gitolite3/repositories diff --git a/gerboweb/deploy/checkouts/pogojig b/gerboweb/deploy/checkouts/pogojig deleted file mode 160000 index 13a5721..0000000 --- a/gerboweb/deploy/checkouts/pogojig +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 13a57211f0d0feb34b452b3e19be83a095707ed6 diff --git a/gerboweb/deploy/clippy-nspawn.service b/gerboweb/deploy/clippy-nspawn.service deleted file mode 100644 index 8dbedbd..0000000 --- a/gerboweb/deploy/clippy-nspawn.service +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1+ -# -# This file is part of systemd. -# -# systemd is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. - -[Unit] -Description=Clippy container -PartOf=machines.target -Before=machines.target -After=network.target systemd-resolved.service -RequiresMountsFor=/var/lib/machines - -[Service] -ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --ephemeral --boot -U --settings=override --machine=clippy -KillMode=mixed -Type=notify -RestartForceExitStatus=133 -SuccessExitStatus=133 -WatchdogSec=3min -Slice=machine.slice -Delegate=yes -TasksMax=512 - -# Enforce a strict device policy, similar to the one nspawn configures when it -# allocates its own scope unit. Make sure to keep these policies in sync if you -# change them! -DevicePolicy=closed -DeviceAllow=/dev/net/tun rwm -DeviceAllow=char-pts rw - -[Install] -WantedBy=machines.target diff --git a/gerboweb/deploy/clippy.nspawn b/gerboweb/deploy/clippy.nspawn deleted file mode 100644 index dfe2935..0000000 --- a/gerboweb/deploy/clippy.nspawn +++ /dev/null @@ -1,2 +0,0 @@ -[Network] -VirtualEthernet=no diff --git a/gerboweb/deploy/clippy.service.j2 b/gerboweb/deploy/clippy.service.j2 deleted file mode 100644 index 22b3d7d..0000000 --- a/gerboweb/deploy/clippy.service.j2 +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Clippy listener daemon - -[Service] -WorkingDirectory=/var/lib/clippy.git -ExecStart=/usr/bin/python3 clippy.py -s -x 60x30 -e - -[Install] -WantedBy=multi-user.target diff --git a/gerboweb/deploy/credentials.ini.example b/gerboweb/deploy/credentials.ini.example deleted file mode 100644 index 9b87321..0000000 --- a/gerboweb/deploy/credentials.ini.example +++ /dev/null @@ -1,3 +0,0 @@ -[inwx] -user=... -pass=... diff --git a/gerboweb/deploy/gerboweb-job-processor.service.j2 b/gerboweb/deploy/gerboweb-job-processor.service.j2 deleted file mode 100644 index 517d8b8..0000000 --- a/gerboweb/deploy/gerboweb-job-processor.service.j2 +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Gerboweb gerber job processor - -[Service] -WorkingDirectory=/var/lib/gerboweb -ExecStart=/usr/bin/python3 job_processor.py {{gerboweb_cache}}/job_queue.sqlite3 - -[Install] -WantedBy=uwsgi-app@gerboweb.service diff --git a/gerboweb/deploy/gerboweb.cfg.j2 b/gerboweb/deploy/gerboweb.cfg.j2 deleted file mode 100644 index 994cd08..0000000 --- a/gerboweb/deploy/gerboweb.cfg.j2 +++ /dev/null @@ -1,4 +0,0 @@ -MAX_CONTENT_LENGTH=10000000 -SECRET_KEY="{{lookup('password', 'gerboweb_flask_secret.txt length=32')}}" -UPLOAD_PATH="{{gerboweb_cache}}/upload" -JOB_QUEUE_DB="{{gerboweb_cache}}/job_queue.sqlite3" diff --git a/gerboweb/deploy/gitolite.rc b/gerboweb/deploy/gitolite.rc deleted file mode 100644 index 2c8435a..0000000 --- a/gerboweb/deploy/gitolite.rc +++ /dev/null @@ -1,202 +0,0 @@ -# configuration variables for gitolite - -# This file is in perl syntax. But you do NOT need to know perl to edit it -- -# just mind the commas, use single quotes unless you know what you're doing, -# and make sure the brackets and braces stay matched up! - -# (Tip: perl allows a comma after the last item in a list also!) - -# HELP for commands can be had by running the command with "-h". - -# HELP for all the other FEATURES can be found in the documentation (look for -# "list of non-core programs shipped with gitolite" in the master index) or -# directly in the corresponding source file. - -%RC = ( - - # ------------------------------------------------------------------ - - # default umask gives you perms of '0700'; see the rc file docs for - # how/why you might change this - UMASK => 0027, - - # look for "git-config" in the documentation - GIT_CONFIG_KEYS => 'core\.sharedRepository', - - # comment out if you don't need all the extra detail in the logfile - LOG_EXTRA => 1, - # logging options - # 1. leave this section as is for 'normal' gitolite logging (default) - # 2. uncomment this line to log ONLY to syslog: - # LOG_DEST => 'syslog', - # 3. uncomment this line to log to syslog and the normal gitolite log: - # LOG_DEST => 'syslog,normal', - # 4. prefixing "repo-log," to any of the above will **also** log just the - # update records to "gl-log" in the bare repo directory: - # LOG_DEST => 'repo-log,normal', - # LOG_DEST => 'repo-log,syslog', - # LOG_DEST => 'repo-log,syslog,normal', - # syslog 'facility': defaults to 'local0', uncomment if needed. For example: - # LOG_FACILITY => 'local4', - - # roles. add more roles (like MANAGER, TESTER, ...) here. - # WARNING: if you make changes to this hash, you MUST run 'gitolite - # compile' afterward, and possibly also 'gitolite trigger POST_COMPILE' - ROLES => { - READERS => 1, - WRITERS => 1, - }, - - # enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!! - # CACHE => 'Redis', - - # ------------------------------------------------------------------ - - # rc variables used by various features - - # the 'info' command prints this as additional info, if it is set - # SITE_INFO => 'Please see http://blahblah/gitolite for more help', - - # the CpuTime feature uses these - # display user, system, and elapsed times to user after each git operation - # DISPLAY_CPU_TIME => 1, - # display a warning if total CPU times (u, s, cu, cs) crosses this limit - # CPU_TIME_WARN_LIMIT => 0.1, - - # the Mirroring feature needs this - # HOSTNAME => "foo", - - # TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING! - # CACHE_TTL => 600, - - # ------------------------------------------------------------------ - - # suggested locations for site-local gitolite code (see cust.html) - - # this one is managed directly on the server - # LOCAL_CODE => "$ENV{HOME}/local", - - # or you can use this, which lets you put everything in a subdirectory - # called "local" in your gitolite-admin repo. For a SECURITY WARNING - # on this, see http://gitolite.com/gitolite/non-core.html#pushcode - # LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local", - - # ------------------------------------------------------------------ - - # List of commands and features to enable - - ENABLE => [ - - # COMMANDS - - # These are the commands enabled by default - 'help', - 'desc', - 'info', - 'perms', - 'writable', - - # Uncomment or add new commands here. - # 'create', - # 'fork', - # 'mirror', - # 'readme', - # 'sskm', - 'D', - - # These FEATURES are enabled by default. - - # essential (unless you're using smart-http mode) - 'ssh-authkeys', - - # creates git-config entries from gitolite.conf file entries like 'config foo.bar = baz' - 'git-config', - - # creates git-daemon-export-ok files; if you don't use git-daemon, comment this out - 'daemon', - - # creates projects.list file; if you don't use gitweb, comment this out - 'gitweb', - - # These FEATURES are disabled by default; uncomment to enable. If you - # need to add new ones, ask on the mailing list :-) - - # user-visible behaviour - - # prevent wild repos auto-create on fetch/clone - # 'no-create-on-read', - # no auto-create at all (don't forget to enable the 'create' command!) - # 'no-auto-create', - - # access a repo by another (possibly legacy) name - # 'Alias', - - # give some users direct shell access. See documentation in - # sts.html for details on the following two choices. - # "Shell $ENV{HOME}/.gitolite.shell-users", - # 'Shell alice bob', - - # set default roles from lines like 'option default.roles-1 = ...', etc. - # 'set-default-roles', - - # show more detailed messages on deny - # 'expand-deny-messages', - - # show a message of the day - # 'Motd', - - # system admin stuff - - # enable mirroring (don't forget to set the HOSTNAME too!) - # 'Mirroring', - - # allow people to submit pub files with more than one key in them - # 'ssh-authkeys-split', - - # selective read control hack - # 'partial-copy', - - # manage local, gitolite-controlled, copies of read-only upstream repos - # 'upstream', - - # updates 'description' file instead of 'gitweb.description' config item - # 'cgit', - - # allow repo-specific hooks to be added - # 'repo-specific-hooks', - - # performance, logging, monitoring... - - # be nice - # 'renice 10', - - # log CPU times (user, system, cumulative user, cumulative system) - # 'CpuTime', - - # syntactic_sugar for gitolite.conf and included files - - # allow backslash-escaped continuation lines in gitolite.conf - # 'continuation-lines', - - # create implicit user groups from directory names in keydir/ - # 'keysubdirs-as-groups', - - # allow simple line-oriented macros - # 'macros', - - # Kindergarten mode - - # disallow various things that sensible people shouldn't be doing anyway - # 'Kindergarten', - ], - -); - -# ------------------------------------------------------------------------------ -# per perl rules, this should be the last line in such a file: -1; - -# Local variables: -# mode: perl -# End: -# vim: set syn=perl: diff --git a/gerboweb/deploy/inventory.yml b/gerboweb/deploy/inventory.yml deleted file mode 100644 index 913ea5f..0000000 --- a/gerboweb/deploy/inventory.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -all: - hosts: - wendelstein: - ansible_host: wendelstein.jaseg.net - ansible_ssh_identity_file: ~/.ssh/id_ed25519 - ansible_user: root - ansible_python_interpreter: /usr/bin/python3 - localhost: - ansible_connection: local - ansible_python_interpreter: "{{ansible_playbook_python}}" diff --git a/gerboweb/deploy/iptables.rules b/gerboweb/deploy/iptables.rules deleted file mode 100644 index 620c4d3..0000000 --- a/gerboweb/deploy/iptables.rules +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019 -*nat -:PREROUTING ACCEPT [13:648] -:INPUT ACCEPT [8:440] -:OUTPUT ACCEPT [18:1260] -:POSTROUTING ACCEPT [18:1260] --A PREROUTING -i eth0 -p tcp -m tcp --dport 23 -j REDIRECT --to-ports 2342 -COMMIT -# Completed on Thu Apr 4 11:06:33 2019 -# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019 -*filter -:INPUT ACCEPT [0:0] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [360:761646] --A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT --A INPUT -p icmp -j ACCEPT --A INPUT -i lo -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 2342 -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 23 -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT --A INPUT -p udp --dport 53 -j ACCEPT --A INPUT -j REJECT --reject-with icmp-host-prohibited --A FORWARD -j REJECT --reject-with icmp-host-prohibited -COMMIT -# Completed on Thu Apr 4 11:06:33 2019 diff --git a/gerboweb/deploy/library/inwx-collection b/gerboweb/deploy/library/inwx-collection deleted file mode 160000 index 0ac040d..0000000 --- a/gerboweb/deploy/library/inwx-collection +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0ac040da14cc9d834098addc03cd8d4d26647df0 diff --git a/gerboweb/deploy/mirrorlist b/gerboweb/deploy/mirrorlist deleted file mode 100644 index a2fd58c..0000000 --- a/gerboweb/deploy/mirrorlist +++ /dev/null @@ -1,474 +0,0 @@ -## -## Arch Linux repository mirrorlist -## Generated on 2017-06-06 -## - -## Worldwide -#Server = https://archlinux.surlyjake.com/archlinux/$repo/os/$arch -#Server = http://mirrors.evowise.com/archlinux/$repo/os/$arch -Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch - -## Australia -#Server = https://mirror.aarnet.edu.au/pub/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch -#Server = http://ftp.iinet.net.au/pub/archlinux/$repo/os/$arch -#Server = http://mirror.internode.on.net/pub/archlinux/$repo/os/$arch -#Server = http://ftp.swin.edu.au/archlinux/$repo/os/$arch -#Server = http://archlinux.uberglobalmirror.com/$repo/os/$arch - -## Austria -#Server = http://mirror.digitalnova.at/archlinux/$repo/os/$arch -#Server = http://mirror.easyname.at/archlinux/$repo/os/$arch -#Server = http://mirror1.htu.tugraz.at/archlinux/$repo/os/$arch - -## Belarus -#Server = http://ftp.byfly.by/pub/archlinux/$repo/os/$arch -#Server = http://mirror.datacenter.by/pub/archlinux/$repo/os/$arch - -## Belgium -#Server = http://archlinux.cu.be/$repo/os/$arch -#Server = http://archlinux.mirror.kangaroot.net/$repo/os/$arch - -## Bosnia and Herzegovina -#Server = http://burek.archlinux.ba/$repo/os/$arch -#Server = http://archlinux.mirror.ba/$repo/os/$arch - -## Brazil -#Server = http://br.mirror.archlinux-br.org/$repo/os/$arch -#Server = http://archlinux.c3sl.ufpr.br/$repo/os/$arch -#Server = http://linorg.usp.br/archlinux/$repo/os/$arch -#Server = http://pet.inf.ufsc.br/mirrors/archlinux/$repo/os/$arch -#Server = http://archlinux.pop-es.rnp.br/$repo/os/$arch - -## Bulgaria -#Server = http://mirror.host.ag/archlinux/$repo/os/$arch -#Server = http://mirrors.netix.net/archlinux/$repo/os/$arch -#Server = http://mirror.telepoint.bg/archlinux/$repo/os/$arch -#Server = http://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch -#Server = https://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch - -## Canada -#Server = http://mirror.cedille.club/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.colo-serv.net/$repo/os/$arch -#Server = http://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch -#Server = https://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch -#Server = http://mirror.frgl.pw/archlinux/$repo/os/$arch -#Server = https://mirror.frgl.pw/archlinux/$repo/os/$arch -#Server = http://mirror.its.dal.ca/archlinux/$repo/os/$arch -#Server = http://muug.ca/mirror/archlinux/$repo/os/$arch -#Server = https://muug.ca/mirror/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.rafal.ca/$repo/os/$arch - -## Chile -#Server = http://mirror.archlinux.cl/$repo/os/$arch - -## China -#Server = http://mirrors.163.com/archlinux/$repo/os/$arch -#Server = http://mirror.lzu.edu.cn/archlinux/$repo/os/$arch -#Server = http://mirrors.neusoft.edu.cn/archlinux/$repo/os/$arch -#Server = https://mirrors.skyshe.cn/archlinux/$repo/os/$arch -#Server = http://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch -#Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch -#Server = http://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch -#Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch -#Server = http://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch -#Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch -#Server = http://mirrors.zju.edu.cn/archlinux/$repo/os/$arch - -## Colombia -#Server = http://mirror.edatel.net.co/archlinux/$repo/os/$arch -#Server = http://mirror.upb.edu.co/archlinux/$repo/os/$arch - -## Croatia -#Server = http://archlinux.iskon.hr/$repo/os/$arch - -## Czech Republic -#Server = http://mirror.dkm.cz/archlinux/$repo/os/$arch -#Server = https://mirror.dkm.cz/archlinux/$repo/os/$arch -#Server = http://ftp.fi.muni.cz/pub/linux/arch/$repo/os/$arch -#Server = http://ftp.linux.cz/pub/linux/arch/$repo/os/$arch -#Server = http://gluttony.sin.cvut.cz/arch/$repo/os/$arch -#Server = https://gluttony.sin.cvut.cz/arch/$repo/os/$arch -#Server = http://mirrors.nic.cz/archlinux/$repo/os/$arch -#Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch -#Server = https://ftp.sh.cvut.cz/arch/$repo/os/$arch -#Server = http://mirror.vpsfree.cz/archlinux/$repo/os/$arch - -## Denmark -#Server = http://mirrors.dotsrc.org/archlinux/$repo/os/$arch -#Server = https://mirrors.dotsrc.org/archlinux/$repo/os/$arch -#Server = http://ftp.klid.dk/ftp/archlinux/$repo/os/$arch -#Server = http://mirror.one.com/archlinux/$repo/os/$arch -#Server = https://mirror.one.com/archlinux/$repo/os/$arch - -## Ecuador -#Server = http://mirror.cedia.org.ec/archlinux/$repo/os/$arch -#Server = http://mirror.espoch.edu.ec/archlinux/$repo/os/$arch -#Server = http://mirror.uta.edu.ec/archlinux/$repo/os/$arch - -## Finland -#Server = http://arch.mirror.far.fi/$repo/os/$arch - -## France -#Server = http://archlinux.de-labrusse.fr/$repo/os/$arch -#Server = http://mirror.archlinux.ikoula.com/archlinux/$repo/os/$arch -#Server = http://archlinux.vi-di.fr/$repo/os/$arch -#Server = https://archlinux.vi-di.fr/$repo/os/$arch -#Server = http://mirror.armbrust.me/archlinux/$repo/os/$arch -#Server = https://mirror.armbrust.me/archlinux/$repo/os/$arch -#Server = https://archlinux.ec-tech.fr/$repo/os/$arch -#Server = http://fooo.biz/archlinux/$repo/os/$arch -#Server = https://fooo.biz/archlinux/$repo/os/$arch -#Server = http://mirror.gerhard.re/archlinux/$repo/os/$arch -#Server = http://mirror.ibcp.fr/pub/archlinux/$repo/os/$arch -#Server = http://mirror.lastmikoi.net/archlinux/$repo/os/$arch -#Server = http://archlinux.mailtunnel.eu/$repo/os/$arch -#Server = https://www.mailtunnel.eu/archlinux/$repo/os/$arch -#Server = http://mir.archlinux.fr/$repo/os/$arch -#Server = http://archlinux.mirrors.ovh.net/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.pkern.at/$repo/os/$arch -#Server = https://archlinux.mirror.pkern.at/$repo/os/$arch -#Server = http://archlinux.polymorf.fr/$repo/os/$arch -#Server = http://mirrors.standaloneinstaller.com/archlinux/$repo/os/$arch -#Server = http://arch.tamcore.eu/$repo/os/$arch -#Server = http://mirror.tyborek.pl/arch/$repo/os/$arch -#Server = https://mirror.tyborek.pl/arch/$repo/os/$arch -#Server = http://ftp.u-strasbg.fr/linux/distributions/archlinux/$repo/os/$arch -#Server = https://mirror.wormhole.eu/archlinux/$repo/os/$arch -#Server = http://arch.yourlabs.org/$repo/os/$arch - -## Germany -#Server = http://mirror.23media.de/archlinux/$repo/os/$arch -#Server = https://arch.32g.eu/$repo/os/$arch -#Server = http://artfiles.org/archlinux.org/$repo/os/$arch -#Server = https://fabric-mirror.vps.hosteurope.de/archlinux/$repo/os/$arch -#Server = https://mirror.bethselamin.de/$repo/os/$arch -#Server = http://mirror.euserv.net/linux/archlinux/$repo/os/$arch -#Server = http://mirror.f4st.host/archlinux/$repo/os/$arch -#Server = https://mirror.f4st.host/archlinux/$repo/os/$arch -#Server = http://ftp.fau.de/archlinux/$repo/os/$arch -#Server = https://ftp.fau.de/archlinux/$repo/os/$arch -#Server = http://mirror.fluxent.de/archlinux/$repo/os/$arch -#Server = https://mirror.fluxent.de/archlinux/$repo/os/$arch -#Server = http://mirror.gnomus.de/$repo/os/$arch -#Server = http://www.gutscheindrache.com/mirror/archlinux/$repo/os/$arch -#Server = http://ftp.gwdg.de/pub/linux/archlinux/$repo/os/$arch -#Server = http://mirror.hactar.xyz/$repo/os/$arch -#Server = https://mirror.hactar.xyz/$repo/os/$arch -#Server = http://archlinux.honkgong.info/$repo/os/$arch -#Server = http://ftp.hosteurope.de/mirror/ftp.archlinux.org/$repo/os/$arch -#Server = http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.iphh.net/$repo/os/$arch -#Server = http://repo.itmettke.de/archlinux/$repo/os/$arch -#Server = https://repo.itmettke.de/archlinux/$repo/os/$arch -#Server = https://mirror.jankoppe.de/archlinux/$repo/os/$arch -#Server = http://arch.jensgutermuth.de/$repo/os/$arch -#Server = https://arch.jensgutermuth.de/$repo/os/$arch -#Server = http://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch -#Server = https://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch -#Server = http://k42.ch/mirror/archlinux/$repo/os/$arch -#Server = https://k42.ch/mirror/archlinux/$repo/os/$arch -#Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch -Server = https://mirror.de.leaseweb.net/archlinux/$repo/os/$arch -#Server = http://mirror.loli.forsale/arch/$repo/os/$arch -#Server = https://mirror.loli.forsale/arch/$repo/os/$arch -#Server = http://mirror.metalgamer.eu/archlinux/$repo/os/$arch -#Server = https://mirror.metalgamer.eu/archlinux/$repo/os/$arch -#Server = http://mirror.michael-eckert.net/archlinux/$repo/os/$arch -#Server = https://mirror.michael-eckert.net/archlinux/$repo/os/$arch -#Server = http://mirrors.n-ix.net/archlinux/$repo/os/$arch -#Server = https://mirrors.n-ix.net/archlinux/$repo/os/$arch -#Server = http://mirror.netcologne.de/archlinux/$repo/os/$arch -Server = https://mirror.netcologne.de/archlinux/$repo/os/$arch -#Server = http://mirrors.niyawe.de/archlinux/$repo/os/$arch -#Server = https://mirrors.niyawe.de/archlinux/$repo/os/$arch -#Server = http://archlinux.nullpointer.io/$repo/os/$arch -#Server = https://archlinux.nullpointer.io/$repo/os/$arch -#Server = http://mirror.pseudoform.org/$repo/os/$arch -#Server = https://mirror.pseudoform.org/$repo/os/$arch -#Server = https://www.ratenzahlung.de/mirror/archlinux/$repo/os/$arch -#Server = http://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch -#Server = http://linux.rz.rub.de/archlinux/$repo/os/$arch -#Server = http://mirror.selfnet.de/archlinux/$repo/os/$arch -#Server = http://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch -#Server = https://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch -#Server = http://archlinux.thaller.ws/$repo/os/$arch -#Server = https://archlinux.thaller.ws/$repo/os/$arch -#Server = http://archlinux.thelinuxnetworx.rocks/$repo/os/$arch -#Server = https://archlinux.thelinuxnetworx.rocks/$repo/os/$arch -#Server = http://archmirror.tomforb.es/$repo/os/$arch -#Server = https://archmirror.tomforb.es/$repo/os/$arch -#Server = http://ftp.tu-chemnitz.de/pub/linux/archlinux/$repo/os/$arch -#Server = http://mirror.ubrco.de/archlinux/$repo/os/$arch -#Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch -#Server = http://ftp.uni-bayreuth.de/linux/archlinux/$repo/os/$arch -#Server = http://ftp.uni-hannover.de/archlinux/$repo/os/$arch -#Server = http://ftp.uni-kl.de/pub/linux/archlinux/$repo/os/$arch -#Server = http://mirror.united-gameserver.de/archlinux/$repo/os/$arch -#Server = http://mirror.vfn-nrw.de/archlinux/$repo/os/$arch -#Server = https://mirror.vfn-nrw.de/archlinux/$repo/os/$arch - -## Greece -#Server = http://ftp.cc.uoc.gr/mirrors/linux/archlinux/$repo/os/$arch -#Server = http://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch -#Server = https://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch -#Server = http://mirrors.myaegean.gr/linux/archlinux/$repo/os/$arch -#Server = http://ftp.ntua.gr/pub/linux/archlinux/$repo/os/$arch -#Server = http://ftp.otenet.gr/linux/archlinux/$repo/os/$arch - -## Hong Kong -#Server = http://arch-mirror.wtako.net/$repo/os/$arch -#Server = https://arch-mirror.wtako.net/$repo/os/$arch - -## Hungary -#Server = http://ftp.energia.mta.hu/pub/mirrors/ftp.archlinux.org/$repo/os/$arch -#Server = http://archmirror.hbit.sztaki.hu/archlinux/$repo/os/$arch - -## Iceland -#Server = http://mirror.system.is/arch/$repo/os/$arch -#Server = https://mirror.system.is/arch/$repo/os/$arch - -## India -#Server = http://mirror.cse.iitk.ac.in/archlinux/$repo/os/$arch -#Server = http://ftp.iitm.ac.in/archlinux/$repo/os/$arch - -## Indonesia -#Server = http://mirror.devilzc0de.org/archlinux/$repo/os/$arch -#Server = http://mirror.poliwangi.ac.id/archlinux/$repo/os/$arch -#Server = http://suro.ubaya.ac.id/archlinux/$repo/os/$arch - -## Iran -#Server = http://repo.sadjad.ac.ir/arch/$repo/os/$arch -#Server = https://repo.sadjad.ac.ir/arch/$repo/os/$arch - -## Ireland -#Server = http://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch -#Server = https://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch - -## Israel -#Server = http://mirror.isoc.org.il/pub/archlinux/$repo/os/$arch - -## Italy -#Server = http://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch -#Server = https://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch -#Server = https://archlinux.beccacervello.it/archlinux/$repo/os/$arch -#Server = http://mi.mirror.garr.it/mirrors/archlinux/$repo/os/$arch -#Server = http://mirrors.prometeus.net/archlinux/$repo/os/$arch -#Server = http://archlinux.students.cs.unibo.it/$repo/os/$arch - -## Japan -#Server = http://ftp.tsukuba.wide.ad.jp/Linux/archlinux/$repo/os/$arch -Server = http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch - -## Kazakhstan -#Server = http://mirror.neolabs.kz/archlinux/$repo/os/$arch - -## Latvia -#Server = http://archlinux.koyanet.lv/archlinux/$repo/os/$arch - -## Lithuania -#Server = http://mirrors.atviras.lt/archlinux/$repo/os/$arch -#Server = https://mirrors.atviras.lt/archlinux/$repo/os/$arch - -## Luxembourg -#Server = http://archlinux.mirror.root.lu/$repo/os/$arch - -## Macedonia -#Server = http://arch.softver.org.mk/archlinux/$repo/os/$arch -#Server = http://mirror.t-home.mk/archlinux/$repo/os/$arch -#Server = https://mirror.t-home.mk/archlinux/$repo/os/$arch - -## Netherlands -#Server = http://arch.apt-get.eu/$repo/os/$arch -#Server = http://mirror.i3d.net/pub/archlinux/$repo/os/$arch -#Server = https://mirror.i3d.net/pub/archlinux/$repo/os/$arch -#Server = http://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch -#Server = https://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch -#Server = http://mirror.netrouting.net/archlinux/$repo/os/$arch -#Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch -#Server = http://ftp.snt.utwente.nl/pub/os/linux/archlinux/$repo/os/$arch -#Server = http://archlinux.mirror.wearetriple.com/$repo/os/$arch -#Server = https://archlinux.mirror.wearetriple.com/$repo/os/$arch - -## New Caledonia -#Server = http://mirror.lagoon.nc/pub/archlinux/$repo/os/$arch -#Server = http://archlinux.nautile.nc/archlinux/$repo/os/$arch - -## New Zealand -#Server = https://mirror.smith.geek.nz/archlinux/$repo/os/$arch - -## Norway -#Server = http://mirror.archlinux.no/$repo/os/$arch -#Server = http://archlinux.uib.no/$repo/os/$arch -#Server = http://mirror.neuf.no/archlinux/$repo/os/$arch -#Server = https://mirror.neuf.no/archlinux/$repo/os/$arch - -## Philippines -#Server = http://mirror.rise.ph/archlinux/$repo/os/$arch - -## Poland -#Server = http://mirror.chmuri.net/archmirror/$repo/os/$arch -#Server = http://arch.midov.pl/arch/$repo/os/$arch -#Server = http://mirror.onet.pl/pub/mirrors/archlinux/$repo/os/$arch -#Server = http://piotrkosoft.net/pub/mirrors/ftp.archlinux.org/$repo/os/$arch -#Server = http://ftp.vectranet.pl/archlinux/$repo/os/$arch - -## Portugal -#Server = http://glua.ua.pt/pub/archlinux/$repo/os/$arch -#Server = https://glua.ua.pt/pub/archlinux/$repo/os/$arch -#Server = http://ftp.rnl.tecnico.ulisboa.pt/pub/archlinux/$repo/os/$arch - -## Qatar -#Server = http://mirror.qnren.qa/archlinux/$repo/os/$arch - -## Romania -#Server = http://mirror.archlinux.ro/archlinux/$repo/os/$arch -#Server = http://archlinux.mirrors.linux.ro/$repo/os/$arch -#Server = http://mirrors.m247.ro/archlinux/$repo/os/$arch -#Server = http://mirrors.pidginhost.com/arch/$repo/os/$arch - -## Russia -#Server = http://mirror.aur.rocks/$repo/os/$arch -#Server = https://mirror.aur.rocks/$repo/os/$arch -#Server = http://mirror.rol.ru/archlinux/$repo/os/$arch -#Server = https://mirror.rol.ru/archlinux/$repo/os/$arch -#Server = http://mirror.yandex.ru/archlinux/$repo/os/$arch -#Server = https://mirror.yandex.ru/archlinux/$repo/os/$arch - -## Serbia -#Server = http://mirror.pmf.kg.ac.rs/archlinux/$repo/os/$arch - -## Singapore -#Server = http://mirror.0x.sg/archlinux/$repo/os/$arch -#Server = http://download.nus.edu.sg/mirror/arch/$repo/os/$arch - -## Slovakia -#Server = http://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch -#Server = https://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch -#Server = http://tux.rainside.sk/archlinux/$repo/os/$arch - -## Slovenia -#Server = http://archimonde.ts.si/archlinux/$repo/os/$arch -#Server = https://archimonde.ts.si/archlinux/$repo/os/$arch - -## South Africa -#Server = http://za.mirror.archlinux-br.org/$repo/os/$arch -#Server = http://ftp.wa.co.za/pub/archlinux/$repo/os/$arch -#Server = http://mirror.is.co.za/mirror/archlinux.org/$repo/os/$arch -#Server = http://mirror.wbs.co.za/archlinux/$repo/os/$arch - -## South Korea -#Server = http://ftp.kaist.ac.kr/ArchLinux/$repo/os/$arch -#Server = http://mirror.premi.st/archlinux/$repo/os/$arch - -## Spain -#Server = http://osl.ugr.es/archlinux/$repo/os/$arch -#Server = http://sunsite.rediris.es/mirror/archlinux/$repo/os/$arch - -## Sweden -#Server = http://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch -#Server = https://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch -#Server = http://archlinux.dynamict.se/$repo/os/$arch -#Server = https://archlinux.dynamict.se/$repo/os/$arch -#Server = http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch -#Server = https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch -#Server = http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch -#Server = https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch -#Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch -#Server = http://ftp.portlane.com/pub/os/linux/archlinux/$repo/os/$arch - -## Switzerland -#Server = http://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch -#Server = https://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch -#Server = http://archlinux.puzzle.ch/$repo/os/$arch - -## Taiwan -#Server = http://archlinux.cs.nctu.edu.tw/$repo/os/$arch -#Server = http://shadow.ind.ntou.edu.tw/archlinux/$repo/os/$arch -#Server = http://ftp.tku.edu.tw/Linux/ArchLinux/$repo/os/$arch -#Server = http://ftp.yzu.edu.tw/Linux/archlinux/$repo/os/$arch - -## Thailand -#Server = http://mirror.adminbannok.com/archlinux/$repo/os/$arch -#Server = http://mirror.kku.ac.th/archlinux/$repo/os/$arch -#Server = https://mirror.kku.ac.th/archlinux/$repo/os/$arch - -## Turkey -#Server = http://ftp.linux.org.tr/archlinux/$repo/os/$arch - -## Ukraine -#Server = http://archlinux.ip-connect.vn.ua/$repo/os/$arch -#Server = https://archlinux.ip-connect.vn.ua/$repo/os/$arch -#Server = http://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch -#Server = https://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch - -## United Kingdom -#Server = http://mirror.bytemark.co.uk/archlinux/$repo/os/$arch -#Server = http://mirrors.manchester.m247.com/arch-linux/$repo/os/$arch -#Server = http://www.mirrorservice.org/sites/ftp.archlinux.org/$repo/os/$arch -#Server = http://arch.serverspace.co.uk/arch/$repo/os/$arch -#Server = http://archlinux.mirrors.uk2.net/$repo/os/$arch - -## United States -#Server = http://mirrors.acm.wpi.edu/archlinux/$repo/os/$arch -#Server = http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch -#Server = http://mirrors.aggregate.org/archlinux/$repo/os/$arch -#Server = http://ca.us.mirror.archlinux-br.org/$repo/os/$arch -#Server = http://il.us.mirror.archlinux-br.org/$repo/os/$arch -#Server = http://archlinux.surlyjake.com/archlinux/$repo/os/$arch -#Server = http://arlm.tyzoid.com/$repo/os/$arch -#Server = http://mirror.as65535.net/archlinux/$repo/os/$arch -#Server = http://mirrors.cat.pdx.edu/archlinux/$repo/os/$arch -#Server = http://mirror.cc.columbia.edu/pub/linux/archlinux/$repo/os/$arch -#Server = http://arch.mirror.constant.com/$repo/os/$arch -#Server = https://arch.mirror.constant.com/$repo/os/$arch -#Server = http://cosmos.cites.illinois.edu/pub/archlinux/$repo/os/$arch -#Server = http://mirror.cs.pitt.edu/archlinux/$repo/os/$arch -#Server = http://mirror.cs.vt.edu/pub/ArchLinux/$repo/os/$arch -#Server = http://mirror.epiphyte.network/archlinux/$repo/os/$arch -#Server = https://mirror.epiphyte.network/archlinux/$repo/os/$arch -#Server = http://mirror.es.its.nyu.edu/archlinux/$repo/os/$arch -#Server = http://mirrors.gigenet.com/archlinux/$repo/os/$arch -#Server = http://mirror.grig.io/archlinux/$repo/os/$arch -#Server = https://mirror.grig.io/archlinux/$repo/os/$arch -#Server = http://www.gtlib.gatech.edu/pub/archlinux/$repo/os/$arch -#Server = http://mirror1.hackingand.coffee/arch/$repo/os/$arch -#Server = http://mirror2.hackingand.coffee/arch/$repo/os/$arch -#Server = http://mirror3.hackingand.coffee/arch/$repo/os/$arch -#Server = http://mirror.htnshost.com/archlinux/$repo/os/$arch -#Server = http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch -#Server = http://mirrors.kernel.org/archlinux/$repo/os/$arch -#Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch -#Server = http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch -#Server = https://mirror.us.leaseweb.net/archlinux/$repo/os/$arch -#Server = http://il.mirrors.linaxe.net/archlinux/$repo/os/$arch -#Server = http://mirrors.liquidweb.com/archlinux/$repo/os/$arch -#Server = http://arch.localmsp.org/arch/$repo/os/$arch -#Server = https://arch.localmsp.org/arch/$repo/os/$arch -#Server = http://mirror.lty.me/archlinux/$repo/os/$arch -#Server = https://mirror.lty.me/archlinux/$repo/os/$arch -#Server = http://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch -#Server = https://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch -#Server = http://mirror.math.princeton.edu/pub/archlinux/$repo/os/$arch -#Server = http://mirror.metrocast.net/archlinux/$repo/os/$arch -#Server = http://mirror.kaminski.io/archlinux/$repo/os/$arch -#Server = https://mirror.kaminski.io/archlinux/$repo/os/$arch -#Server = http://mirror.nexcess.net/archlinux/$repo/os/$arch -#Server = http://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch -#Server = https://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch -#Server = http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch -#Server = http://arch.mirrors.pair.com/$repo/os/$arch -#Server = http://mirrors.rit.edu/archlinux/$repo/os/$arch -#Server = https://mirrors.rit.edu/archlinux/$repo/os/$arch -#Server = http://mirrors.rutgers.edu/archlinux/$repo/os/$arch -#Server = https://mirrors.rutgers.edu/archlinux/$repo/os/$arch -#Server = https://mirrors.tuxns.net/archlinux/$repo/os/$arch -#Server = http://mirror.umd.edu/archlinux/$repo/os/$arch -#Server = http://mirror.vtti.vt.edu/archlinux/$repo/os/$arch -#Server = http://mirrors.xmission.com/archlinux/$repo/os/$arch -#Server = http://mirror.yellowfiber.net/archlinux/$repo/os/$arch - -## Vietnam -#Server = http://f.archlinuxvn.org/archlinux/$repo/os/$arch -#Server = http://mirror-fpt-telecom.fpt.net/archlinux/$repo/os/$arch - diff --git a/gerboweb/deploy/nginx.conf b/gerboweb/deploy/nginx.conf deleted file mode 100644 index 744bd01..0000000 --- a/gerboweb/deploy/nginx.conf +++ /dev/null @@ -1,412 +0,0 @@ -# For more information on configuration, see: -# * Official English Documentation: http://nginx.org/en/docs/ -# * Official Russian Documentation: http://nginx.org/ru/docs/ - -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log; -pid /run/nginx.pid; - -# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. -include /usr/share/nginx/modules/*.conf; - -events { - worker_connections 1024; -} - -http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 4096; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Load modular configuration files from the /etc/nginx/conf.d directory. - # See http://nginx.org/en/docs/ngx_core_module.html#include - # for more information. - include /etc/nginx/conf.d/*.conf; - - server { - listen 80; - listen [::]:80; - server_name .jaseg.net; - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl http2 default_server; - listen [::]:443 ssl http2 default_server; - server_name gerbolyze.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/gerbolyze.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/gerbolyze.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location ^~ /static/ { - root /var/lib/gerboweb; - } - - location / { - include uwsgi_params; - uwsgi_pass unix:/run/uwsgi/gerboweb.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name blog.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/blog.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/blog.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - root /var/www/blog.jaseg.net; - } - - location /d/ { - access_log off; - log_not_found off; - rewrite ^/d/(.*)$ /$1 break; - include uwsgi_params; - uwsgi_pass unix:/run/uwsgi/secure-download.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name automation.jaseg.de; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/automation.jaseg.de/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/automation.jaseg.de/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - include uwsgi_params; - uwsgi_pass unix:/run/uwsgi/notification-proxy.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name kochbuch.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/kochbuch.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/kochbuch.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - auth_basic "blubb"; - auth_basic_user_file /etc/nginx/kochbuch.htpasswd; - root /var/www/kochbuch.jaseg.net; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name pogojig.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/pogojig.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/pogojig.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - client_max_body_size 10M; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location ^~ /pogospace/ { - root /var/lib/pogojig/pogospace; - } - - location / { - include uwsgi_params; - uwsgi_pass unix:/run/uwsgi/pogojig.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name tracespace.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/tracespace.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/tracespace.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - root /var/www/tracespace.jaseg.net; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name openjscad.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/openjscad.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/openjscad.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - root /var/www/openjscad.jaseg.net; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name git.jaseg.net; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/git.jaseg.net/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/git.jaseg.net/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location ~ ^/(cgit.css|robots.txt) { - root /usr/share/cgit; - expires 30d; - } - - location ~ ^/(cgit.png|favicon.png) { - alias /var/www/git.jaseg.net/cgit.png; - } - - location / { - include uwsgi_params; - uwsgi_modifier1 9; - uwsgi_pass unix:/run/uwsgi/cgit.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name dyndns.jaseg.de; - root /usr/share/nginx/html; - - ssl_certificate "/etc/letsencrypt/live/dyndns.jaseg.de/fullchain.pem"; - ssl_certificate_key "/etc/letsencrypt/live/dyndns.jaseg.de/privkey.pem"; - ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; - include /etc/letsencrypt/options-ssl-nginx.conf; - - ssl_stapling on; - ssl_stapling_verify on; - - resolver 67.207.67.2 67.207.67.3 valid=300s; - resolver_timeout 10s; - - add_header Strict-Transport-Security "max-age=86400"; - - # Load configuration files for the default server block. - include /etc/nginx/default.d/*.conf; - - location / { - include uwsgi_params; - uwsgi_pass unix:/run/uwsgi/dyndns.socket; - } - - error_page 404 /404.html; - location = /40x.html { - root /usr/share/nginx/html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } -} - diff --git a/gerboweb/deploy/nginx_nossl.conf b/gerboweb/deploy/nginx_nossl.conf deleted file mode 100644 index 8d5a5a5..0000000 --- a/gerboweb/deploy/nginx_nossl.conf +++ /dev/null @@ -1,52 +0,0 @@ -# For more information on configuration, see: -# * Official English Documentation: http://nginx.org/en/docs/ -# * Official Russian Documentation: http://nginx.org/ru/docs/ - -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log; -pid /run/nginx.pid; - -# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. -include /usr/share/nginx/modules/*.conf; - -events { - worker_connections 1024; -} - -http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 4096; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Load modular configuration files from the /etc/nginx/conf.d directory. - # See http://nginx.org/en/docs/ngx_core_module.html#include - # for more information. - include /etc/nginx/conf.d/*.conf; - - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name gerbolyze.jaseg.net; - return 301 https://$host$request_uri; - } - - server { - listen 80; - listen [::]:80; - server_name blog.jaseg.net; - return 301 https://$host$request_uri; - } -} - diff --git a/gerboweb/deploy/notification_proxy.py b/gerboweb/deploy/notification_proxy.py deleted file mode 100644 index 117f8e1..0000000 --- a/gerboweb/deploy/notification_proxy.py +++ /dev/null @@ -1,179 +0,0 @@ -import smtplib -import ssl -import email.utils -import hmac -from email.mime.text import MIMEText -from datetime import datetime -import time -import functools -import json -import binascii -import uwsgidecorators - -import sqlite3 - -from flask import Flask, request, abort - -app = Flask(__name__) -app.config.from_pyfile('config.py') - -db = sqlite3.connect(app.config['SQLITE_DB'], check_same_thread=False) -with db as conn: - conn.execute('''CREATE TABLE IF NOT EXISTS seqs_seen - (route_name TEXT PRIMARY KEY, - seq INTEGER)''') - conn.execute('''CREATE TABLE IF NOT EXISTS time_seen - (route_name TEXT PRIMARY KEY)''') - - conn.execute('''CREATE TABLE IF NOT EXISTS heartbeats_seen - (route_name TEXT PRIMARY KEY, - timestamp INTEGER, - notified INTEGER)''') - # Clear table on startup to avoid spurious notifications - conn.execute('''DELETE FROM heartbeats_seen''') - -mail_routes = {} - -def mail_route(name, receiver, secret): - def wrap(func): - global routes - mail_routes[name] = (receiver, func, secret) - return func - return wrap - - -def authenticate(route_name, secret, clock_delta_tolerance:'s'=120): - with db as conn: - if not request.is_json: - print('Rejecting notification: Incorrect content type') - abort(400) - - if not 'auth' in request.json and 'payload' in request.json: - print('Rejecting notification: signature or payload not found') - abort(400) - - if not isinstance(request.json['auth'], str): - print('Rejecting notification: signature is of incorrect type') - abort(400) - their_digest = binascii.unhexlify(request.json['auth']) - - our_digest = hmac.digest(secret.encode('utf-8'), request.json['payload'].encode('utf-8'), 'sha256') - if not hmac.compare_digest(their_digest, our_digest): - print('Rejecting notification: Incorrect signature') - abort(403) - - try: - payload = json.loads(request.json['payload']) - except: - print('Rejecting notification: Payload is not JSON') - abort(400) - - last_seqnum = conn.execute('SELECT seq FROM seqs_seen WHERE route_name = ?', (route_name,)).fetchone() or 0 - # We can check for seq here: Only an attacker with knowledge of the secret would be able to remove - # seq from a message. This means for a single key, only messages with or without seq may ever be used. - if 'seq' in payload: - seq = payload['seq'] - if not isinstance(seq, int): - print('Rejecting notification: seq of wrong type') - abort(400) - - if seq <= last_seqnum: - print('Rejecting notification: seq out of order') - abort(400) - - conn.execute('INSERT OR REPLACE INTO seqs_seen VALUES (?, ?)', (route_name, seq)) - - elif last_seqnum: - print('Rejecting notification: seq not included but past messages included seq') - abort(400) - - msg_time = None - if 'time' in payload: - msg_time = payload['time'] - if not isinstance(msg_time, int): - print('Rejecting notification: time of wrong type') - abort(400) - - if abs(msg_time - int(time.time())) > clock_delta_tolerance: - print('Rejecting notification: timestamp too far in the future or past') - abort(400) - - conn.execute('INSERT OR REPLACE INTO time_seen VALUES (?)', (route_name,)) - - elif conn.execute('SELECT * FROM time_seen WHERE route_name = ?', (route_name,)).fetchone(): - print('Rejecting notification: time not included but past messages included time') - abort(400) - - if msg_time is None: - msg_time = int(time.time()) - - return msg_time, payload['scope'], payload['d'] - -@mail_route('klingel', 'computerstuff@jaseg.de', app.config['SECRET_KLINGEL']) -def klingel(classification='somewhere', rms=None, capture=None, **kwargs): - return (f'It rang {classification}!', - f'rms={rms}\ncapture={capture}\nextra_args={kwargs}') - - -def send_mail(route_name, receiver, subject, body): - try: - context = ssl.create_default_context() - smtp = smtplib.SMTP_SSL(app.config['SMTP_HOST'], app.config['SMTP_PORT']) - smtp.login('apikey', app.config['SENDGRID_APIKEY']) - - sender = f'{route_name}@{app.config["DOMAIN"]}' - - msg = MIMEText(body) - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = receiver - msg['Date'] = email.utils.formatdate() - - smtp.sendmail(sender, receiver, msg.as_string()) - finally: - smtp.quit() - -@app.route('/v1/notify/', methods=['POST']) -def notify(route_name): - receiver, func, secret = mail_routes[route_name] - msg_time, scope, kwargs = authenticate(route_name, secret) - - if scope == 'default': - # Exceptions will yield a 500 error - subject, body = func(**kwargs) - send_mail(route_name, receiver, subject, body or 'empty message') - - elif scope == 'info': - send_mail(route_name, receiver, f'System info: {kwargs["info_msg"]}', f'Logged data: {kwargs}') - - elif scope == 'boot': - formatted = datetime.utcfromtimestamp(msg_time).isoformat() - send_mail(route_name, receiver, 'System startup', f'System powered up at {formatted}') - - elif scope == 'heartbeat': - with db as conn: - conn.execute('INSERT OR REPLACE INTO heartbeats_seen VALUES (?, ?, 0)', (route_name, int(time.time()))) - - elif scope == 'error': - print(f'Device error: {kwargs}') - - return 'success' - -@uwsgidecorators.timer(60) -def heartbeat_timer(_uwsgi_signum): - threshold = int(time.time()) - app.config['HEARTBEAT_TIMEOUT'] - with db as conn: - for route, ts in db.execute( - 'SELECT route_name, timestamp FROM heartbeats_seen WHERE timestamp <= ? AND notified == 0', - (threshold,)).fetchall(): - print(f'Heartbeat expired for {route}: {ts} < {threshold}') - - receiver, *_ = mail_routes[route] - last = datetime.utcfromtimestamp(ts).isoformat() - - send_mail(route, receiver, 'Heartbeat timeout', f'Last heartbeat at {last}') - db.execute('UPDATE heartbeats_seen SET notified = ? WHERE route_name = ?', (int(time.time()), route)) - -if __name__ == '__main__': - app.run() - diff --git a/gerboweb/deploy/notification_proxy_config.py.j2 b/gerboweb/deploy/notification_proxy_config.py.j2 deleted file mode 100644 index 2ecf571..0000000 --- a/gerboweb/deploy/notification_proxy_config.py.j2 +++ /dev/null @@ -1,9 +0,0 @@ - -SENDGRID_APIKEY = '{{lookup('file', 'notification_proxy_sendgrid_apikey.txt')}}' -DOMAIN = 'automation.jaseg.de' -SMTP_HOST = "smtp.sendgrid.net" -SMTP_PORT = 465 -HEARTBEAT_TIMEOUT = 300 -SQLITE_DB = '{{notification_proxy_sqlite_dbfile}}' - -SECRET_KLINGEL = '{{lookup('password', 'notification_proxy_klingel_secret.txt length=32')}}' diff --git a/gerboweb/deploy/playbook.yml b/gerboweb/deploy/playbook.yml deleted file mode 100644 index 6b1f46f..0000000 --- a/gerboweb/deploy/playbook.yml +++ /dev/null @@ -1,111 +0,0 @@ -- name: DNS setup - hosts: localhost - module_defaults: - inwx: - username: "{{lookup('ini', 'user section=inwx file=credentials.ini')}}" - password: "{{lookup('ini', 'pass section=inwx file=credentials.ini')}}" - vars: - subdomains: - - git.jaseg.net - - blog.jaseg.net - - kochbuch.jaseg.net - - gerbolyze.jaseg.net - - tracespace.jaseg.net - - openjscad.jaseg.net - - pogojig.jaseg.net - - automation.jaseg.de - - dyndns.jaseg.de - fastmail_domains: - - jaseg.net - - jaseg.de - tasks: - - name: Gather wendelstein facts - setup: - delegate_to: wendelstein - delegate_facts: True - - - name: Setup DNS - include_tasks: dns.yml - - -- name: Wendelstein setup - hosts: wendelstein - tasks: - - name: Set hostname - hostname: - name: wendelstein.jaseg.net - - - name: Install common admin tools - dnf: - name: htop,tmux,fish,mosh,neovim,sqlite - state: latest - - - name: Install host requisites - dnf: - name: nginx,uwsgi,python3-flask,python3-flask-wtf,uwsgi-plugin-python3,certbot,python3-certbot-nginx,libselinux-python,git,iptables-services,python3-pycryptodomex,zip,python3-uwsgidecorators,nsd - state: latest - - - name: Disable password-based root login - lineinfile: - path: /etc/ssh/sshd_config - regexp: '^PermitRootLogin' - line: 'PermitRootLogin without-password' - register: disable_root_pw_ssh - - - name: Restart sshd - systemd: - name: sshd - state: restarted - when: disable_root_pw_ssh is changed - - - name: Configure iptables firewall service - copy: - src: iptables.rules - dest: /etc/sysconfig/iptables - owner: root - group: root - mode: 0664 - - - name: Enable iptables firewall service - systemd: - name: iptables - enabled: yes - state: started - - - name: Create containers - include_tasks: setup_containers.yml - vars: - containers: - - gerboweb - - clippy - - pogojig - - - name: Setup web server - include_tasks: setup_webserver.yml - - - name: Setup gerboweb - include_tasks: setup_gerboweb.yml - - - name: Setup clippy - include_tasks: setup_clippy.yml - - - name: Setup secure download - include_tasks: setup_secure_download.yml - - - name: Setup tracespace - include_tasks: setup_tracespace.yml - - - name: Setup openjscad - include_tasks: setup_openjscad.yml - - - name: Setup pogojig - include_tasks: setup_pogojig.yml - - - name: Setup notification proxy - include_tasks: setup_notification_proxy.yml - - - name: Setup semi-public git server - include_tasks: setup_git.yml - - - name: Setup private DynDNS service - include_tasks: setup_dyndns.yml diff --git a/gerboweb/deploy/pogojig-job-processor.service.j2 b/gerboweb/deploy/pogojig-job-processor.service.j2 deleted file mode 100644 index 5ca9a8b..0000000 --- a/gerboweb/deploy/pogojig-job-processor.service.j2 +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Pogojig render job processor - -[Service] -WorkingDirectory=/var/lib/pogojig -ExecStart=/usr/bin/python3 job_processor.py {{pogojig_cache}}/job_queue.sqlite3 - -[Install] -WantedBy=uwsgi-app@pogojig.service diff --git a/gerboweb/deploy/pogojig.cfg.j2 b/gerboweb/deploy/pogojig.cfg.j2 deleted file mode 100644 index 3dd7160..0000000 --- a/gerboweb/deploy/pogojig.cfg.j2 +++ /dev/null @@ -1,4 +0,0 @@ -MAX_CONTENT_LENGTH=10000000 -SECRET_KEY="{{lookup('password', 'pogojig_flask_secret.txt length=32')}}" -UPLOAD_PATH="{{pogojig_cache}}/upload" -JOB_QUEUE_DB="{{pogojig_cache}}/job_queue.sqlite3" diff --git a/gerboweb/deploy/pogojig_generate.sh.j2 b/gerboweb/deploy/pogojig_generate.sh.j2 deleted file mode 100755 index c1cc023..0000000 --- a/gerboweb/deploy/pogojig_generate.sh.j2 +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -[ $# != 1 ] && exit 1 -ID=$1 -egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2 - -systemd-nspawn \ - -D {{pogojig_root}} \ - -x --bind={{pogojig_cache}}/upload/$ID:/mnt \ - /bin/sh -c "set -euo pipefail -cd /mnt - -date; echo 'Cleaning up previous output' -rm -rf pcb_shape.dxf jig.stl kicad kicad.zip sources.zip - -date; echo 'Rendering' -cp -r /var/lib/pogojig_renderer sources -cp input.svg sources/ -make -C sources - -date; echo 'Packing source bundle' -cp -r sources/out/pcb_shape.dxf sources/out/jig.stl sources/out/kicad ./ -zip -r sources.zip sources -zip -r kicad.zip kicad -rm -rf sources" diff --git a/gerboweb/deploy/render.sh.j2 b/gerboweb/deploy/render.sh.j2 deleted file mode 100755 index ceb837d..0000000 --- a/gerboweb/deploy/render.sh.j2 +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -[ $# != 1 ] && exit 1 -ID=$1 -egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2 - -systemd-nspawn \ - -D {{gerboweb_root}} \ - -x --bind={{gerboweb_cache}}/upload/$ID:/mnt \ - /bin/sh -c "set -euo pipefail -unzip -j -d /tmp/gerber /mnt/gerber.zip -rm -f /mnt/render_top.png /mnt/render_bottom.png /mnt/render_top.small.png /mnt/render_bottom.small.png -date; echo 'Rendering bottom layer' -gerbolyze render top /tmp/gerber /mnt/render_top.png -date; echo 'Scaling down' -convert /mnt/render_top.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_top.small.png -date; echo 'Rendering top layer' -gerbolyze render bottom /tmp/gerber /mnt/render_bottom.png -date; echo 'Scaling down' -convert /mnt/render_bottom.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_bottom.small.png" diff --git a/gerboweb/deploy/secure_download.cfg.j2 b/gerboweb/deploy/secure_download.cfg.j2 deleted file mode 100644 index 36d86c1..0000000 --- a/gerboweb/deploy/secure_download.cfg.j2 +++ /dev/null @@ -1 +0,0 @@ -SERVE_PATH="{{secure_download_dir}}" diff --git a/gerboweb/deploy/setup_clippy.yml b/gerboweb/deploy/setup_clippy.yml deleted file mode 100644 index 26142b6..0000000 --- a/gerboweb/deploy/setup_clippy.yml +++ /dev/null @@ -1,85 +0,0 @@ ---- -- name: Clone pixelterm git - git: - repo: https://github.com/jaseg/pixelterm - dest: "{{clippy_root}}/var/lib/pixelterm.git" - -- name: Clone clippy git - git: - repo: https://github.com/jaseg/clippy - dest: "{{clippy_root}}/var/lib/clippy.git" - -- name: Setup required packages for clippy - command: arch-chroot "{{clippy_root}}" pacman -Syu --noconfirm python3 python-pip python-numpy python-pillow - -- name: Setup pixelterm - command: arch-chroot "{{clippy_root}}" sh -c "cd /var/lib/pixelterm.git && python3 setup.py install" - -- name: Setup container clippy systemd service file - template: - src: clippy.service.j2 - dest: "{{clippy_root}}/etc/systemd/system/clippy.service" - owner: root - group: root - mode: 0664 - -- name: Enable systemd machines target - systemd: - name: machines.target - enabled: yes - -- name: Copy over clippy container auto boot service file - copy: - src: clippy-nspawn.service - dest: /etc/systemd/system/clippy-nspawn.service - owner: root - group: root - mode: 0664 - -- name: Create systemd-nspawn config dir - file: - path: /etc/systemd/nspawn - state: directory - owner: root - group: root - mode: 0775 - -- name: Copy over clippy container config - copy: - src: clippy.nspawn - dest: /etc/systemd/nspawn/clippy.nspawn - owner: root - group: root - mode: 0664 - -- name: Enable clippy container auto boot - systemd: - daemon-reload: yes - name: clippy-nspawn.service - enabled: yes - -- name: Restart clippy container - shell: | - systemctl stop clippy-nspawn - sleep 1 - systemctl start clippy-nspawn - for x in $(seq 0 30); do - systemctl -M clippy is-system-running && exit - sleep 1 - done - -- name: Enable clippy systemd service in container - command: systemctl enable -M clippy clippy.service - -- name: Restart clippy systemd service in container - command: systemctl restart -M clippy clippy.service - -#- name: Enable host networkd -# systemd: -# name: systemd-networkd -# enabled: yes -# state: started - -#- name: Enable clippy container networkd -# command: systemctl enable -M clippy systemd-networkd - diff --git a/gerboweb/deploy/setup_containers.yml b/gerboweb/deploy/setup_containers.yml deleted file mode 100644 index 4738f1e..0000000 --- a/gerboweb/deploy/setup_containers.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -- name: Install host requisites - dnf: - name: btrfs-progs,arch-install-scripts,systemd-container,libselinux-python - state: latest - -- name: Create individual containers - include_tasks: bootstrap_arch_container.yml - with_items: "{{ containers }}" - loop_control: - loop_var: container - -- name: Cleanup bootstrap image - file: - path: /tmp/arch-bootstrap.tar.xz - state: absent - diff --git a/gerboweb/deploy/setup_gerboweb.yml b/gerboweb/deploy/setup_gerboweb.yml deleted file mode 100644 index 6a20eed..0000000 --- a/gerboweb/deploy/setup_gerboweb.yml +++ /dev/null @@ -1,100 +0,0 @@ ---- -- name: Set local facts - set_fact: - gerboweb_cache: /var/cache/gerboweb - -- name: Copy render script - template: - src: render.sh.j2 - dest: /usr/local/sbin/gerbolyze_render.sh - mode: ug+x - -- name: Copy vector script - template: - src: vector.sh.j2 - dest: /usr/local/sbin/gerbolyze_vector.sh - mode: ug+x - -- name: Install packages into gerbolyze container - shell: arch-chroot "{{gerboweb_root}}" pacman -Syu --noconfirm python3 opencv hdf5 gtk3 python-numpy python-pip imagemagick unzip zip - -- name: Workaround for cairoffi problem - shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager wheel - - # TODO maybe install directly from local git checkout? -- name: Install gerbolyze - shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager gerbolyze - -- name: Copy webapp sources - synchronize: - # FIXME: make this path configurable - src: ~/gerbolyze/gerboweb/ - dest: /var/lib/gerboweb/ - rsync_opts: - - "--exclude=/deploy" - group: no - owner: no - -- name: Create uwsgi worker user and group - user: - name: uwsgi-gerboweb - create_home: no - group: uwsgi - password: '!' - shell: /sbin/nologin - system: yes - -- name: Template webapp config - template: - src: gerboweb.cfg.j2 - dest: /var/lib/gerboweb/gerboweb_prod.cfg - owner: uwsgi-gerboweb - group: root - mode: 0660 - -- name: Copy uwsgi config - copy: - src: uwsgi-gerboweb.ini - dest: /etc/uwsgi.d/gerboweb.ini - owner: uwsgi-gerboweb - group: uwsgi - mode: 0440 - -- name: Copy job processor systemd service config - template: - src: gerboweb-job-processor.service.j2 - dest: /etc/systemd/system/gerboweb-job-processor.service - -- name: Enable uwsgi systemd socket - systemd: - daemon-reload: yes - name: uwsgi-app@gerboweb.socket - enabled: yes - -- name: Copy gerboweb cache dir tmpfiles.d config - template: - src: tmpfiles-gerboweb.conf.j2 - dest: /etc/tmpfiles.d/gerboweb.conf - owner: root - group: root - mode: 0644 - register: tmpfiles_config - -- name: Kick systemd tmpfiles service to create cache dir - command: systemd-tmpfiles --create - when: tmpfiles_config is changed - -- name: Create job queue db - file: - path: "{{gerboweb_cache}}/job_queue.sqlite3" - owner: root - group: uwsgi - mode: 0660 - state: touch - -- name: Enable and launch job processor - systemd: - name: gerboweb-job-processor.service - enabled: yes - state: restarted - diff --git a/gerboweb/deploy/setup_git.yml b/gerboweb/deploy/setup_git.yml deleted file mode 100644 index 9d351e5..0000000 --- a/gerboweb/deploy/setup_git.yml +++ /dev/null @@ -1,115 +0,0 @@ -- name: Install host requisites - dnf: - name: cgit,gitolite3,python3-pygments,python3-docutils,nodejs-markdown - state: latest - -- name: Copy cgit favicon - copy: - src: cgit-logo.png - dest: /var/www/git.jaseg.net/cgit.png - -- name: Create cgit instance config dir - file: - path: /var/lib/cgit - state: directory - mode: 0755 - -- name: Copy cgit rc - copy: - src: cgitrc - dest: /var/lib/cgit/cgitrc-gitolite-public - mode: 0644 - -- name: Create uwsgi worker user and group - user: - name: uwsgi-cgit - create_home: no - group: uwsgi - password: '!' - shell: /sbin/nologin - system: yes - -- name: Copy uwsgi config - copy: - src: uwsgi-cgit.ini - dest: /etc/uwsgi.d/cgit.ini - owner: uwsgi-cgit - group: uwsgi - mode: 0440 - -- name: Enable uwsgi systemd socket - systemd: - daemon-reload: yes - name: uwsgi-app@cgit.socket - enabled: yes - -- name: Copy gitolite admin pubkey - copy: - src: ~/.ssh/id_ed25519.gitolite.pub - dest: /tmp/jaseg-gitolite.pub - owner: gitolite3 - group: gitolite3 - -- name: Run gitolite initialization - command: gitolite setup -pk /tmp/jaseg-gitolite.pub - become: true - become_method: su - become_user: gitolite3 - become_flags: '-s /bin/sh' - args: - creates: /var/lib/gitolite3/projects.list - -- name: Remove leftover admin pubkey - file: - state: absent - path: /tmp/jaseg-gitolite.pub - -- name: Allow uwsgi group to access gitolite repo dir - file: - path: /var/lib/gitolite3 - state: directory - owner: gitolite3 - group: uwsgi - -- name: Add cgit uwsgi user to gitolite group - user: - name: uwsgi-cgit - groups: gitolite3 - append: yes - -- name: Allow cgit uwsgi user to access gitolite repos - file: - path: /var/lib/gitolite3/repositories - mode: 0750 - -- name: Allow cgit uwsgi user to gitolite repo list - file: - path: /var/lib/gitolite3/projects.list - mode: 0640 - -- name: Copy gitolite rc - copy: - src: gitolite.rc - dest: /var/lib/gitolite3/.gitolite.rc - owner: gitolite3 - group: gitolite3 - mode: 0600 - -- name: Query system user account info - getent: - database: passwd - key: gitolite3 - -- name: Create git alias user - user: - name: git - create_home: no - group: gitolite3 - password: '!' - comment: Alias for gitolite3 user - shell: "{{ getent_passwd['gitolite3'][5] }}" - system: yes - non_unique: yes - home: "{{ getent_passwd['gitolite3'][4] }}" - uid: "{{ getent_passwd['gitolite3'][1] }}" - diff --git a/gerboweb/deploy/setup_notification_proxy.yml b/gerboweb/deploy/setup_notification_proxy.yml deleted file mode 100644 index b47af05..0000000 --- a/gerboweb/deploy/setup_notification_proxy.yml +++ /dev/null @@ -1,61 +0,0 @@ ---- -- name: Set local facts - set_fact: - notification_proxy_sqlite_dbfile: /var/lib/notification-proxy/db.sqlite3 - -- name: Create notification proxy worker user and group - user: - name: uwsgi-notification-proxy - create_home: no - group: uwsgi - password: '!' - shell: /sbin/nologin - system: yes - -- name: Create webapp dir - file: - path: /var/lib/notification-proxy - state: directory - owner: uwsgi-notification-proxy - group: uwsgi - mode: 0750 - -- name: Copy webapp sources - copy: - src: notification_proxy.py - dest: /var/lib/notification-proxy/ - owner: uwsgi-notification-proxy - group: uwsgi - mode: 0440 - -- name: Template webapp config - template: - src: notification_proxy_config.py.j2 - dest: /var/lib/notification-proxy/config.py - owner: uwsgi-notification-proxy - group: root - mode: 0660 - -- name: Copy uwsgi config - copy: - src: uwsgi-notification-proxy.ini - dest: /etc/uwsgi.d/notification-proxy.ini - owner: uwsgi-notification-proxy - group: uwsgi - mode: 0440 - -- name: Enable uwsgi systemd socket - systemd: - daemon-reload: yes - name: uwsgi-app@notification-proxy.socket - enabled: yes - -- name: Create sqlite db file - file: - path: "{{notification_proxy_sqlite_dbfile}}" - owner: uwsgi-notification-proxy - group: uwsgi - mode: 0660 - state: touch - - diff --git a/gerboweb/deploy/setup_openjscad.yml b/gerboweb/deploy/setup_openjscad.yml deleted file mode 100644 index dea4ad2..0000000 --- a/gerboweb/deploy/setup_openjscad.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: Copy openjscad webapp sources - synchronize: - # FIXME: make this path configurable - src: ~/openjscad_dist/ - dest: /var/www/openjscad.jaseg.net/ - group: no - owner: no - diff --git a/gerboweb/deploy/setup_pogojig.yml b/gerboweb/deploy/setup_pogojig.yml deleted file mode 100644 index cf49fbe..0000000 --- a/gerboweb/deploy/setup_pogojig.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -- name: Set local facts - set_fact: - pogojig_cache: /var/cache/pogojig - -- name: Copy render script - template: - src: pogojig_generate.sh.j2 - dest: /usr/local/sbin/pogojig_generate.sh - mode: ug+x - -- name: Install packages into pogojig container - shell: arch-chroot "{{pogojig_root}}" pacman -Syu --noconfirm python3 python-pip imagemagick unzip zip openscad inkscape make python-lxml xorg-server-xvfb - -- name: Install python dependencies into pogojig container - shell: arch-chroot "{{pogojig_root}}" pip install -U --upgrade-strategy=eager ezdxf xvfbwrapper - -- name: Install pogojig - synchronize: - # FIXME: make this path configurable - src: checkouts/pogojig/renderer/ - dest: "{{pogojig_root}}/var/lib/pogojig_renderer" - group: no - -- name: Copy webapp sources - synchronize: - # FIXME: make this path configurable - src: checkouts/pogojig/webapp/ - dest: /var/lib/pogojig - delete: true - group: no - owner: no - -- name: Pack makefile template zip - archive: - path: "{{pogojig_root}}/var/lib/pogojig_renderer" - dest: /var/lib/pogojig/static/pogojig_makefile_template.zip - format: zip - -- name: Create web home for modified tracespace - file: - path: /var/lib/pogojig/pogospace - state: directory - owner: nginx - group: nginx - mode: 0550 - -- name: Unpack modified tracespace sources - unarchive: - src: resource/pogojig-tracespace.tar.gz - dest: /var/lib/pogojig/pogospace - extra_opts: [--strip-components=1] - owner: nginx - group: nginx - -- name: Create uwsgi worker user and group - user: - name: uwsgi-pogojig - create_home: no - group: uwsgi - password: '!' - shell: /sbin/nologin - system: yes - -- name: Template webapp config - template: - src: pogojig.cfg.j2 - dest: /var/lib/pogojig/pogojig_prod.cfg - owner: uwsgi-pogojig - group: root - mode: 0660 - -- name: Copy uwsgi config - copy: - src: uwsgi-pogojig.ini - dest: /etc/uwsgi.d/pogojig.ini - owner: uwsgi-pogojig - group: uwsgi - mode: 440 - -- name: Copy job processor systemd service config - template: - src: pogojig-job-processor.service.j2 - dest: /etc/systemd/system/pogojig-job-processor.service - -- name: Enable uwsgi systemd socket - systemd: - daemon-reload: yes - name: uwsgi-app@pogojig.socket - enabled: yes - -# FIXME the socket doesn't seem to work properly -- name: Enable uwsgi systemd service - systemd: - daemon-reload: yes - name: uwsgi-app@pogojig.service - enabled: yes - -- name: Copy pogojig cache dir tmpfiles.d config - template: - src: tmpfiles-pogojig.conf.j2 - dest: /etc/tmpfiles.d/pogojig.conf - owner: root - group: root - mode: 0644 - register: pogojig_tmpfiles_config - -- name: Kick systemd tmpfiles service to create cache dir - command: systemd-tmpfiles --create - when: pogojig_tmpfiles_config is changed - -- name: Create job queue db - file: - path: "{{pogojig_cache}}/job_queue.sqlite3" - owner: root - group: uwsgi - mode: 0660 - state: touch - -- name: Enable and launch job processor - systemd: - name: pogojig-job-processor.service - enabled: yes - state: restarted - diff --git a/gerboweb/deploy/setup_secure_download.yml b/gerboweb/deploy/setup_secure_download.yml deleted file mode 100644 index aa94a53..0000000 --- a/gerboweb/deploy/setup_secure_download.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -- name: Set local facts - set_fact: - secure_download_dir: /var/cache/secure_download - -- name: Copy webapp sources - synchronize: - # FIXME: make this path configurable - src: ~/secure_download/ - dest: /var/lib/secure_download/ - group: no - owner: no - -- name: Create secure download worker user and group - user: - name: uwsgi-secure-download - create_home: no - group: uwsgi - password: '!' - shell: /sbin/nologin - system: yes - -- name: Template webapp config - template: - src: secure_download.cfg.j2 - dest: /var/lib/secure_download/secure_download_prod.cfg - owner: uwsgi-secure-download - group: root - mode: 0660 - -- name: Copy uwsgi config - copy: - src: uwsgi-secure-download.ini - dest: /etc/uwsgi.d/secure-download.ini - owner: uwsgi-secure-download - group: uwsgi - mode: 440 - -- name: Enable uwsgi systemd socket - systemd: - daemon-reload: yes - name: uwsgi-app@secure-download.socket - enabled: yes - -- name: Copy server dir tmpfiles.d config - template: - src: tmpfiles-secure-download.conf.j2 - dest: /etc/tmpfiles.d/secure-download.conf - owner: root - group: root - mode: 0644 - register: sec_dl_tmpfiles_config - -- name: Kick systemd tmpfiles service to create serve dir - command: systemd-tmpfiles --create - when: sec_dl_tmpfiles_config is changed - diff --git a/gerboweb/deploy/setup_tracespace.yml b/gerboweb/deploy/setup_tracespace.yml deleted file mode 100644 index 2975967..0000000 --- a/gerboweb/deploy/setup_tracespace.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: Copy tracespace webapp sources - synchronize: - # FIXME: make this path configurable - src: ~/tracespace_dist/ - dest: /var/www/tracespace.jaseg.net/ - group: no - owner: no - diff --git a/gerboweb/deploy/setup_webserver.yml b/gerboweb/deploy/setup_webserver.yml deleted file mode 100644 index eb34a5b..0000000 --- a/gerboweb/deploy/setup_webserver.yml +++ /dev/null @@ -1,77 +0,0 @@ -- name: Copy first stage nginx config - copy: - src: nginx_nossl.conf - dest: /etc/nginx/nginx.conf - -- name: Add nginx user to uwsgi group for access to uwsgi socket - user: - name: nginx - groups: uwsgi - append: yes - -- name: Create subdomain content dirs - file: - path: /var/www/{{item}} - state: directory - owner: nginx - group: nginx - mode: 0550 - loop: - - git.jaseg.net - - blog.jaseg.net - - kochbuch.jaseg.net - - tracespace.jaseg.net - - openjscad.jaseg.net - - automation.jaseg.de - -- name: Copy uwsgi systemd socket config - copy: - src: uwsgi-app@.socket - dest: /etc/systemd/system/ - -- name: Copy uwsgi systemd service config - copy: - src: uwsgi-app@.service - dest: /etc/systemd/system/ - -- name: Set SELinux to permissive mode # FIXME this is to let nginx talk to uwsgi - selinux: - state: permissive - policy: targeted - -- name: Enable and launch nginx systemd service - systemd: - name: nginx.service - enabled: yes - state: restarted - -- name: Create subdomain letsencrypt certificates - command: certbot --nginx certonly -d {{item}} -n --agree-tos --email {{item}}-letsencrypt@jaseg.net - args: - creates: /etc/letsencrypt/live/{{item}}/fullchain.pem - loop: - - git.jaseg.net - - blog.jaseg.net - - kochbuch.jaseg.net - - gerbolyze.jaseg.net - - tracespace.jaseg.net - - openjscad.jaseg.net - - pogojig.jaseg.net - - automation.jaseg.de - - dyndns.jaseg.de - -- name: Copy final nginx config - copy: - src: nginx.conf - dest: /etc/nginx/nginx.conf - -- name: Restart nginx to load new cert - systemd: - name: nginx.service - state: restarted - -- name: Enable certbot renewal timer - systemd: - name: certbot-renew.timer - enabled: yes - diff --git a/gerboweb/deploy/tmpfiles-gerboweb.conf.j2 b/gerboweb/deploy/tmpfiles-gerboweb.conf.j2 deleted file mode 100644 index 18469b7..0000000 --- a/gerboweb/deploy/tmpfiles-gerboweb.conf.j2 +++ /dev/null @@ -1 +0,0 @@ -d {{gerboweb_cache}} 770 uwsgi-gerboweb uwsgi 2d diff --git a/gerboweb/deploy/tmpfiles-pogojig.conf.j2 b/gerboweb/deploy/tmpfiles-pogojig.conf.j2 deleted file mode 100644 index 4e9fef1..0000000 --- a/gerboweb/deploy/tmpfiles-pogojig.conf.j2 +++ /dev/null @@ -1 +0,0 @@ -d {{pogojig_cache}} 770 uwsgi-pogojig uwsgi 2d diff --git a/gerboweb/deploy/tmpfiles-secure-download.conf.j2 b/gerboweb/deploy/tmpfiles-secure-download.conf.j2 deleted file mode 100644 index 84d7add..0000000 --- a/gerboweb/deploy/tmpfiles-secure-download.conf.j2 +++ /dev/null @@ -1 +0,0 @@ -d {{secure_download_dir}} 770 uwsgi-download uwsgi 45d diff --git a/gerboweb/deploy/uwsgi-app@.service b/gerboweb/deploy/uwsgi-app@.service deleted file mode 100644 index bdae8fd..0000000 --- a/gerboweb/deploy/uwsgi-app@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=%i uWSGI app -After=syslog.target - -[Service] -ExecStart=/usr/sbin/uwsgi \ - --ini /etc/uwsgi.d/%i.ini \ - --chmod-socket=660 \ - --socket=/run/uwsgi/%i.socket -User=uwsgi-%i -Group=uwsgi -Restart=on-failure -KillSignal=SIGQUIT -Type=notify -StandardError=syslog -NotifyAccess=all diff --git a/gerboweb/deploy/uwsgi-app@.socket b/gerboweb/deploy/uwsgi-app@.socket deleted file mode 100644 index ae06d71..0000000 --- a/gerboweb/deploy/uwsgi-app@.socket +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Socket for uWSGI app %i - -[Socket] -ListenStream=/run/uwsgi/%i.socket -SocketUser=uwsgi-%i -SocketGroup=nginx -SocketMode=0660 - -[Install] -WantedBy=sockets.target diff --git a/gerboweb/deploy/uwsgi-cgit.ini b/gerboweb/deploy/uwsgi-cgit.ini deleted file mode 100644 index 9a10350..0000000 --- a/gerboweb/deploy/uwsgi-cgit.ini +++ /dev/null @@ -1,8 +0,0 @@ -[uwsgi] -master = True -plugins = cgi -chdir = /var/lib/gitolite3 -processes = 1 -threads = 2 -cgi = /var/www/cgi-bin/cgit -env = CGIT_CONFIG=/var/lib/cgit/cgitrc-gitolite-public diff --git a/gerboweb/deploy/uwsgi-gerboweb.ini b/gerboweb/deploy/uwsgi-gerboweb.ini deleted file mode 100644 index 155d01a..0000000 --- a/gerboweb/deploy/uwsgi-gerboweb.ini +++ /dev/null @@ -1,10 +0,0 @@ -[uwsgi] -master = True -cheap = True -die-on-idle = False -manage-script-name = True -plugins = python3 -chdir = /var/lib/gerboweb -mount = /=gerboweb:app -env = GERBOWEB_SETTINGS=gerboweb_prod.cfg - diff --git a/gerboweb/deploy/uwsgi-notification-proxy.ini b/gerboweb/deploy/uwsgi-notification-proxy.ini deleted file mode 100644 index aab2b5a..0000000 --- a/gerboweb/deploy/uwsgi-notification-proxy.ini +++ /dev/null @@ -1,10 +0,0 @@ -[uwsgi] -master = True -cheap = True -die-on-idle = False -manage-script-name = True -log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core)) -plugins = python3 -chdir = /var/lib/notification-proxy -mount = /=notification_proxy:app - diff --git a/gerboweb/deploy/uwsgi-pogojig.ini b/gerboweb/deploy/uwsgi-pogojig.ini deleted file mode 100644 index 003702d..0000000 --- a/gerboweb/deploy/uwsgi-pogojig.ini +++ /dev/null @@ -1,10 +0,0 @@ -[uwsgi] -master = True -cheap = True -die-on-idle = False -manage-script-name = True -plugins = python3 -chdir = /var/lib/pogojig -mount = /=pogojig:app -env = POGOJIG_SETTINGS=pogojig_prod.cfg - diff --git a/gerboweb/deploy/uwsgi-secure-download.ini b/gerboweb/deploy/uwsgi-secure-download.ini deleted file mode 100644 index 4a4aa65..0000000 --- a/gerboweb/deploy/uwsgi-secure-download.ini +++ /dev/null @@ -1,11 +0,0 @@ -[uwsgi] -master = True -cheap = True -die-on-idle = False -manage-script-name = True -log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core)) -plugins = python3 -chdir = /var/lib/secure_download -mount = /=server:app -env = SECURE_DOWNLOAD_SETTINGS=secure_download_prod.cfg - diff --git a/gerboweb/deploy/vector.sh.j2 b/gerboweb/deploy/vector.sh.j2 deleted file mode 100755 index b17116e..0000000 --- a/gerboweb/deploy/vector.sh.j2 +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -[ $# != 2 ] && exit 1 -ID=$1 -egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2 -LAYER=$2 -egrep -x -q '^(top|bottom)$'<<<"$LAYER" || exit 2 - -systemd-nspawn \ - -D {{gerboweb_root}} \ - -x --bind={{gerboweb_cache}}/upload/$ID:/mnt \ - /bin/sh -c "set -euo pipefail -cd /tmp -unzip -j -d gerber_in /mnt/gerber.zip -gerbolyze vectorize $LAYER gerber_in gerber /mnt/overlay.png -rm -f /mnt/gerber_out.zip -zip -r /mnt/gerber_out.zip gerber" - diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 2633e7b..c227e63 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -72,7 +72,7 @@ def index(): r = make_response(render_template('index.html', has_renders = path.isfile(tempfile_path('gerber.zip')), - has_output = path.isfile(tempfile_path('overlay.png')), + has_output = path.isfile(tempfile_path('overlay.svg')), **forms)) if 'vector_job' in session or 'render_job' in session: r.headers.set('refresh', '10') @@ -108,7 +108,7 @@ def upload_gerber(): session['filename'] = secure_filename(f.filename) # Cache filename for later download render() - if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change + if path.isfile(tempfile_path('overlay.svg')): # Re-vectorize when gerbers change vectorize() flash(f'Gerber file successfully uploaded.', 'success') @@ -121,7 +121,7 @@ def upload_overlay(): if upload_form.validate_on_submit(): # FIXME raise error when no side selected f = upload_form.upload_file.data - f.save(tempfile_path('overlay.png')) + f.save(tempfile_path('overlay.svg')) session['side_selected'] = upload_form.side.data vectorize() @@ -133,7 +133,7 @@ def upload_overlay(): def render_preview(side): if not side in ('top', 'bottom'): return abort(400, 'side must be either "top" or "bottom"') - return send_file(tempfile_path(f'render_{side}.small.png')) + return send_file(tempfile_path(f'template_{side}.preview.png')) @app.route('/render/download/') def render_download(side): @@ -141,10 +141,10 @@ def render_download(side): return abort(400, 'side must be either "top" or "bottom"') session['last_download'] = side - return send_file(tempfile_path(f'render_{side}.png'), - mimetype='image/png', + return send_file(tempfile_path(f'template_{side}.svg'), + mimetype='image/svg', as_attachment=True, - attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png') + attachment_filename=f'{path.splitext(session["filename"])[0]}_template_{side}.svg') @app.route('/output/download') def output_download(): diff --git a/gerboweb/static/sample2.jpg b/gerboweb/static/sample2.jpg index ef47bd4..ba8e984 100644 Binary files a/gerboweb/static/sample2.jpg and b/gerboweb/static/sample2.jpg differ diff --git a/gerboweb/templates/index.html b/gerboweb/templates/index.html index a19fc88..3895185 100644 --- a/gerboweb/templates/index.html +++ b/gerboweb/templates/index.html @@ -1,7 +1,7 @@ - Gerbolyze Raster image to PCB renderer + Gerbolyze Image to PCB Toolchain @@ -10,157 +10,159 @@
-
-

Raster image to PCB converter

-

- Gerbolyze is a tool for rendering black and white raster (PNG) images directly onto gerber layers. You can - use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is a black-and-white PNG - image that is vectorized and rendered into an existing gerber file. Gerbolyze works with gerber files - produced with any EDA toolchain and has been tested to work with both Altium and KiCAD. -

-
+
+

SVG/JPG/PNG to PCB converter

+

+ This is the toy web frontend to Gerbolyze. + + Gerbolyze is a tool for rendering arbitrary vector (SVG) and raster (PNG/JPG) images directly onto gerber layers. + You can use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is an SVG file + generated from a template. This SVG file has one layer for each PCB layer and the layers are rendered one by one + into the existing gerber file. SVG primitives are converted as-is with (almost) full SVG support, and bitmap + images are vectorized using a vector halftone processor. Gerbolyze works with gerber files produced with any EDA + toolchain and has been tested to work with both Altium and KiCAD. +

+
{% with messages = get_flashed_messages(with_categories=True) %} - {% if messages %} -
- {% for category, message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} {% endwith %}
{{reset_form.csrf_token}}
-
-
-

Upload zipped gerber files

-

- First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle - and Altium are supported. -

-
+
+
+

Upload zipped gerber files

+

+ First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle + and Altium are supported. +

+
-
-
- {{gerber_form.csrf_token}} -
-
-
Upload Gerber file:
- -
-
- - -
-
-
+
+
+ {{gerber_form.csrf_token}} +
+
+
Upload Gerber file:
+ +
+
+ + +
+
+
- {% if 'render_job' in session or has_renders %} -
-
-

Download the target side's preview image

-

- Second, download either the top or bottom preview image and use it to align and scale your own artwork - in an image editing program such as Gimp. Then upload your overlay image below. + {% if 'render_job' in session or has_renders %} +

+
+

Download the target side's preview image

+

+ Second, download either the top or bottom SVG template and place your own artwork in it on the appropriate + layers. The template is made to work well with the excellent open-source Inkscape + vector graphics editor. When you are done, upload your overlay below. - Note that you will have to convert grayscale images into binary images yourself. Gerbolyze can't do this - for you since there are lots of variables involved. Our Guideline on image processing gives an overview on - one way to produce agreeable binary images from grayscale source material. -

-
-
- {% if 'render_job' in session %} -
-
-
Processing...
-
(this may take several minutes!)
-
- {% else %} - - {% endif %} -
- -
-
-
+ If you wish to put a bitmap image (PNG/JPG) on your board, simply place it into the SVG on the appropriate + layer. Make sure you select Inkscape's "embed image" option when importing it. +

+
+
+ {% if 'render_job' in session %} +
+
+
Processing...
+
(this may take several minutes!)
+
+ {% else %} + + {% endif %} +
+ +
+
+
-
-
-

Upload overlay image

-

- Now, upload your binary overlay image as a PNG and let gerbolyze render it onto the target layer. The PNG - file should be a black and white binary file with details generally above about 10px size. Antialiased - edges are supported. -

-
-
-
- {{overlay_form.csrf_token}} -
-
-
Upload Overlay PNG file:
- -
-
-
Target layer:
- - - - -
-
- - -
-
-
+
+
+

Upload overlay SVG

+

+ Now, upload your binary overlay as an SVG and let gerbolyze paste it onto the target layers. +

+
+
+
+ {{overlay_form.csrf_token}} +
+
+
Upload Overlay PNG file:
+ +
+
+
Target layer:
+ + + + +
+
+ + +
+
+
- {% if 'vector_job' in session or has_output %} -
-
-

Download the processed gerber files

-
-
- {% if 'vector_job' in session %} -
-
-
Processing...
-
(this may take several minutes!)
-
- {% else %} - - {% endif %} -
- -
- -
-
- {% endif %} {# vector job #} - {% endif %} {# render job #} + {% if 'vector_job' in session or has_output %} +
+
+

Download the processed gerber files

+
+
+ {% if 'vector_job' in session %} +
+
+
Processing...
+
(this may take several minutes!)
+
+ {% else %} + + {% endif %} +
+ +
+ +
+
+ {% endif %} {# vector job #} + {% endif %} {# render job #}
-

Sample images

- - - +

Sample images

+ + +
diff --git a/kicad_mod_template.svg b/kicad_mod_template.svg new file mode 100644 index 0000000..4e7a6b7 --- /dev/null +++ b/kicad_mod_template.svg @@ -0,0 +1,95 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/pics/ex-flattening.png b/pics/ex-flattening.png new file mode 100644 index 0000000..e702733 Binary files /dev/null and b/pics/ex-flattening.png differ diff --git a/pics/ex-intersections.png b/pics/ex-intersections.png new file mode 100644 index 0000000..05ccc59 Binary files /dev/null and b/pics/ex-intersections.png differ diff --git a/pics/ex-strokes.png b/pics/ex-strokes.png new file mode 100644 index 0000000..5ef3c49 Binary files /dev/null and b/pics/ex-strokes.png differ diff --git a/pics/ex-svg-joins.png b/pics/ex-svg-joins.png new file mode 100644 index 0000000..534e4c2 Binary files /dev/null and b/pics/ex-svg-joins.png differ diff --git a/pics/ex-svg-strokes.png b/pics/ex-svg-strokes.png new file mode 100644 index 0000000..bec71c0 Binary files /dev/null and b/pics/ex-svg-strokes.png differ diff --git a/pics/ex-svg-winding.png b/pics/ex-svg-winding.png new file mode 100644 index 0000000..3f85f6f Binary files /dev/null and b/pics/ex-svg-winding.png differ diff --git a/pics/fr4_comparison2.jpg b/pics/fr4_comparison2.jpg new file mode 100644 index 0000000..fa7c92e Binary files /dev/null and b/pics/fr4_comparison2.jpg differ diff --git a/pics/pcbway_sample_01_small.jpg b/pics/pcbway_sample_01_small.jpg new file mode 100644 index 0000000..f8d6397 Binary files /dev/null and b/pics/pcbway_sample_01_small.jpg differ diff --git a/pics/pcbway_sample_02_small.jpg b/pics/pcbway_sample_02_small.jpg new file mode 100644 index 0000000..ba8e984 Binary files /dev/null and b/pics/pcbway_sample_02_small.jpg differ diff --git a/pics/pcbway_sample_03_small.jpg b/pics/pcbway_sample_03_small.jpg new file mode 100644 index 0000000..1336739 Binary files /dev/null and b/pics/pcbway_sample_03_small.jpg differ diff --git a/pics/process-overview.png b/pics/process-overview.png new file mode 100644 index 0000000..01d3811 Binary files /dev/null and b/pics/process-overview.png differ diff --git a/pics/process-overview.svg b/pics/process-overview.svg new file mode 100644 index 0000000..b21aa70 --- /dev/null +++ b/pics/process-overview.svg @@ -0,0 +1,7230 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KiCAD/Altium + SVG + Gerber! + + + + + + + + + + + + diff --git a/sample1.jpg b/pics/sample1.jpg similarity index 100% rename from sample1.jpg rename to pics/sample1.jpg diff --git a/sample2.jpg b/pics/sample2.jpg similarity index 100% rename from sample2.jpg rename to pics/sample2.jpg diff --git a/sample3.jpg b/pics/sample3.jpg similarity index 100% rename from sample3.jpg rename to pics/sample3.jpg diff --git a/pics/subtract_example.png b/pics/subtract_example.png new file mode 100644 index 0000000..f8e138a Binary files /dev/null and b/pics/subtract_example.png differ diff --git a/pics/test_svg_readme.svg b/pics/test_svg_readme.svg new file mode 100644 index 0000000..1a0178e --- /dev/null +++ b/pics/test_svg_readme.svg @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + सर्वे मानवाः स्वतन्त्राः समुत्पन्नाः वर्तन्ते अपि च, गौरवदृशा + + + + + لكن لا بد أن أوضح لك أن كل هذه الأفكار المغلوطة حول استنكار + + This is a gerber export test + + diff --git a/pics/test_svg_readme_composited.png b/pics/test_svg_readme_composited.png new file mode 100644 index 0000000..f686e19 Binary files /dev/null and b/pics/test_svg_readme_composited.png differ diff --git a/pics/vec_contours_composited.png b/pics/vec_contours_composited.png new file mode 100644 index 0000000..aa826f8 Binary files /dev/null and b/pics/vec_contours_composited.png differ diff --git a/pics/vec_hexgrid_composited.png b/pics/vec_hexgrid_composited.png new file mode 100644 index 0000000..9578260 Binary files /dev/null and b/pics/vec_hexgrid_composited.png differ diff --git a/pics/vec_poisson_composited.png b/pics/vec_poisson_composited.png new file mode 100644 index 0000000..e3ac758 Binary files /dev/null and b/pics/vec_poisson_composited.png differ diff --git a/pics/vec_square_composited.png b/pics/vec_square_composited.png new file mode 100644 index 0000000..02a89ea Binary files /dev/null and b/pics/vec_square_composited.png differ diff --git a/podman/arch-testenv b/podman/arch-testenv new file mode 100644 index 0000000..37236c9 --- /dev/null +++ b/podman/arch-testenv @@ -0,0 +1,9 @@ + +FROM docker.io/archlinux:latest +MAINTAINER gerbolyze@jaseg.de +RUN pacman --noconfirm -Syu +RUN pacman --noconfirm -Sy pugixml opencv pango cairo git python make clang rustup cargo python-pip base-devel +RUN rustup install stable +RUN rustup default stable +RUN cargo install usvg + diff --git a/podman/debian-testenv b/podman/debian-testenv new file mode 100644 index 0000000..544b3f2 --- /dev/null +++ b/podman/debian-testenv @@ -0,0 +1,11 @@ + +FROM docker.io/debian:latest +MAINTAINER gerbolyze@jaseg.de +RUN env DEBIAN_FRONTEND=noninteractive apt update -y +RUN env DEBIAN_FRONTEND=noninteractive apt install -y libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN sh -c '. ~/.cargo/env && rustup install stable' +RUN sh -c '. ~/.cargo/env && rustup default stable' +RUN sh -c '. ~/.cargo/env && cargo install usvg' + diff --git a/podman/fedora-testenv b/podman/fedora-testenv new file mode 100644 index 0000000..c37de87 --- /dev/null +++ b/podman/fedora-testenv @@ -0,0 +1,7 @@ + +FROM docker.io/fedora:latest +MAINTAINER gerbolyze@jaseg.de +RUN dnf update --refresh -y +RUN dnf install -y python3 make clang opencv-devel pugixml-devel pango-devel cairo-devel rust cargo +RUN cargo install usvg + diff --git a/podman/testdata/gerbolyze-2.0.0.tar.gz b/podman/testdata/gerbolyze-2.0.0.tar.gz new file mode 100644 index 0000000..50b2da0 Binary files /dev/null and b/podman/testdata/gerbolyze-2.0.0.tar.gz differ diff --git a/podman/testdata/test_svg_readme.svg b/podman/testdata/test_svg_readme.svg new file mode 100644 index 0000000..1a0178e --- /dev/null +++ b/podman/testdata/test_svg_readme.svg @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + सर्वे मानवाः स्वतन्त्राः समुत्पन्नाः वर्तन्ते अपि च, गौरवदृशा + + + + + لكن لا بد أن أوضح لك أن كل هذه الأفكار المغلوطة حول استنكار + + This is a gerber export test + + diff --git a/podman/testdata/testscript.sh b/podman/testdata/testscript.sh new file mode 100755 index 0000000..59d8b0e --- /dev/null +++ b/podman/testdata/testscript.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +pip3 install --user /data/gerbolyze-*.tar.gz --no-binary gerbolyze +/root/.local/bin/svg-flatten --clear-color black --dark-color white --format svg /data/test_svg_readme.svg /out/test_out.svg + diff --git a/podman/ubuntu-testenv b/podman/ubuntu-testenv new file mode 100644 index 0000000..2665d9b --- /dev/null +++ b/podman/ubuntu-testenv @@ -0,0 +1,7 @@ + +FROM docker.io/ubuntu:latest +MAINTAINER gerbolyze@jaseg.de +RUN env DEBIAN_FRONTEND=noninteractive apt update -y +RUN env DEBIAN_FRONTEND=noninteractive apt install -y libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv cargo +RUN cargo install usvg + diff --git a/post.html b/post.html new file mode 100644 index 0000000..dbfbcf3 --- /dev/null +++ b/post.html @@ -0,0 +1,463 @@ + + + + Create Beautiful Circuit Boards With Gerbolyze + + + +
+

Create Beautiful Circuit Boards With Gerbolyze

+

+ Today, there is an increasingly large crowd of people who do artistic circuit board designs. People who fuse + the roles of engineer and artist. Unitl today, circuit board design tools mostly ignore this use case and + present a multitude of obstacles for such use. Gerbolyze finally solves this problem and presents an + integrated solution for artistic PCB design that is compatible with real designer's workflows on one side + and with real electronic design automation software on the other side. +

+
+ A printed
+                         circuit board showing a surrealist manga-style drawing of a woman sitting atop several traffic
+                         lights. The woman's hair looks golden from the circuit board's gold copper finish. The
+                         background is blue and white with scales of gray emulated through a halftone technique like it
+                         is used in newspapers. +
+ An artistic PCB design +

+ This design was created from a digital artwork in a raster image file after circuit board + manufacturer PCBWay offered me some free boards. The artwork was pre-processed using a raster + graphics tool: It was split into layers for the different colors. Then, its color components were + adjusted for brightness and contrast and finally passed through a raster-based halftone filter. The + resulting file was then converted into circuit board manufacturing files using Gerbolyze. +

+
+
+

+ Thirty years ago, the world of printed circuit board design was revolutionized by the introduction of + computer aided design tools. These tools enabled extremely complex designs through automation features + like autorouting and through automatic design rule checking to weed out human error. While the first + such tools were still very limited, their capabilites quickly grew and a few years after their + introduction modern electronics design without computers was already unthinkable. +

+

+ Today, circuit board design programs can look back on a rich history and have accumulated a healty + amount of expert knowledge in their design. Despite their difficult economic niche, even free + software design tools have grown to become usable for advanced designs. However, all modern + circuit board design tools are severely lacking in one area: That of artistic design. Many + design assumptions are hard-wired deep into their design: Traces should be at 45° angles. Silkscreen is + one opaque color. etc. These design assumptions lead to these tools being in the way more often + than not for the increasing crowd of designers who try to create art with them. +

+

+ Gerbolyze solves this problem. Gerbolyze interfaces with circuit board design tools such as Altium or Kicad + on one side through standard Gerber files. It interfaces with vector graphics editors such as inkscape on + the other side through Scaleable Vector Graphics (SVG) files. By fusing both, it yields a powerful + environment for artistic circuit board design. +

+

Gerbolyze Algorithm Overview

+

+ Gerbolyze has two major components. The first is the gerbolyze executable itself, which orchestrates the + process of fusing SVG and Gerber files. The second is svg-flatten, a tool encapsulating all of the + heavy-duty computer graphics code. The gerbolyze executable is a python script for readability, while the + geometry backend is a C++ binary for performance. +

+

+ In the beginning of the fusing process, the orchestrator figures out what semantic layers such as silkscreen + or copper the tool's input files correspond to. The assigned layers are then processed one by one. For each + layer, the tool first checks the input SVG file for any content. If there is none on this layer, the layer + file is directly copied to the output. If ther is some, this SVG content is passed through the geometry + backend to convert it to Gerber code. The resulting Gerber code is then read and added on top of the input + file's gerber code. The result is written to the output. +

+

The Computer Geometry of Scaleable Vector Graphics

+
+ +
+ A complex SVG file... + ...illustrates the range of features that Gerbolyze supports. The left side of this picture shows + the input to gerbolze, the right side the resulting output. As you can see, clips, bitmap images, + pattern fills and strokes all behave in a "visually intuitive" way. +
+
+

+ The heavy lifting during this process is done by the geometry backend. While its job seems simple at first, + it is surprisingly stretching the state of the art in both academic research and technical implementations + of computer graphics. In its core the problem is that while SVG and Gerber are both essentially vector + graphics formats, both have very different conceptions of their drawing models: They differ significantly in + what happens when one of the input file's vectors is drawn. +

+

+ SVG is little more than a highly standardized description of the basic operations provided by the modern 2D + graphics interfaces such as Qt, Cairo or Skia that are built-in to all operating systems. Drawing + paths that are described by the vector coördinates of points along them is the most basic of these + operations. SVG also includes support for a surprising number of decidedly raster operations such + as masking but, operating on grayscales, these are less relevant for the type of design one would create + when targeting circuit board production processes. +

+

+ Similar to SVG, the Gerber file format also targets a type of graphics programming interface. Only instead + of that of operating systems 2D graphics APIs of the 90ies and 2000s, the Gerber format was created as a way + to encapsulate commands for photoplotters, computer-controlled machines that physically move a light source + across a photo-sensitive material. Gerber's concept of "aptertures" goes back to mechanical photoplotters + having magazines of stencils of different shapes and sizes that could be swapped into the path of light + during the plotting process. +

+
+ +
+ An SVG path with one self-intersection and one hole + On the left you see our example path illustrated with the locations of its nodes, that is + the points where one segment starts and another ends. Also illustrated are the handles that + define its curvature. Note that not every node has handles. This is because some of the path's + segments are straight lines instead of bezier curves. +
+
+

+ Like SVG has its paths, the Gerber format has polygons. Polygons were added to the format to ease the + description of irregularly-shaped areas: Previously, these would have to be drawn by overlaying thousands of + thin lines, described one by one in the Gerber file. Using a Polygon, one only needs to describe the shape's + outline as a series of points and the photoplotter will fill in the rest. +

+

Transforming Vectors into Vectors

+

+ The crux in converting from SVG to Gerber lies here, in the conversion of paths. While in both a path or + polygon is described by its outline, which is described by points, there are significant differences in the + limitations both place on these outlines. In SVG an outline can consist of several types of segments: + besides basic straight lines, multiple types of parametrized curves including cubic bezier curves are + possible. An SVG path's outline can self-intersect (cross over itself). The path can also have holes, + additional parts that are inside the outline. +

+
+ +
+ Bezier flattening... + ...transforms a elegant bezier curve into a dumb list of points. The resulting image can still be + called a vector image since after all points are vectors, too, but they can only allude to + their previous mathematical glory. At least straight lines are easy to deal with, though! +
+
+

+ Gerber, on the other hand, has a much more limited view of what a polygon is. In gerber, a polygon is + something bounded by straight line segments, that cannot touch except under very particular circumstances, + and holes are simply not supported. Converting from SVG's flexible model of a path to Gerber's very + limited model of a polygon while preserving the fidelity of the input data is the true challenge here. As an + aside, a fun complication one will encounter when embarking on this endeavour is that most programs that + display Gerber files use modern graphics libraries in the backend. In these programs, a valid SVG path + ineptly converted into an illegal gerber polygon may still end up looking alright since these programs + usually just pass through the gerber's input data to the underlying graphics layer without validation—and + that graphics layer is the one from SVG. +

+

Styles and Strokes

+

+ Creating gerber-compatible polygons from SVG paths is not the only place where some heavy computer geometry + is necessary. SVG also allows a path to be drawn with its outline stroked with a set width. Though stroking + a path with a given width is very intuitive, it again happens to be surprisingly complex to do in + computer-geometrically. +

+

+ At first one might think SVG stroke widths naturally map to gerber apertures, and all that is left is + flattening the path's bezier curves into straight line segments for it to be drawn. This approach would be + a passable approximation in many cases, but there is a large part of SVG's expressivity that will be lost + under this mapping. SVG allows designers to define join styles and end cap styles on a path. The join style + describes how two path segments that are at an angle will be joined. End caps describe how open ends of the + path will be rendered. Gerber does not have native support for either. +

+
+ +
+ Strokes can be quite complex +

+ In this picture, on the left you see a circle that is stroked with a pattern. To draw this to screen + or gerber, you have to first convert the ring's path to its outline. Then, you have to render the + pattern tiles that overlap that outline, while clipping them against the outline. The right-hand + picture illustrates different end and join styles. +

+
+

+ Regular 2D renderers perform stroking as a part of their rasterization routine. This means that a regular + renderer will likely never actually calculate the vector representation of the outline of the stroke at all, + instead bypassing that step and directly converting the path's vector representation into its pixel + representation given some stroke width. In our implementation we instead convert the outline of the path's + stroke into its vector polygon representation, which we then output as gerber code. +

+

+ This transformation from path plus stroke width to vector outline can be done in several ways. The one we + chose was to leverage the excellent Clipper library that we are already using for clipping shapes. Clipper's + offsetting function is essentially a turn-key solution for this use case. A nice side-effect of this + approach is that we can directly use the resulting stroke polygons as clips when a user specifies a path + with a patterned stroke! +

+

Gerbolyze Image Vectorization

+

+ To make Gerbolyze as user-friendly as possible I decided to include support for raster images as well. The + previous version of Gerbolyze in fact exclusively handled raster images, so keeping this support seemed + natural to me. +

+

+ There are several ways to convert raster images into a vector representation. Roughly, they fall into two + categories: Tracing and halftone processing. Tracing tries to read contours of colored areas of a raster + image, and approximates these contours with vector shapes. Tracing is most useful for raster images that + contain text or graphics. On good-quality input, tracing can produce surprisingly accurate results. In + contrast to tracing, halftone processing tries to emulate the picture's tones (be they grayscale or color) + with thousands or even millions of tiny colored filled shapes. Halftone processing to this day is used in + all kinds of printing processes. +

+

+ For bringing grayscale imagery into circuit board production, halftone processing is a very good fit. The + silkscreen processes that are commonly used for circuit boards have very high resolution and can reproduce + any input shape with micrometer precision, but they are limited in the smallest amount of ink they can put + on a board or the smallest gap between two blobs of ink that they can reliably produce. These limitations + are not due to the printers' or photoplotters' resolution or precision but simply due to the mechanics of + small droplets of liquid being squeegied onto or shot at a circuit board. +

+ +

+ The following pictures illustrate the different vectorization processes gerbolyze supports. In each row you + see the output of each vectorization process zoomed out on the left, and a zoomed-in detail view on the + right. For grayscale images, the poisson-disc-sampled halftone vectorizer works best. For tracing graphics, + the OpenCV contour tracer does a good job. This contour tracer is the + exact same one that was used in the previous version of Gerbolyze. +

+ +
+ Poisson disc sampling + Poisson disc sampling randomly distributes points on the plane. We then calculate these point's + voronoi tesselation, whose cells we then fill proportional to the image's brightness at that spot. + The resulting image looks very natural as it is devoid of distracting regular aliasing patterns. +
+
+
+ +
+ Hexagon grid sampling + The hexgrid sampler actually uses the same voronoi-based halftone code of the poisson-disc + sampling-based code, just with points generated on a hexgrid instead of randomly. The result is an + image that has a decidedly "space" look to it. At coarser resolutions, this sampling method has the + chance to shine with its attractive geometry. +
+
+
+ +
+ Square grid sampling + Though I think the square grid method looks the worst of the three halftone methods compared here, + it would have been ridiculous not to implement it given how little work it was using the + voronoi-cell halftone code. +
+
+
+ +
+ Binary contour tracing + This method calls into the same piece of the OpenCV image + processing library that the old gerbolyze used. As demonstrated here, this method lends itself well + to graphic inputs. It does also enable you to experiment with basically any rasterized halftone + processor such as the "Newsprint" filter built into GIMP. +
+
+

Manufacturing Considerations

+ +

+ When creating artistic designs for PCB manufacturing, you have to keep in mind the limitations of the PCB + manufacturing process at all times. PCB manufacturing processes only know filled and unfilled areas, and + fundamentally cannot do grayscale without hacks like halftone processing. More importantly, these + manufacturing processes have significant limitations in the smallest detail size that they can resolve. + For inexpensive processes, these trace/space design rules are commonly in the range of 50-150µm. While this + sounds great at first, it is vastly larger than what even a cheap home printer can accomplish. For + comparison, a regular inkjet printer for home use can print photos at 1200 dpi without breaking a sweat. + 1200 dpi means that this printer can put dots of ink on paper that are only 20 µm in size! And it can do + these dots in millions of colors, too. +

+

+ When tailoring a design for PCB manufacturing there's two things one can do. Number one is to be creative + with graphical parts of the design and avoid extremely narrow lines, wedges or other thin features that will + not come out during circuit board manufacturing. Number two is to keep detail in raster images several times + larger than the manufacturing processes native capability. For example, to target a trace/space design rule + of 100 µm, the smallest detail in embedded raster graphics should not be much below 1mm. +

+

+ Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still + produce small artifacts that violate these rules, their output should be close enough to satifsy board + houses and close enough for the result to look good. The way gerbolyze does this is to clip the halftone + cell's values to zero whenevery they get too small, and to forcefully split or merge two neighboring cells + when they get too close. While this process introduces slight steps at the top and bottom of grayscale + response, for most inputs these are not noticeable. +

+

+ On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with + these design rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design + here, so all that's on the roadmap here is to eventually include a gerber-level design rule checker. +

+

+ As far as board houses go, I have made good experiences with the popular Chinese board houses. In my + experience, JLC will just produce whatever you send them with little fucks being given about design rule + adherence or validity of the input gerbers. This is great if you just want artistic circuit boards without + much of a hassle, and you don't care if they come out exactly as you imagined. The worst I've had happen was + when an older version of gerbolyze generated polygons with holes assuming standard fill-rule processing. The + in the board house's online gerber viewer things looked fine, and neither did they complain during file + review. However, the resulting boards looked completely wrong because all the dark halftones were missing. +

+

+ PCBWay on the other hand has a much more rigurous file review process. They will complain when you + throw illegal garbage gerbers at them, and they will helpfully guide you through your design rule + violations. In this way you get much more of a professional service from them and for designs that have to + be functional their higher level of scrutiny definitely is a good thing. For the design you saw in the + first picture in this article, I ended up begging them to just plot my files if it doesn't physically break + their machines and to their credit, while they seemed unhappy about it they did it and the result looks + absolutely stunning. +

+

+ PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else + (large boards, multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of + manufacturing options such as flexible circuit boards, multi-layer boards, thick or thin substrates and + high-temperature substrates so they closer to an industry supplier like Eurocircuits than they are to a + hobbyist prototyping service like dirtypcbs. +

+
+ +
+ FR-4 substrate color differs significantly... + ...between these two boards. These boards come from two batches PCBWay produced of the exact same + design. As you can see, it would not have been wise to rely too much on the substrate's color in + this design. Obviously, this is not a defect as the precise color of the FR-4 substrate used is + perfectly irrelevant for the market these manufacturers actually target. +
+
+

+ When in doubt about how your design is going to come out on the board, do not hesitate to contact your board + house. Most of the customer-facing online PCB services have a number of different factories that do a number + of different fabrication processes for them depending on order parameters. Places like PCBWay have + exceptional quality control, but that is mostly focused on the technical aspects of the PCB. If you rely on + visual aspects like silkscreen uniformity or solder mask color, you may find significant variations between + manufacturers or even between orders with the same manufacturer. +

+

Conclusion

+

+ I hope Gerbolyze will make you life a bit easier when it comes to artistic PCB design. I hope I have managed + to illustrate a bit the design choices I made in Gerbolyze in this article. If you have any comments or + suggestions, please feel free to write me an email or open an + issue on Github. +

+
+ + diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..dae718b --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +python setup.py sdist build +cp dist/*.tar.gz podman/testdata + +for distro in arch fedora debian ubuntu +do + podman build -t gerbolyze-$distro-testenv -f podman/$distro-testenv + mkdir -p /tmp/gerbolyze-test-out + podman run --mount type=bind,src=podman/testdata,dst=/data,ro --mount type=bind,src=/tmp/gerbolyze-test-out,dst=/out gerbolyze-$distro-testenv /data/testscript.sh +done + diff --git a/setup.py b/setup.py index 3388853..2db4a8c 100755 --- a/setup.py +++ b/setup.py @@ -1,25 +1,57 @@ #!/usr/bin/env python3 +import os +import sys from setuptools import setup +from setuptools.command.install import install +import subprocess +from multiprocessing import cpu_count +from pathlib import Path def readme(): with open('README.rst') as f: return f.read() +def compile_and_install_svgflatten(target_dir): + src_path = 'svg-flatten' + + try: + subprocess.run(['make', 'check-deps'], cwd=src_path, check=True) + subprocess.run(['make', '-j', str(cpu_count()), 'all'], cwd=src_path, check=True) + bin_dir = target_dir / ".." + bin_dir.mkdir(parents=True, exist_ok=True) + subprocess.run(['make', 'install', f'PREFIX={bin_dir.resolve()}'], cwd=src_path, check=True) + except subprocess.CalledProcessError: + print('Error building svg-flatten C++ binary. Please see log above for details.', file=sys.stderr) + sys.exit(1) + +class CustomInstall(install): + """Custom handler for the 'install' command.""" + def run(self): + compile_and_install_svgflatten(Path(self.install_scripts)) + super().run() + setup( + cmdclass={'install': CustomInstall}, name = 'gerbolyze', - version = '0.1.10', + version = '2.0.8', py_modules = ['gerbolyze'], - scripts = ['gerbolyze'], - description = ('A high-resolution image-to-PCB converter. Gerbolyze reads and vectorizes black-and-white raster ' - 'images, then plots the vectorized image into an existing gerber file while avoiding existing features such as ' - 'text or holes.'), + package_dir = {'': 'gerbolyze'}, + entry_points = ''' + [console_scripts] + gerbolyze=gerbolyze:cli + ''', + description = ('A high-resolution image-to-PCB converter. Gerbolyze plots SVG, PNG and JPG onto existing gerber ' + 'files. It handles almost the full SVG spec and deals with text, path outlines, patterns, arbitrary paths with ' + 'self-intersections and holes, etc. fully automatically. It can vectorize raster images both by contour ' + 'tracing and by grayscale dithering. All processing is done at the vector level without intermediate ' + 'conversions to raster images accurately preserving the input.'), long_description=readme(), long_description_content_type='text/x-rst', - url = 'https://github.com/jaseg/gerbolyze', + url = 'https://git.jaseg.de/gerbolyze', author = 'jaseg', - author_email = 'github@jaseg.net', - install_requires = ['pcb-tools', 'tqdm', 'numpy', 'opencv-python'], + author_email = 'github@jaseg.de', + install_requires = ['pcb-tools', 'numpy', 'python-slugify', 'lxml', 'click', 'pcb-tools-extension'], license = 'AGPLv3', classifiers = [ 'Development Status :: 5 - Production/Stable', @@ -34,3 +66,4 @@ setup( 'Topic :: Utilities' ] ) + diff --git a/svg-flatten/Makefile b/svg-flatten/Makefile new file mode 100644 index 0000000..a82094c --- /dev/null +++ b/svg-flatten/Makefile @@ -0,0 +1,83 @@ + +CXX := clang++ +LD ?= ld +INSTALL := install +PKG_CONFIG ?= pkg-config + +BUILDDIR ?= build +PREFIX ?= /usr/local +UPSTREAM_DIR ?= ../upstream + +SOURCES := src/svg_color.cpp \ + src/svg_doc.cpp \ + src/svg_geom.cpp \ + src/svg_import_util.cpp \ + src/svg_path.cpp \ + src/svg_pattern.cpp \ + src/vec_core.cpp \ + src/vec_grid.cpp \ + src/main.cpp \ + src/flatten.cpp \ + src/out_svg.cpp \ + src/out_gerber.cpp \ + src/out_sexp.cpp \ + src/out_flattener.cpp \ + src/out_dilater.cpp \ + src/lambda_sink.cpp \ + src/flatten.cpp \ + $(UPSTREAM_DIR)/cpp-base64/base64.cpp + +CLIPPER_SOURCES ?= $(UPSTREAM_DIR)/clipper-6.4.2/cpp/clipper.cpp +CLIPPER_INCLUDES ?= -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp +VORONOI_INCLUDES ?= -I$(UPSTREAM_DIR)/voronoi/src +POISSON_INCLUDES ?= -I$(UPSTREAM_DIR)/poisson-disk-sampling/thinks/poisson_disk_sampling/ +BASE64_INCLUDES ?= -I$(UPSTREAM_DIR)/cpp-base64 +ARGAGG_INCLUDES ?= -I$(UPSTREAM_DIR)/argagg/include/argagg +CAVC_INCLUDES ?= -I$(UPSTREAM_DIR)/CavalierContours/include/cavc/ +SUBPROCESS_INCLUDES ?= -I$(UPSTREAM_DIR)/subprocess.h + +SOURCES += $(CLIPPER_SOURCES) +INCLUDES := -Iinclude -Isrc $(CLIPPER_INCLUDES) $(VORONOI_INCLUDES) $(POISSON_INCLUDES) $(BASE64_INCLUDES) $(ARGAGG_INCLUDES) $(CAVC_INCLUDES) $(SUBPROCESS_INCLUDES) + +PKG_CONFIG_DEPS := pugixml +CXXFLAGS := -std=c++2a -g -Wall -Wextra -O0 +CXXFLAGS += $(shell $(PKG_CONFIG) --cflags $(PKG_CONFIG_DEPS)) +# hack for stone age opencv in debian stable +CXXFLAGS += $(shell $(PKG_CONFIG) --cflags opencv4 2> /dev/null || $(PKG_CONFIG) --cflags opencv 2>/dev/null) + +LDFLAGS := -lm -lc -lstdc++ +LDFLAGS += $(shell $(PKG_CONFIG) --libs $(PKG_CONFIG_DEPS)) +# debian hack. see above. +OPENCV_LDFLAGS := $(shell $(PKG_CONFIG) --libs opencv4 2> /dev/null || $(PKG_CONFIG) --libs opencv 2>/dev/null) +LDFLAGS += $(shell echo $(OPENCV_LDFLAGS) | sed 's/-l\S\+ //g') -lopencv_core -lopencv_imgproc -lopencv_imgcodecs + +TARGET := svg-flatten + +all: $(BUILDDIR)/$(TARGET) + +.PHONY: check-deps +check-deps: + @echo + @$(PKG_CONFIG) --cflags --libs pugixml >/dev/null + # debian hack. see above. + @$(PKG_CONFIG) --cflags --libs opencv4 >/dev/null ||$(PKG_CONFIG) --cflags --libs opencv >/dev/null + +$(BUILDDIR)/%.o: %.cpp + @mkdir -p $(dir $@) + $(CXX) -c $(CXXFLAGS) $(CXXFLAGS) $(INCLUDES) -o $@ $^ + +$(BUILDDIR)/$(TARGET): $(SOURCES:%.cpp=$(BUILDDIR)/%.o) + @mkdir -p $(dir $@) + if [ $$(uname -s) = "Darwin" ]; then \ + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^; \ + else \ + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ -Wl,--start-group $^ -lstdc++fs -Wl,--end-group; \ + fi + +.PHONY: install +install: + $(INSTALL) $(BUILDDIR)/$(TARGET) $(PREFIX)/bin + +.PHONY: clean +clean: + rm -rf $(BUILDDIR) diff --git a/svg-flatten/include/flatten.hpp b/svg-flatten/include/flatten.hpp new file mode 100644 index 0000000..92cbf38 --- /dev/null +++ b/svg-flatten/include/flatten.hpp @@ -0,0 +1,27 @@ + +#include "gerbolyze.hpp" + +namespace gerbolyze { + class curve4_div { + public: + curve4_div(double distance_tolerance=0.1, double angle_tolerance=0.0, double cusp_limit=0.0) + : m_cusp_limit(cusp_limit), + m_distance_tolerance_square(0.25*distance_tolerance*distance_tolerance), + m_angle_tolerance(angle_tolerance) + { + } + + void run(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4); + const std::vector &points() { return m_points; } + + private: + void recursive_bezier(double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4, + unsigned level); + double m_cusp_limit; + double m_distance_tolerance_square; + double m_angle_tolerance; + std::vector m_points; + }; +} + diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp new file mode 100644 index 0000000..ac56628 --- /dev/null +++ b/svg-flatten/include/geom2d.hpp @@ -0,0 +1,157 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "svg_import_defs.h" + +using namespace std; + +namespace gerbolyze { + + typedef std::array d2p; + typedef std::vector Polygon; + + class xform2d { + public: + xform2d(double xx, double yx, double xy, double yy, double x0=0.0, double y0=0.0) : + xx(xx), yx(yx), xy(xy), yy(yy), x0(x0), y0(y0) {} + + xform2d() : xform2d(1.0, 0.0, 0.0, 1.0) {} + + xform2d(const string &svg_transform) : xform2d() { + if (svg_transform.empty()) + return; + + string start("matrix("); + if (svg_transform.substr(0, start.length()) != start) + return; + if (svg_transform.back() != ')') + return; + + const string &foo = svg_transform.substr(start.length(), svg_transform.length()); + const string &bar = foo.substr(0, foo.length() - 1); + + istringstream xform(bar); + + double a, c, e, + b, d, f; + xform >> a >> b >> c >> d >> e >> f; + if (xform.fail()) + return; + + xx=a, yx=b, xy=c, yy=d, x0=e, y0=f; + } + + xform2d &translate(double x, double y) { + x0 += x; + y0 += y; + return *this; + } + + xform2d &scale(double x, double y) { + xx *= x; yx *= y; xy *= x; + yy *= y; x0 *= x; y0 *= y; + return *this; + } + + xform2d &transform(const xform2d &other) { + double n_xx = xx * other.xx + yx * other.xy; + double n_yx = xx * other.yx + yx * other.yy; + + double n_xy = xy * other.xx + yy * other.xy; + double n_yy = xy * other.yx + yy * other.yy; + + double n_x0 = x0 * other.xx + y0 * other.xy + other.x0; + double n_y0 = x0 * other.yx + y0 * other.yy + other.y0; + + xx = n_xx; + yx = n_yx; + xy = n_xy; + yy = n_yy; + x0 = n_x0; + y0 = n_y0; + + return *this; + }; + + double doc2phys_dist(double dist_doc) { + return xx * dist_doc; + } + + double phys2doc_dist(double dist_doc) { + return xx * dist_doc; + } + + d2p doc2phys(const d2p p) { + return d2p { + xx * p[0] + xy * p[1] + x0, + xy * p[1] + yy * p[1] + y0 + }; + } + + xform2d &invert(bool *success_out=nullptr) { + /* From Cairo source */ + + /* inv (A) = 1/det (A) * adj (A) */ + double det = xx*yy - yx*xy; + + if (det == 0 || !isfinite(det)) { + if (success_out) + *success_out = false; + *this = xform2d(); /* unity matrix */ + return *this; + } + + *this = xform2d(yy/det, -yx/det, + -xy/det, xx/det, + (xy*y0 - yy*x0)/det, (yx*x0 - xx*y0)/det); + + if (success_out) + *success_out = true; + return *this; + } + + /* Transform given clipper paths */ + void transform_paths(ClipperLib::Paths &paths) { + for (auto &p : paths) { + std::transform(p.begin(), p.end(), p.begin(), + [this](ClipperLib::IntPoint p) -> ClipperLib::IntPoint { + d2p out(this->doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale})); + return { + (ClipperLib::cInt)round(out[0] * clipper_scale), + (ClipperLib::cInt)round(out[1] * clipper_scale) + }; + }); + } + } + + private: + double xx, yx, + xy, yy, + x0, y0; + }; +} diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp new file mode 100644 index 0000000..d1d5f85 --- /dev/null +++ b/svg-flatten/include/gerbolyze.hpp @@ -0,0 +1,269 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "svg_pattern.h" +#include "geom2d.hpp" + +namespace gerbolyze { + + constexpr char lib_version[] = "2.0"; + + typedef std::function *(double, double, double)> sampling_fun; + + enum GerberPolarityToken { + GRB_POL_CLEAR, + GRB_POL_DARK + }; + + class LayerNameToken { + public: + std::string m_name; + }; + + class PolygonSink { + public: + virtual ~PolygonSink() {} + virtual void header(d2p origin, d2p size) {(void) origin; (void) size;} + virtual PolygonSink &operator<<(const Polygon &poly) = 0; + virtual PolygonSink &operator<<(const LayerNameToken &) { return *this; }; + virtual PolygonSink &operator<<(GerberPolarityToken pol) = 0; + virtual void footer() {} + }; + + class Flattener_D; + class Flattener : public PolygonSink { + public: + Flattener(PolygonSink &sink); + virtual ~Flattener(); + virtual void header(d2p origin, d2p size); + virtual Flattener &operator<<(const Polygon &poly); + virtual Flattener &operator<<(const LayerNameToken &layer_name); + virtual Flattener &operator<<(GerberPolarityToken pol); + virtual void footer(); + + private: + void render_out_clear_polys(); + void flush_polys_to_sink(); + PolygonSink &m_sink; + GerberPolarityToken m_current_polarity = GRB_POL_DARK; + Flattener_D *d; + }; + + class Dilater : public PolygonSink { + public: + Dilater(PolygonSink &sink, double dilation) : m_sink(sink), m_dilation(dilation) {} + virtual void header(d2p origin, d2p size); + virtual Dilater &operator<<(const Polygon &poly); + virtual Dilater &operator<<(const LayerNameToken &layer_name); + virtual Dilater &operator<<(GerberPolarityToken pol); + virtual void footer(); + + private: + PolygonSink &m_sink; + double m_dilation; + GerberPolarityToken m_current_polarity = GRB_POL_DARK; + }; + + class StreamPolygonSink : public PolygonSink { + public: + StreamPolygonSink(std::ostream &out, bool only_polys=false) : m_only_polys(only_polys), m_out(out) {} + virtual ~StreamPolygonSink() {} + virtual void header(d2p origin, d2p size) { if (!m_only_polys) header_impl(origin, size); } + virtual void footer() { if (!m_only_polys) { footer_impl(); } m_out.flush(); } + + protected: + virtual void header_impl(d2p origin, d2p size) = 0; + virtual void footer_impl() = 0; + + bool m_only_polys = false; + std::ostream &m_out; + }; + + extern const std::vector kicad_default_layers; + + class ElementSelector { + public: + virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const = 0; + }; + + class IDElementSelector : public ElementSelector { + public: + virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const; + + std::vector include; + std::vector exclude; + const std::vector *layers = nullptr; + }; + + class ImageVectorizer { + public: + virtual ~ImageVectorizer() {}; + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px) = 0; + }; + + ImageVectorizer *makeVectorizer(const std::string &name); + + class VectorizerSelectorizer { + public: + VectorizerSelectorizer(const std::string default_vectorizer="dev-null", const std::string defs=""); + + ImageVectorizer *select(const pugi::xml_node &img); + + private: + std::string m_default; + std::map m_map; + }; + + class RenderSettings { + public: + double m_minimum_feature_size_mm = 0.1; + double curve_tolerance_mm; + VectorizerSelectorizer &m_vec_sel; + }; + + class SVGDocument { + public: + SVGDocument() : _valid(false) {} + + /* true -> load successful */ + bool load(std::istream &in); + bool load(std::string filename); + /* true -> load successful */ + bool valid() const { return _valid; } + operator bool() const { return valid(); } + + double mm_to_doc_units(double) const; + double doc_units_to_mm(double) const; + + double width() const { return page_w_mm; } + double height() const { return page_h_mm; } + + void render(const RenderSettings &rset, PolygonSink &sink, const ElementSelector *sel=nullptr); + void render_to_list(const RenderSettings &rset, std::vector> &out, const ElementSelector *sel=nullptr); + + private: + friend class Pattern; + + const ClipperLib::Paths *lookup_clip_path(const pugi::xml_node &node); + Pattern *lookup_pattern(const std::string id); + + void export_svg_group(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &group, ClipperLib::Paths &parent_clip_path, const ElementSelector *sel=nullptr, bool included=true, bool is_root=false); + void export_svg_path(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &node, ClipperLib::Paths &clip_path); + void setup_viewport_clip(); + void load_clips(const RenderSettings &rset); + void load_patterns(); + + bool _valid; + pugi::xml_document svg_doc; + pugi::xml_node root_elem; + pugi::xml_node defs_node; + double vb_x, vb_y, vb_w, vb_h; + double page_w, page_h; + double page_w_mm, page_h_mm; + std::map pattern_map; + std::map clip_path_map; + ClipperLib::Paths vb_paths; /* viewport clip rect */ + + PolygonSink *polygon_sink = nullptr; + + static constexpr double dbg_fill_alpha = 0.8; + static constexpr double dbg_stroke_alpha = 1.0; + static constexpr double assumed_usvg_dpi = 96.0; + }; + + typedef std::function lambda_sink_fun; + class LambdaPolygonSink : public PolygonSink { + public: + LambdaPolygonSink(lambda_sink_fun lambda) : m_lambda(lambda) {} + + virtual LambdaPolygonSink &operator<<(const Polygon &poly); + virtual LambdaPolygonSink &operator<<(GerberPolarityToken pol); + private: + GerberPolarityToken m_currentPolarity = GRB_POL_DARK; + lambda_sink_fun m_lambda; + }; + + class SimpleGerberOutput : public StreamPolygonSink { + public: + SimpleGerberOutput(std::ostream &out, bool only_polys=false, int digits_int=4, int digits_frac=6, double scale=1.0, d2p offset={0,0}, bool flip_polarity=false); + virtual ~SimpleGerberOutput() {} + virtual SimpleGerberOutput &operator<<(const Polygon &poly); + virtual SimpleGerberOutput &operator<<(GerberPolarityToken pol); + virtual void header_impl(d2p origin, d2p size); + virtual void footer_impl(); + + private: + int m_digits_int; + int m_digits_frac; + double m_width; + double m_height; + long long int m_gerber_scale; + d2p m_offset; + double m_scale; + bool m_flip_pol; + }; + + class SimpleSVGOutput : public StreamPolygonSink { + public: + SimpleSVGOutput(std::ostream &out, bool only_polys=false, int digits_frac=6, std::string dark_color="#000000", std::string clear_color="#ffffff"); + virtual ~SimpleSVGOutput() {} + virtual SimpleSVGOutput &operator<<(const Polygon &poly); + virtual SimpleSVGOutput &operator<<(GerberPolarityToken pol); + virtual void header_impl(d2p origin, d2p size); + virtual void footer_impl(); + + private: + int m_digits_frac; + std::string m_dark_color; + std::string m_clear_color; + std::string m_current_color; + d2p m_offset; + }; + + class KicadSexpOutput : public StreamPolygonSink { + public: + KicadSexpOutput(std::ostream &out, std::string mod_name, std::string layer, bool only_polys=false, std::string m_ref_text="", std::string m_val_text="G*****", d2p ref_pos={0,10}, d2p val_pos={0,-10}); + virtual ~KicadSexpOutput() {} + virtual KicadSexpOutput &operator<<(const Polygon &poly); + virtual KicadSexpOutput &operator<<(const LayerNameToken &layer_name); + virtual KicadSexpOutput &operator<<(GerberPolarityToken pol); + virtual void header_impl(d2p origin, d2p size); + virtual void footer_impl(); + + void set_export_layers(const std::vector &layers) { m_export_layers = &layers; } + + private: + const std::vector *m_export_layers = &kicad_default_layers; + std::string m_mod_name; + std::string m_layer; + bool m_auto_layer; + std::string m_ref_text; + std::string m_val_text; + d2p m_ref_pos; + d2p m_val_pos; + }; +} diff --git a/svg-flatten/src/flatten.cpp b/svg-flatten/src/flatten.cpp new file mode 100644 index 0000000..cb7f427 --- /dev/null +++ b/svg-flatten/src/flatten.cpp @@ -0,0 +1,230 @@ +/* Copied from Antigrain Graphics (AGG) v2.4 */ +/* Mirror: https://github.com/pelson/antigrain/blob/master/agg-2.4/src/agg_curves.cpp */ + +#include +#include + +using namespace gerbolyze; + +namespace gerbolyze { + const double curve_collinearity_epsilon = 1e-15; + const double curve_angle_tolerance_epsilon = 0.1; + constexpr unsigned curve_recursion_limit = 20; +} + +static inline double calc_sq_distance(double x1, double y1, double x2, double y2) +{ + double dx = x2-x1; + double dy = y2-y1; + return dx * dx + dy * dy; +} + +void curve4_div::run(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { + m_points.clear(); + m_points.emplace_back(d2p{x1, y1}); + recursive_bezier(x1, y1, x2, y2, x3, y3, x4, y4, 0); + m_points.emplace_back(d2p{x4, y4}); +} + +void curve4_div::recursive_bezier(double x1, double y1, + double x2, double y2, + double x3, double y3, + double x4, double y4, + unsigned level) +{ + if(level > curve_recursion_limit) { + return; + } + + double pi = M_PI; + + // Calculate all the mid-points of the line segments + //---------------------- + double x12 = (x1 + x2) / 2; + double y12 = (y1 + y2) / 2; + double x23 = (x2 + x3) / 2; + double y23 = (y2 + y3) / 2; + double x34 = (x3 + x4) / 2; + double y34 = (y3 + y4) / 2; + double x123 = (x12 + x23) / 2; + double y123 = (y12 + y23) / 2; + double x234 = (x23 + x34) / 2; + double y234 = (y23 + y34) / 2; + double x1234 = (x123 + x234) / 2; + double y1234 = (y123 + y234) / 2; + + + // Try to approximate the full cubic curve by a single straight line + //------------------ + double dx = x4-x1; + double dy = y4-y1; + + double d2 = fabs(((x2 - x4) * dy - (y2 - y4) * dx)); + double d3 = fabs(((x3 - x4) * dy - (y3 - y4) * dx)); + double da1, da2, k; + + switch((int(d2 > curve_collinearity_epsilon) << 1) + + int(d3 > curve_collinearity_epsilon)) + { + case 0: + // All collinear OR p1==p4 + //---------------------- + k = dx*dx + dy*dy; + if(k == 0) { + d2 = calc_sq_distance(x1, y1, x2, y2); + d3 = calc_sq_distance(x4, y4, x3, y3); + + } else { + k = 1 / k; + da1 = x2 - x1; + da2 = y2 - y1; + d2 = k * (da1*dx + da2*dy); + da1 = x3 - x1; + da2 = y3 - y1; + d3 = k * (da1*dx + da2*dy); + + if(d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1) { + // Simple collinear case, 1---2---3---4 + // We can leave just two endpoints + return; + } + + if(d2 <= 0) { + d2 = calc_sq_distance(x2, y2, x1, y1); + } else if(d2 >= 1) { + d2 = calc_sq_distance(x2, y2, x4, y4); + } else { + d2 = calc_sq_distance(x2, y2, x1 + d2*dx, y1 + d2*dy); + } + + if(d3 <= 0) { + d3 = calc_sq_distance(x3, y3, x1, y1); + } else if(d3 >= 1) { + d3 = calc_sq_distance(x3, y3, x4, y4); + } else { + d3 = calc_sq_distance(x3, y3, x1 + d3*dx, y1 + d3*dy); + } + + } + + if(d2 > d3) { + if(d2 < m_distance_tolerance_square) { + m_points.emplace_back(d2p{x2, y2}); + return; + } + } else { + if(d3 < m_distance_tolerance_square) { + m_points.emplace_back(d2p{x3, y3}); + return; + } + } + break; + + case 1: + // p1,p2,p4 are collinear, p3 is significant + //---------------------- + if(d3 * d3 <= m_distance_tolerance_square * (dx*dx + dy*dy)) { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) { + m_points.emplace_back(d2p{x23, y23}); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y4 - y3, x4 - x3) - atan2(y3 - y2, x3 - x2)); + if(da1 >= pi) da1 = 2*pi - da1; + + if(da1 < m_angle_tolerance) { + m_points.emplace_back(d2p{x2, y2}); + m_points.emplace_back(d2p{x3, y3}); + return; + } + + if(m_cusp_limit != 0.0) { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(d2p{x3, y3}); + return; + } + } + } + break; + + case 2: + // p1,p3,p4 are collinear, p2 is significant + //---------------------- + if(d2 * d2 <= m_distance_tolerance_square * (dx*dx + dy*dy)) { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) { + m_points.emplace_back(d2p{x23, y23}); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y3 - y2, x3 - x2) - atan2(y2 - y1, x2 - x1)); + if(da1 >= pi) da1 = 2*pi - da1; + + if(da1 < m_angle_tolerance) { + m_points.emplace_back(d2p{x2, y2}); + m_points.emplace_back(d2p{x3, y3}); + return; + } + + if(m_cusp_limit != 0.0) { + if(da1 > m_cusp_limit) { + m_points.emplace_back(d2p{x2, y2}); + return; + } + } + } + break; + + case 3: + // Regular case + //----------------- + if((d2 + d3)*(d2 + d3) <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + // If the curvature doesn't exceed the distance_tolerance value + // we tend to finish subdivisions. + //---------------------- + if(m_angle_tolerance < curve_angle_tolerance_epsilon) { + m_points.emplace_back(d2p{x23, y23}); + return; + } + + // Angle & Cusp Condition + //---------------------- + k = atan2(y3 - y2, x3 - x2); + da1 = fabs(k - atan2(y2 - y1, x2 - x1)); + da2 = fabs(atan2(y4 - y3, x4 - x3) - k); + if(da1 >= pi) da1 = 2*pi - da1; + if(da2 >= pi) da2 = 2*pi - da2; + + if(da1 + da2 < m_angle_tolerance) { + // Finally we can stop the recursion + //---------------------- + m_points.emplace_back(d2p{x23, y23}); + return; + } + + if(m_cusp_limit != 0.0) { + if(da1 > m_cusp_limit) { + m_points.emplace_back(d2p{x2, y2}); + return; + } + + if(da2 > m_cusp_limit) { + m_points.emplace_back(d2p{x3, y3}); + return; + } + } + } + break; + } + + // Continue subdivision + //---------------------- + recursive_bezier(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1); + recursive_bezier(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1); +} + diff --git a/svg-flatten/src/lambda_sink.cpp b/svg-flatten/src/lambda_sink.cpp new file mode 100644 index 0000000..5172f24 --- /dev/null +++ b/svg-flatten/src/lambda_sink.cpp @@ -0,0 +1,36 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +using namespace gerbolyze; +using namespace std; + +LambdaPolygonSink& LambdaPolygonSink::operator<<(const Polygon &poly) { + m_lambda(poly, m_currentPolarity); + return *this; +} + +LambdaPolygonSink& LambdaPolygonSink::operator<<(GerberPolarityToken pol) { + m_currentPolarity = pol; + return *this; +} diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp new file mode 100644 index 0000000..722b356 --- /dev/null +++ b/svg-flatten/src/main.cpp @@ -0,0 +1,446 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vec_core.h" +#include + +using argagg::parser_results; +using argagg::parser; +using namespace std; +using namespace gerbolyze; + +int main(int argc, char **argv) { + parser argparser {{ + {"help", {"-h", "--help"}, + "Print help and exit", + 0}, + {"version", {"-v", "--version"}, + "Print version and exit", + 0}, + {"ofmt", {"-o", "--format"}, + "Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression)", + 1}, + {"precision", {"-p", "--precision"}, + "Number of decimal places use for exported coordinates (gerber: 1-9, SVG: 0-*)", + 1}, + {"svg_clear_color", {"--clear-color"}, + "SVG color to use for \"clear\" areas (default: white)", + 1}, + {"svg_dark_color", {"--dark-color"}, + "SVG color to use for \"dark\" areas (default: black)", + 1}, + {"flip_gerber_polarity", {"-f", "--flip-gerber-polarity"}, + "Flip polarity of all output gerber primitives for --format gerber.", + 0}, + {"min_feature_size", {"-d", "--trace-space"}, + "Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.", + 1}, + {"curve_tolerance", {"-c", "--curve-tolerance"}, + "Tolerance for curve flattening in mm. Default: 0.1mm.", + 1}, + {"no_header", {"--no-header"}, + "Do not export output format header/footer, only export the primitives themselves", + 0}, + {"flatten", {"--flatten"}, + "Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level. Potentially slow.", + 0}, + {"no_flatten", {"--no-flatten"}, + "Disable automatic flattening for KiCAD S-Exp export", + 0}, + {"dilate", {"--dilate"}, + "Dilate output gerber primitives by this amount in mm. Used for masking out other layers.", + 1}, + {"only_groups", {"-g", "--only-groups"}, + "Comma-separated list of group IDs to export.", + 1}, + {"vectorizer", {"-b", "--vectorizer"}, + "Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours, dev-null.", + 1}, + {"vectorizer_map", {"--vectorizer-map"}, + "Map from image element id to vectorizer. Overrides --vectorizer. Format: id1=vectorizer,id2=vectorizer,...", + 1}, + {"force_svg", {"--force-svg"}, + "Force SVG input irrespective of file name", + 0}, + {"force_png", {"--force-png"}, + "Force bitmap graphics input irrespective of file name", + 0}, + {"size", {"-s", "--size"}, + "Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78", + 1}, + {"sexp_mod_name", {"--sexp-mod-name"}, + "Module name for KiCAD S-Exp output", + 1}, + {"sexp_layer", {"--sexp-layer"}, + "Layer for KiCAD S-Exp output. Defaults to auto-detect layers from SVG layer/top-level group names", + 1}, + {"preserve_aspect_ratio", {"-a", "--preserve-aspect-ratio"}, + "Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG preserveAspectRatio syntax.", + 1}, + {"skip_usvg", {"--no-usvg"}, + "Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing)", + 0}, + {"usvg_dpi", {"--usvg-dpi"}, + "Passed through to usvg's --dpi, in case the input file has different ideas of DPI than usvg has.", + 1}, + {"scale", {"--scale"}, + "Scale input svg lengths by this factor.", + 1}, + {"exclude_groups", {"-e", "--exclude-groups"}, + "Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.", + 1}, + + }}; + + + ostringstream usage; + usage + << argv[0] << " " << lib_version << endl + << endl + << "Usage: " << argv[0] << " [options]... [input_file] [output_file]" << endl + << endl + << "Specify \"-\" for stdin/stdout." << endl + << endl; + + argagg::parser_results args; + try { + args = argparser.parse(argc, argv); + } catch (const std::exception& e) { + argagg::fmt_ostream fmt(cerr); + fmt << usage.str() << argparser << '\n' + << "Encountered exception while parsing arguments: " << e.what() + << '\n'; + return EXIT_FAILURE; + } + + if (args["help"]) { + argagg::fmt_ostream fmt(cerr); + fmt << usage.str() << argparser; + return EXIT_SUCCESS; + } + + if (args["version"]) { + cerr << lib_version << endl; + return EXIT_SUCCESS; + } + + string in_f_name; + istream *in_f = &cin; + ifstream in_f_file; + string out_f_name; + ostream *out_f = &cout; + ofstream out_f_file; + + if (args.pos.size() >= 1) { + in_f_name = args.pos[0]; + + if (args.pos.size() >= 2) { + out_f_name = args.pos[1]; + } + } + + if (!in_f_name.empty() && in_f_name != "-") { + in_f_file.open(in_f_name); + if (!in_f_file) { + cerr << "Cannot open input file \"" << in_f_name << "\"" << endl; + return EXIT_FAILURE; + } + in_f = &in_f_file; + } + + if (!out_f_name.empty() && out_f_name != "-") { + out_f_file.open(out_f_name); + if (!out_f_file) { + cerr << "Cannot open output file \"" << out_f_name << "\"" << endl; + return EXIT_FAILURE; + } + out_f = &out_f_file; + } + + bool only_polys = args["no_header"]; + + int precision = 6; + if (args["precision"]) { + precision = atoi(args["precision"]); + } + + string fmt = args["ofmt"] ? args["ofmt"].as() : "gerber"; + transform(fmt.begin(), fmt.end(), fmt.begin(), [](unsigned char c){ return std::tolower(c); }); /* c++ yeah */ + + string sexp_layer = args["sexp_layer"] ? args["sexp_layer"].as() : "auto"; + + bool force_flatten = false; + bool is_sexp = false; + PolygonSink *sink = nullptr; + PolygonSink *flattener = nullptr; + PolygonSink *dilater = nullptr; + if (fmt == "svg") { + string dark_color = args["svg_dark_color"] ? args["svg_dark_color"].as() : "#000000"; + string clear_color = args["svg_clear_color"] ? args["svg_clear_color"].as() : "#ffffff"; + sink = new SimpleSVGOutput(*out_f, only_polys, precision, dark_color, clear_color); + + } else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber") { + double scale = args["scale"].as(1.0); + cerr << "loading @scale=" << scale << endl; + sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"]); + + } else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") { + if (!args["sexp_mod_name"]) { + cerr << "Error: --sexp-mod-name must be given for sexp export" << endl; + return EXIT_FAILURE; + } + + sink = new KicadSexpOutput(*out_f, args["sexp_mod_name"], sexp_layer, only_polys); + force_flatten = true; + is_sexp = true; + + } else { + cerr << "Error: Unknown output format \"" << fmt << "\"" << endl; + return EXIT_FAILURE; + } + + PolygonSink *top_sink = sink; + + if (args["dilate"]) { + dilater = new Dilater(*top_sink, args["dilate"].as()); + top_sink = dilater; + } + + if (args["flatten"] || (force_flatten && !args["no_flatten"])) { + flattener = new Flattener(*top_sink); + top_sink = flattener; + } + + /* Because the C++ stdlib is bullshit */ + auto id_match = [](string in, vector &out) { + stringstream ss(in); + while (getline(ss, out.emplace_back(), ',')) { + } + out.pop_back(); + }; + + IDElementSelector sel; + if (args["only_groups"]) + id_match(args["only_groups"], sel.include); + if (args["exclude_groups"]) + id_match(args["exclude_groups"], sel.exclude); + if (is_sexp && sexp_layer == "auto") { + sel.layers = &gerbolyze::kicad_default_layers; + } + + string vectorizer = args["vectorizer"] ? args["vectorizer"].as() : "poisson-disc"; + /* Check argument */ + ImageVectorizer *vec = makeVectorizer(vectorizer); + if (!vec) { + cerr << "Unknown vectorizer \"" << vectorizer << "\"." << endl; + argagg::fmt_ostream fmt(cerr); + fmt << usage.str() << argparser; + return EXIT_FAILURE; + } + delete vec; + + double min_feature_size = args["min_feature_size"].as(0.1); /* mm */ + double curve_tolerance = args["curve_tolerance"].as(0.1); /* mm */ + + string ending = ""; + auto idx = in_f_name.rfind("."); + if (idx != string::npos) { + ending = in_f_name.substr(idx); + transform(ending.begin(), ending.end(), ending.begin(), [](unsigned char c){ return std::tolower(c); }); /* c++ yeah */ + } + + filesystem::path barf = { filesystem::temp_directory_path() /= (std::tmpnam(nullptr) + string(".svg")) }; + filesystem::path frob = { filesystem::temp_directory_path() /= (std::tmpnam(nullptr) + string(".svg")) }; + + bool is_svg = args["force_svg"] || (ending == ".svg" && !args["force_png"]); + if (!is_svg) { + cerr << "writing bitmap into svg" << endl; + if (!args["size"]) { + cerr << "Error: --size must be given when using bitmap input." << endl; + return EXIT_FAILURE; + } + + string sz = args["size"].as(); + auto pos = sz.find_first_of("x*,"); + if (pos == string::npos) { + cerr << "Error: --size must be of form 12.34x56.78" << endl; + return EXIT_FAILURE; + } + + string x_str = sz.substr(0, pos); + string y_str = sz.substr(pos+1); + + double width = std::strtod(x_str.c_str(), nullptr); + double height = std::strtod(y_str.c_str(), nullptr); + + if (width < 1 || height < 1) { + cerr << "Error: --size must be of form 12.34x56.78 and values must be positive floating-point numbers in mm" << endl; + return EXIT_FAILURE; + } + + ofstream svg(barf.c_str()); + + svg << "" << endl; + + string par_attr = "none"; + if (args["preserve_aspect_ratio"]) { + string aspect_ratio = args["preserve_aspect_ratio"].as(); + if (aspect_ratio == "meet") { + par_attr = "xMidYMid meet"; + } else if (aspect_ratio == "slice") { + par_attr = "xMidYMid slice"; + } else { + par_attr = aspect_ratio; + } + } + svg << "rdbuf(); + string le_data = sstr.str(); + + svg << base64_encode(le_data); + svg << "\"/>" << endl; + + svg << "" << endl; + svg.close(); + + } else { /* svg file */ + cerr << "copying svg input into temp svg" << endl; + + /* c++ has the best hacks */ + std::ostringstream sstr; + sstr << in_f->rdbuf(); + + ofstream tmp_out(barf.c_str()); + tmp_out << sstr.str(); + tmp_out.close(); + + } + + if (args["skip_usvg"]) { + cerr << "skipping usvg" << endl; + frob = barf; + + } else { + cerr << "calling usvg on " << barf << " and " << frob << endl; + int dpi = 96; + if (args["usvg_dpi"]) { + dpi = args["usvg_dpi"].as(); + } + string dpi_str = to_string(dpi); + + const char *homedir; + if ((homedir = getenv("HOME")) == NULL) { + homedir = getpwuid(getuid())->pw_dir; + } + string homedir_s(homedir); + string loc_in_home = homedir_s + "/.cargo/bin/usvg"; + + const char *command_line[] = {nullptr, "--keep-named-groups", "--dpi", dpi_str.c_str(), barf.c_str(), frob.c_str(), NULL}; + bool found_usvg = false; + int usvg_rc=-1; + for (int i=0; i<3; i++) { + const char *usvg_envvar; + switch (i) { + case 0: + if ((usvg_envvar = getenv("USVG")) == NULL) { + continue; + } else { + command_line[0] = "usvg"; + } + break; + + case 1: + command_line[0] = "usvg"; + break; + + case 2: + command_line[0] = loc_in_home.c_str(); + break; + } + + struct subprocess_s subprocess; + int rc = subprocess_create(command_line, subprocess_option_inherit_environment, &subprocess); + if (rc) { + cerr << "Error calling usvg!" << endl; + return EXIT_FAILURE; + } + + usvg_rc = -1; + rc = subprocess_join(&subprocess, &usvg_rc); + if (rc) { + cerr << "Error calling usvg!" << endl; + return EXIT_FAILURE; + } + + rc = subprocess_destroy(&subprocess); + if (rc) { + cerr << "Error calling usvg!" << endl; + return EXIT_FAILURE; + } + + if (usvg_rc == 255) { + continue; + } + found_usvg = true; + break; + } + + if (!found_usvg) { + cerr << "Error: Cannot find usvg. Is it installed and in $PATH?" << endl; + return EXIT_FAILURE; + } + + if (usvg_rc) { + cerr << "usvg returned an error code: " << usvg_rc << endl; + return EXIT_FAILURE; + } + } + + VectorizerSelectorizer vec_sel(vectorizer, args["vectorizer_map"] ? args["vectorizer_map"].as() : ""); + RenderSettings rset { + min_feature_size, + curve_tolerance, + vec_sel, + }; + + SVGDocument doc; + cerr << "Loading temporary file " << frob << endl; + ifstream load_f(frob); + if (!doc.load(load_f)) { + cerr << "Error loading input file \"" << in_f_name << "\", exiting." << endl; + return EXIT_FAILURE; + } + + doc.render(rset, *top_sink, &sel); + + remove(frob.c_str()); + remove(barf.c_str()); + + if (flattener) { + delete flattener; + } + if (dilater) { + delete dilater; + } + if (sink) { + delete sink; + } + return EXIT_SUCCESS; +} + diff --git a/svg-flatten/src/out_dilater.cpp b/svg-flatten/src/out_dilater.cpp new file mode 100644 index 0000000..e9aefa8 --- /dev/null +++ b/svg-flatten/src/out_dilater.cpp @@ -0,0 +1,86 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "polylinecombine.hpp" + +using namespace gerbolyze; +using namespace std; + +void Dilater::header(d2p origin, d2p size) { + m_sink.header(origin, size); +} + +void Dilater::footer() { + m_sink.footer(); +} + +Dilater &Dilater::operator<<(const LayerNameToken &layer_name) { + m_sink << layer_name; + + return *this; +} + +Dilater &Dilater::operator<<(GerberPolarityToken pol) { + m_current_polarity = pol; + m_sink << pol; + + return *this; +} + +Dilater &Dilater::operator<<(const Polygon &poly) { + ClipperLib::Path poly_c; + for (auto &p : poly) { + poly_c.push_back({(ClipperLib::cInt)round(p[0] * clipper_scale), (ClipperLib::cInt)round(p[1] * clipper_scale)}); + } + + ClipperLib::ClipperOffset offx; + offx.ArcTolerance = 0.05 * clipper_scale; /* 10µm; TODO: Make this configurable */ + offx.AddPath(poly_c, ClipperLib::jtRound, ClipperLib::etClosedPolygon); + double dilation = m_dilation; + if (m_current_polarity == GRB_POL_CLEAR) { + dilation = -dilation; + } + + ClipperLib::PolyTree solution; + offx.Execute(solution, dilation * clipper_scale); + + ClipperLib::Paths c_nice_polys; + dehole_polytree(solution, c_nice_polys); + + for (auto &nice_poly : c_nice_polys) { + Polygon new_poly; + for (auto &p : nice_poly) { + new_poly.push_back({ + (double)p.X / clipper_scale, + (double)p.Y / clipper_scale }); + } + m_sink << new_poly; + } + + return *this; +} + diff --git a/svg-flatten/src/out_flattener.cpp b/svg-flatten/src/out_flattener.cpp new file mode 100644 index 0000000..8868ca2 --- /dev/null +++ b/svg-flatten/src/out_flattener.cpp @@ -0,0 +1,187 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "polylinecombine.hpp" + +using namespace gerbolyze; +using namespace std; + +static void polygon_to_cavc (const Polygon &in, cavc::Polyline &out) { + for (auto &p : in) { + out.addVertex(p[0], p[1], 0); + } + out.isClosed() = true; /* sic! */ +} + +static void cavc_to_polygon (const cavc::Polyline &in, Polygon &out) { + for (auto &p : in.vertexes()) { + out.emplace_back(d2p{p.x(), p.y()}); + } +} + +namespace gerbolyze { + class Flattener_D { + public: + vector> dark_polys; + vector> clear_polys; + + void add_dark_polygon(const Polygon &in) { + polygon_to_cavc(in, dark_polys.emplace_back()); + } + + void add_clear_polygon(const Polygon &in) { + polygon_to_cavc(in, clear_polys.emplace_back()); + } + }; +} + +Flattener::Flattener(PolygonSink &sink) : m_sink(sink) { + d = new Flattener_D(); +} + +Flattener::~Flattener() { + delete d; +} + +void Flattener::header(d2p origin, d2p size) { + m_sink.header(origin, size); +} + +void Flattener::render_out_clear_polys() { + for (auto &sub : d->clear_polys) { + vector> new_dark_polys; + new_dark_polys.reserve(d->dark_polys.size()); + + for (cavc::Polyline cavc_in : d->dark_polys) { + auto res = cavc::combinePolylines(cavc_in, sub, cavc::PlineCombineMode::Exclude); + + if (res.subtracted.size() == 0) { + for (auto &rem : res.remaining) { + new_dark_polys.push_back(std::move(rem)); + } + + } else { /* custom one-hole deholing code */ + assert (res.remaining.size() == 1); + assert (res.subtracted.size() == 1); + + auto &rem = res.remaining[0]; + auto &sub = res.subtracted[0]; + auto bbox = getExtents(rem); + + cavc::Polyline quad; + quad.addVertex(bbox.xMin, bbox.yMin, 0); + if (sub.vertexes()[0].x() < sub.vertexes()[1].x()) { + quad.addVertex(sub.vertexes()[0]); + quad.addVertex(sub.vertexes()[1]); + } else { + quad.addVertex(sub.vertexes()[1]); + quad.addVertex(sub.vertexes()[0]); + } + quad.addVertex(bbox.xMax, bbox.yMin, 0); + quad.isClosed() = true; /* sic! */ + + auto res2 = cavc::combinePolylines(rem, quad, cavc::PlineCombineMode::Exclude); + assert (res2.subtracted.size() == 0); + + for (auto &rem : res2.remaining) { + auto res3 = cavc::combinePolylines(rem, sub, cavc::PlineCombineMode::Exclude); + assert (res3.subtracted.size() == 0); + for (auto &p : res3.remaining) { + new_dark_polys.push_back(std::move(p)); + } + } + + auto res4 = cavc::combinePolylines(rem, quad, cavc::PlineCombineMode::Intersect); + assert (res4.subtracted.size() == 0); + + for (auto &rem : res4.remaining) { + auto res5 = cavc::combinePolylines(rem, sub, cavc::PlineCombineMode::Exclude); + assert (res5.subtracted.size() == 0); + for (auto &p : res5.remaining) { + new_dark_polys.push_back(std::move(p)); + } + } + } + } + + d->dark_polys = std::move(new_dark_polys); + } + d->clear_polys.clear(); +} + +Flattener &Flattener::operator<<(GerberPolarityToken pol) { + if (m_current_polarity != pol) { + m_current_polarity = pol; + + if (pol == GRB_POL_DARK) { + render_out_clear_polys(); + } + } + + return *this; +} + +Flattener &Flattener::operator<<(const LayerNameToken &layer_name) { + flush_polys_to_sink(); + m_sink << layer_name; + cerr << "Flattener forwarding layer name to sink: \"" << layer_name.m_name << "\"" << endl; + + return *this; +} + +Flattener &Flattener::operator<<(const Polygon &poly) { + if (m_current_polarity == GRB_POL_DARK) { + d->add_dark_polygon(poly); + + } else { /* clear */ + d->add_clear_polygon(poly); + render_out_clear_polys(); + } + + return *this; +} + +void Flattener::flush_polys_to_sink() { + *this << GRB_POL_DARK; /* force render */ + m_sink << GRB_POL_DARK; + + for (auto &poly : d->dark_polys) { + Polygon poly_out; + for (auto &p : poly.vertexes()) { + poly_out.emplace_back(d2p{p.x(), p.y()}); + } + m_sink << poly_out; + } + + d->clear_polys.clear(); + d->dark_polys.clear(); +} + +void Flattener::footer() { + flush_polys_to_sink(); + m_sink.footer(); +} + diff --git a/svg-flatten/src/out_gerber.cpp b/svg-flatten/src/out_gerber.cpp new file mode 100644 index 0000000..9513c0b --- /dev/null +++ b/svg-flatten/src/out_gerber.cpp @@ -0,0 +1,101 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace gerbolyze; +using namespace std; + +SimpleGerberOutput::SimpleGerberOutput(ostream &out, bool only_polys, int digits_int, int digits_frac, double scale, d2p offset, bool flip_polarity) + : StreamPolygonSink(out, only_polys), + m_digits_int(digits_int), + m_digits_frac(digits_frac), + m_offset(offset), + m_scale(scale), + m_flip_pol(flip_polarity) +{ + assert(1 <= digits_int && digits_int <= 9); + assert(0 <= digits_frac && digits_frac <= 9); + m_gerber_scale = round(pow(10, m_digits_frac)); +} + +void SimpleGerberOutput::header_impl(d2p origin, d2p size) { + m_offset[0] += origin[0] * m_scale; + m_offset[1] += origin[1] * m_scale; + m_width = (size[0] - origin[0]) * m_scale; + m_height = (size[1] - origin[1]) * m_scale; + + if (pow(10, m_digits_int-1) < max(m_width, m_height)) { + cerr << "Warning: Input has bounding box too large for " << m_digits_int << "." << m_digits_frac << " gerber resolution!" << endl; + } + + m_out << "%FSLAX" << m_digits_int << m_digits_frac << "Y" << m_digits_int << m_digits_frac << "*%" << endl; + m_out << "%MOMM*%" << endl; + m_out << "%LPD*%" << endl; + m_out << "G01*" << endl; + m_out << "%ADD10C,0.050000*%" << endl; + m_out << "D10*" << endl; +} + +SimpleGerberOutput& SimpleGerberOutput::operator<<(GerberPolarityToken pol) { + assert(pol == GRB_POL_DARK || pol == GRB_POL_CLEAR); + + if ((pol == GRB_POL_DARK) != m_flip_pol) { + m_out << "%LPD*%" << endl; + } else if (pol == GRB_POL_CLEAR) { + m_out << "%LPC*%" << endl; + } + + return *this; +} +SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) { + if (poly.size() < 3) { + cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl; + return *this; + } + + /* NOTE: Clipper and gerber both have different fixed-point scales. We get points in double mm. */ + double x = round((poly[0][0] * m_scale + m_offset[0]) * m_gerber_scale); + double y = round((m_height - poly[0][1] * m_scale + m_offset[1]) * m_gerber_scale); + m_out << "G36*" << endl; + m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal /* isn't C++ a marvel of engineering? */ << (long long int)x + << "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y + << "D02*" << endl; + m_out << "G01*" << endl; + for (size_t i=1; i + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace gerbolyze; +using namespace std; + + +KicadSexpOutput::KicadSexpOutput(ostream &out, string mod_name, string layer, bool only_polys, string ref_text, string val_text, d2p ref_pos, d2p val_pos) + : StreamPolygonSink(out, only_polys), + m_mod_name(mod_name), + m_layer(layer == "auto" ? "unknown" : layer), + m_auto_layer(layer == "auto"), + m_val_text(val_text), + m_ref_pos(ref_pos), + m_val_pos(val_pos) +{ + if (ref_text.empty()) { + m_ref_text = mod_name; + } else { + m_ref_text = ref_text; + } +} + +void KicadSexpOutput::header_impl(d2p, d2p) { + auto tedit = std::time(0); + m_out << "(module " << m_mod_name << " (layer F.Cu) (tedit " << std::hex << std::setfill('0') << std::setw(8) << tedit << ")" << endl; + m_out << " (fp_text reference " << m_ref_text << " (at " << m_ref_pos[0] << " " << m_ref_pos[1] << ") (layer F.SilkS) hide" << endl; + m_out << " (effects (font (size 1 1) (thickness 0.15)))" << endl; + m_out << " )" << endl; + m_out << " (fp_text value " << m_val_text << " (at " << m_val_pos[0] << " " << m_val_pos[1] << ") (layer F.SilkS) hide" << endl; + m_out << " (effects (font (size 1 1) (thickness 0.15)))" << endl; + m_out << " )" << endl; +} + +KicadSexpOutput &KicadSexpOutput::operator<<(GerberPolarityToken pol) { + if (pol == GRB_POL_CLEAR) { + cerr << "Warning: clear polarity not supported since KiCAD manages to have an even worse graphics model than gerber, except it can't excuse itself by its age..... -.-" << endl; + } + + return *this; +} + +KicadSexpOutput &KicadSexpOutput::operator<<(const LayerNameToken &layer_name) { + if (!m_auto_layer) + return *this; + + cerr << "Setting S-Exp export layer to \"" << layer_name.m_name << "\"" << endl; + if (!layer_name.m_name.empty()) { + m_layer = layer_name.m_name; + } else { + m_layer = "unknown"; + } + + return *this; +} + +KicadSexpOutput &KicadSexpOutput::operator<<(const Polygon &poly) { + if (m_auto_layer) { + if (std::find(m_export_layers->begin(), m_export_layers->end(), m_layer) == m_export_layers->end()) { + cerr << "Rejecting S-Exp export layer \"" << m_layer << "\"" << endl; + return *this; + } + } + + if (poly.size() < 3) { + cerr << "Warning: " << poly.size() << "-element polygon passed to KicadSexpOutput" << endl; + return *this; + } + + m_out << " (fp_poly (pts"; + for (auto &p : poly) { + m_out << " (xy " << p[0] << " " << p[1] << ")"; + } + m_out << ")"; + m_out << " (layer " << m_layer << ") (width 0))" << endl; + + return *this; +} + +void KicadSexpOutput::footer_impl() { + m_out << ")" << endl; +} + + diff --git a/svg-flatten/src/out_svg.cpp b/svg-flatten/src/out_svg.cpp new file mode 100644 index 0000000..159bf13 --- /dev/null +++ b/svg-flatten/src/out_svg.cpp @@ -0,0 +1,80 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace gerbolyze; +using namespace std; + +SimpleSVGOutput::SimpleSVGOutput(ostream &out, bool only_polys, int digits_frac, string dark_color, string clear_color) + : StreamPolygonSink(out, only_polys), + m_digits_frac(digits_frac), + m_dark_color(dark_color), + m_clear_color(clear_color), + m_current_color(dark_color) +{ +} + +void SimpleSVGOutput::header_impl(d2p origin, d2p size) { + m_offset[0] = origin[0]; + m_offset[1] = origin[1]; + m_out << "" << endl; +} + +SimpleSVGOutput &SimpleSVGOutput::operator<<(GerberPolarityToken pol) { + if (pol == GRB_POL_DARK) { + m_current_color = m_dark_color; + } else if (pol == GRB_POL_CLEAR) { + m_current_color = m_clear_color; + } else { + assert(false); + } + + return *this; +} + +SimpleSVGOutput &SimpleSVGOutput::operator<<(const Polygon &poly) { + if (poly.size() < 3) { + cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl; + return *this; + } + + m_out << "" << endl; + + return *this; +} + +void SimpleSVGOutput::footer_impl() { + m_out << "" << endl; +} + diff --git a/svg-flatten/src/svg_color.cpp b/svg-flatten/src/svg_color.cpp new file mode 100644 index 0000000..5f1d693 --- /dev/null +++ b/svg-flatten/src/svg_color.cpp @@ -0,0 +1,119 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "svg_color.h" + +#include +#include +#include + +using namespace gerbolyze; +using namespace std; + +/* Map an SVG fill or stroke definition (color, but may also be a pattern) to a gerber color. + * + * This function handles transparency: Transparent SVG colors are mapped such that no gerber output is generated for + * them. + */ +enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, enum gerber_color default_val) { + float alpha = 1.0; + if (!opacity.empty() && opacity[0] != '\0') { + char *endptr = nullptr; + alpha = strtof(opacity.data(), &endptr); + assert(endptr); + assert(*endptr == '\0'); + } + + if (alpha < 0.5f) { + return GRB_NONE; + } + + if (color.empty()) { + return default_val; + } + + if (color == "none") { + return GRB_NONE; + } + + if (color.rfind("url(#", 0) != string::npos) { + return GRB_PATTERN_FILL; + } + + if (color.length() == 7 && color[0] == '#') { + HSVColor hsv(color); + if (hsv.v >= 0.5) { + return GRB_CLEAR; + } + } + + return GRB_DARK; +} + +gerbolyze::RGBColor::RGBColor(string hex) { + assert(hex[0] == '#'); + char *endptr = nullptr; + const char *c = hex.data(); + int rgb = strtol(c + 1, &endptr, 16); + assert(endptr); + assert(endptr == c + 7); + assert(*endptr == '\0'); + r = ((rgb >> 16) & 0xff) / 255.0f; + g = ((rgb >> 8) & 0xff) / 255.0f; + b = ((rgb >> 0) & 0xff) / 255.0f; +}; + +gerbolyze::HSVColor::HSVColor(const RGBColor &color) { + float xmax = fmax(color.r, fmax(color.g, color.b)); + float xmin = fmin(color.r, fmin(color.g, color.b)); + float c = xmax - xmin; + + v = xmax; + + if (c == 0) + h = 0; + else if (v == color.r) + h = 1/3 * (0 + (color.g - color.b) / c); + else if (v == color.g) + h = 1/3 * (2 + (color.b - color.r) / c); + else // v == color.b + h = 1/3 * (4 + (color.r - color.g) / c); + + s = (v == 0) ? 0 : (c/v); +} + +/* Invert gerber color */ +enum gerber_color gerbolyze::gerber_color_invert(enum gerber_color color) { + switch (color) { + case GRB_CLEAR: return GRB_DARK; + case GRB_DARK: return GRB_CLEAR; + default: return color; /* none, pattern */ + } +} + +/* Read node's fill attribute and convert it to a gerber color */ +enum gerber_color gerbolyze::gerber_fill_color(const pugi::xml_node &node) { + return svg_color_to_gerber(node.attribute("fill").value(), node.attribute("fill-opacity").value(), GRB_DARK); +} + +/* Read node's stroke attribute and convert it to a gerber color */ +enum gerber_color gerbolyze::gerber_stroke_color(const pugi::xml_node &node) { + return svg_color_to_gerber(node.attribute("stroke").value(), node.attribute("stroke-opacity").value(), GRB_NONE); +} + + diff --git a/svg-flatten/src/svg_color.h b/svg-flatten/src/svg_color.h new file mode 100644 index 0000000..2817cd9 --- /dev/null +++ b/svg-flatten/src/svg_color.h @@ -0,0 +1,51 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +namespace gerbolyze { + +/* Enum that describes the color with which an SVG primite should be exported */ +enum gerber_color { + GRB_NONE = 0, + GRB_CLEAR, + GRB_DARK, + GRB_PATTERN_FILL, +}; + +class RGBColor { +public: + float r, g, b; + RGBColor(std::string hex); +}; + +class HSVColor { +public: + float h, s, v; + HSVColor(const RGBColor &color); +}; + +enum gerber_color svg_color_to_gerber(std::string color, std::string opacity, enum gerber_color default_val); +enum gerber_color gerber_color_invert(enum gerber_color color); +enum gerber_color gerber_fill_color(const pugi::xml_node &node); +enum gerber_color gerber_stroke_color(const pugi::xml_node &node); + +} /* namespace gerbolyze */ + diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp new file mode 100644 index 0000000..b7ae61b --- /dev/null +++ b/svg-flatten/src/svg_doc.cpp @@ -0,0 +1,480 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include +#include "svg_import_defs.h" +#include "svg_color.h" +#include "svg_geom.h" +#include "svg_path.h" +#include "vec_core.h" + +using namespace gerbolyze; +using namespace std; +using namespace ClipperLib; + +bool gerbolyze::SVGDocument::load(string filename) { + ifstream in_f; + in_f.open(filename); + + return in_f && load(in_f); +} + +bool gerbolyze::SVGDocument::load(istream &in) { + /* Load XML document */ + auto res = svg_doc.load(in); + if (!res) { + cerr << "Cannot parse input file" << endl; + return false; + } + + root_elem = svg_doc.child("svg"); + if (!root_elem) { + cerr << "Input file is missing root element" << endl; + return false; + } + + /* Set up the document's viewport transform */ + istringstream vb_stream(root_elem.attribute("viewBox").value()); + vb_stream >> vb_x >> vb_y >> vb_w >> vb_h; + cerr << "loaded viewbox: " << vb_x << ", " << vb_y << ", " << vb_w << ", " << vb_h << endl; + + page_w = usvg_double_attr(root_elem, "width"); + page_h = usvg_double_attr(root_elem, "height"); + /* usvg resolves all units, but instead of outputting some reasonable absolute length like mm, it converts + * everything to px, which depends on usvg's DPI setting (--dpi). + */ + page_w_mm = page_w / assumed_usvg_dpi * 25.4; + page_h_mm = page_h / assumed_usvg_dpi * 25.4; + if (!(page_w_mm > 0.0 && page_h_mm > 0.0 && page_w_mm < 10e3 && page_h_mm < 10e3)) { + cerr << "Warning: Page has zero or negative size, or is larger than 10 x 10 meters! Parsed size: " << page_w << " x " << page_h << " millimeter" << endl; + } + + if (fabs((vb_w / page_w) / (vb_h / page_h) - 1.0) > 0.001) { + cerr << "Warning: Document has different document unit scale in x and y direction! Output will likely be garbage!" << endl; + } + + /* Get the one document defs element */ + defs_node = root_elem.child("defs"); + if (!defs_node) { + cerr << "Warning: Input file is missing node" << endl; + } + + setup_viewport_clip(); + load_patterns(); + + _valid = true; + return true; +} + +const Paths *gerbolyze::SVGDocument::lookup_clip_path(const pugi::xml_node &node) { + string id(usvg_id_url(node.attribute("clip-path").value())); + if (id.empty() || clip_path_map.count(id) == 0) { + return nullptr; + } + return &clip_path_map[id]; +} + +Pattern *gerbolyze::SVGDocument::lookup_pattern(const string id) { + if (id.empty() || pattern_map.count(id) == 0) { + return nullptr; + } + return &pattern_map[id]; +}; + +/* Used to convert mm values from configuration such as the minimum feature size into document units. */ +double gerbolyze::SVGDocument::mm_to_doc_units(double mm) const { + return mm * (vb_w / page_w_mm); +} + +double gerbolyze::SVGDocument::doc_units_to_mm(double px) const { + return px / (vb_w / page_w_mm); +} + +bool IDElementSelector::match(const pugi::xml_node &node, bool included, bool is_root) const { + string id = node.attribute("id").value(); + if (is_root && layers) { + bool layer_match = std::find(layers->begin(), layers->end(), id) != layers->end(); + if (!layer_match) { + cerr << "Rejecting layer \"" << id << "\"" << endl; + return false; + } + } + + if (include.empty() && exclude.empty()) + return true; + + bool include_match = std::find(include.begin(), include.end(), id) != include.end(); + bool exclude_match = std::find(exclude.begin(), exclude.end(), id) != exclude.end(); + + if (exclude_match || (!included && !include_match)) { + return false; + } + + return true; +} + +/* Recursively export all SVG elements in the given group. */ +void gerbolyze::SVGDocument::export_svg_group(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &group, Paths &parent_clip_path, const ElementSelector *sel, bool included, bool is_root) { + + /* Load clip paths from defs given bezier flattening tolerance from rset */ + load_clips(rset); + + /* Enter the group's coordinate system */ + xform2d local_xf(mat); + local_xf.transform(xform2d(group.attribute("transform").value())); + + /* Fetch clip path from global registry and transform it into document coordinates. */ + Paths clip_path; + auto *lookup = lookup_clip_path(group); + if (!lookup) { + string id(usvg_id_url(group.attribute("clip-path").value())); + if (!id.empty()) { + cerr << "Warning: Cannot find clip path with ID \"" << group.attribute("clip-path").value() << "\" for group \"" << group.attribute("id").value() << "\"." << endl; + } + + } else { + clip_path = *lookup; + } + local_xf.transform_paths(clip_path); + + /* Clip against parent's clip path (both are now in document coordinates) */ + if (!parent_clip_path.empty()) { + if (!clip_path.empty()) { + combine_clip_paths(parent_clip_path, clip_path, clip_path); + } else { + clip_path = parent_clip_path; + } + } + + /* Iterate over the group's children, exporting them one by one. */ + for (const auto &node : group.children()) { + if (sel && !sel->match(node, included, is_root)) + continue; + + string name(node.name()); + if (name == "g") { + if (is_root) { /* Treat top-level groups as "layers" like inkscape does. */ + cerr << "Forwarding layer name to sink: \"" << node.attribute("id").value() << "\"" << endl; + LayerNameToken tok { node.attribute("id").value() }; + *polygon_sink << tok; + } + + export_svg_group(local_xf, rset, node, clip_path, sel, true); + + if (is_root) { + LayerNameToken tok {""}; + *polygon_sink << tok; + } + + } else if (name == "path") { + export_svg_path(local_xf, rset, node, clip_path); + + } else if (name == "image") { + ImageVectorizer *vec = rset.m_vec_sel.select(node); + if (!vec) { + cerr << "Cannot resolve vectorizer for node \"" << node.attribute("id").value() << "\"" << endl; + continue; + } + + double min_feature_size_px = mm_to_doc_units(rset.m_minimum_feature_size_mm); + vec->vectorize_image(local_xf, node, clip_path, *polygon_sink, min_feature_size_px); + delete vec; + + } else if (name == "defs") { + /* ignore */ + } else { + cerr << " Unexpected child: <" << node.name() << ">" << endl; + } + } +} + +/* Export an SVG path element to gerber. Apply patterns and clip on the fly. */ +void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &node, Paths &clip_path) { + enum gerber_color fill_color = gerber_fill_color(node); + enum gerber_color stroke_color = gerber_stroke_color(node); + + double stroke_width = usvg_double_attr(node, "stroke-width", /* default */ 1.0); + assert(stroke_width > 0.0); + enum ClipperLib::EndType end_type = clipper_end_type(node); + enum ClipperLib::JoinType join_type = clipper_join_type(node); + vector dasharray; + parse_dasharray(node, dasharray); + /* TODO add stroke-miterlimit */ + + if (!fill_color && !stroke_color) { /* Ignore "transparent" paths */ + return; + } + + /* Load path from SVG path data and transform into document units. */ + xform2d local_xf(mat); + local_xf.transform(xform2d(node.attribute("transform").value())); + + PolyTree ptree_stroke; + PolyTree ptree_fill; + PolyTree ptree; + load_svg_path(local_xf, node, ptree_stroke, ptree_fill, rset.curve_tolerance_mm); + + Paths open_paths, closed_paths, fill_paths; + OpenPathsFromPolyTree(ptree_stroke, open_paths); + ClosedPathsFromPolyTree(ptree_stroke, closed_paths); + PolyTreeToPaths(ptree_fill, fill_paths); + + /* Skip filling for transparent fills */ + if (fill_color) { + /* Clip paths. Consider all paths closed for filling. */ + if (!clip_path.empty()) { + Clipper c; + c.AddPaths(fill_paths, ptSubject, /* closed */ true); + c.AddPaths(clip_path, ptClip, /* closed */ true); + c.StrictlySimple(true); + /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ + c.Execute(ctIntersection, ptree, pftNonZero, pftNonZero); + PolyTreeToPaths(ptree, fill_paths); + } + + /* Call out to pattern tiler for pattern fills. The path becomes the clip here. */ + if (fill_color == GRB_PATTERN_FILL) { + string fill_pattern_id = usvg_id_url(node.attribute("fill").value()); + Pattern *pattern = lookup_pattern(fill_pattern_id); + if (!pattern) { + cerr << "Warning: Fill pattern with id \"" << fill_pattern_id << "\" not found." << endl; + + } else { + pattern->tile(local_xf, rset, fill_paths); + } + + } else { /* solid fill */ + Paths f_polys; + /* Important for gerber spec compliance and also for reliable rendering results irrespective of board house + * and gerber viewer. */ + dehole_polytree(ptree_fill, f_polys); + + /* export gerber */ + for (const auto &poly : f_polys) { + vector> out; + for (const auto &p : poly) + out.push_back(std::array{ + ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale + }); + *polygon_sink << (fill_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out; + } + } + } + + if (stroke_color && stroke_width > 0.0) { + ClipperOffset offx; + offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */ + + /* For stroking we have to separately handle open and closed paths */ + for (const auto &poly : closed_paths) { + if (poly.empty()) /* do we need this? */ + continue; + + /* Special case: A closed path becomes a number of open paths when it is dashed. */ + if (dasharray.empty()) { + offx.AddPath(poly, join_type, etClosedLine); + + } else { + Path poly_copy(poly); + poly_copy.push_back(poly[0]); + Paths out; + dash_path(poly_copy, out, dasharray); + offx.AddPaths(out, join_type, end_type); + } + } + + for (const auto &poly : open_paths) { + Paths out; + dash_path(poly, out, dasharray); + offx.AddPaths(out, join_type, end_type); + } + + /* Execute clipper offset operation to generate stroke outlines */ + offx.Execute(ptree, 0.5 * stroke_width * clipper_scale); + + /* Clip. Note that after the outline, all we have is closed paths as any open path's stroke outline is itself + * a closed path. */ + if (!clip_path.empty()) { + Clipper c; + + Paths outline_paths; + PolyTreeToPaths(ptree, outline_paths); + c.AddPaths(outline_paths, ptSubject, /* closed */ true); + c.AddPaths(clip_path, ptClip, /* closed */ true); + c.StrictlySimple(true); + /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ + c.Execute(ctIntersection, ptree, pftNonZero, pftNonZero); + } + + /* Call out to pattern tiler for pattern strokes. The stroke's outline becomes the clip here. */ + if (stroke_color == GRB_PATTERN_FILL) { + string stroke_pattern_id = usvg_id_url(node.attribute("stroke").value()); + Pattern *pattern = lookup_pattern(stroke_pattern_id); + if (!pattern) { + cerr << "Warning: Fill pattern with id \"" << stroke_pattern_id << "\" not found." << endl; + + } else { + Paths clip; + PolyTreeToPaths(ptree, clip); + pattern->tile(local_xf, rset, clip); + } + + } else { + Paths s_polys; + dehole_polytree(ptree, s_polys); + + /* export gerber */ + for (const auto &poly : s_polys) { + vector> out; + for (const auto &p : poly) + out.push_back(std::array{ + ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale + }); + *polygon_sink << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out; + } + } + } +} + +void gerbolyze::SVGDocument::render(const RenderSettings &rset, PolygonSink &sink, const ElementSelector *sel) { + assert(_valid); + /* Export the actual SVG document to both SVG for debuggin and to gerber. We do this as we go, i.e. we immediately + * process each element to gerber as we encounter it instead of first rendering everything to a giant list of gerber + * primitives and then serializing those later. Exporting them on the fly saves a ton of memory and is much faster. + */ + polygon_sink = &sink; + sink.header({vb_x, vb_y}, {vb_w, vb_h}); + ClipperLib::Clipper c; + c.AddPaths(vb_paths, ptSubject, /* closed */ true); + ClipperLib::IntRect bbox = c.GetBounds(); + cerr << "document viewbox clip: bbox={" << bbox.left << ", " << bbox.top << "} - {" << bbox.right << ", " << bbox.bottom << "}" << endl; + xform2d xf; + export_svg_group(xf, rset, root_elem, vb_paths, sel, false, true); + sink.footer(); +} + +void gerbolyze::SVGDocument::render_to_list(const RenderSettings &rset, vector> &out, const ElementSelector *sel) { + LambdaPolygonSink sink([&out](const Polygon &poly, GerberPolarityToken pol) { + out.emplace_back(pair{poly, pol}); + }); + render(rset, sink, sel); +} + +void gerbolyze::SVGDocument::setup_viewport_clip() { + /* Set up view port clip path */ + Path vb_path; + for (auto &elem : vector> {{vb_x, vb_y}, {vb_x+vb_w, vb_y}, {vb_x+vb_w, vb_y+vb_h}, {vb_x, vb_y+vb_h}}) { + double x = elem.first, y = elem.second; + vb_path.push_back({ (cInt)round(x * clipper_scale), (cInt)round(y * clipper_scale) }); + } + vb_paths.push_back(vb_path); + + ClipperLib::Clipper c; + c.AddPaths(vb_paths, ptSubject, /* closed */ true); +} + +void gerbolyze::SVGDocument::load_patterns() { + /* Set up document-wide pattern registry. Load patterns from node. */ + for (const auto &node : defs_node.children("pattern")) { + pattern_map.emplace(std::piecewise_construct, std::forward_as_tuple(node.attribute("id").value()), std::forward_as_tuple(node, *this)); + } +} + +void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) { + /* Set up document-wide clip path registry: Extract clip path definitions from element */ + for (const auto &node : defs_node.children("clipPath")) { + + xform2d local_xf(node.attribute("transform").value()); + + string meta_clip_path_id(usvg_id_url(node.attribute("clip-path").value())); + Clipper c; + + /* The clipPath node can only contain children. usvg converts all geometric objects (rect etc.) to + * s. Raster images are invalid inside a clip path. usvg removes all groups that are not relevant to + * rendering, and the only way a group might stay is if it affects rasterization (e.g. through mask, clipPath). + */ + for (const auto &child : node.children("path")) { + PolyTree ptree_stroke; /* discarded */ + PolyTree ptree_fill; + /* TODO: we currently only support clipPathUnits="userSpaceOnUse", not "objectBoundingBox". */ + xform2d child_xf(local_xf); + child_xf.transform(xform2d(child.attribute("transform").value())); + + load_svg_path(child_xf, child, ptree_stroke, ptree_fill, rset.curve_tolerance_mm); + + Paths paths; + PolyTreeToPaths(ptree_fill, paths); + c.AddPaths(paths, ptSubject, /* closed */ false); + } + + /* Support clip paths that themselves have clip paths */ + if (!meta_clip_path_id.empty()) { + if (clip_path_map.count(meta_clip_path_id) > 0) { + /* all clip paths must be closed */ + c.AddPaths(clip_path_map[meta_clip_path_id], ptClip, /* closed */ true); + + } else { + cerr << "Warning: Cannot find clip path with ID \"" << meta_clip_path_id << "\", ignoring." << endl; + } + } + + PolyTree ptree; + c.StrictlySimple(true); + /* This unions all child s together and at the same time applies any meta clip path. */ + /* The fill rules are both nonzero since both subject and clip have already been normalized by clipper. */ + c.Execute(ctUnion, ptree, pftNonZero, pftNonZero); + /* Insert into document clip path map */ + PolyTreeToPaths(ptree, clip_path_map[node.attribute("id").value()]); + } +} + +/* Note: These values come from KiCAD's common/lset.cpp. KiCAD uses *multiple different names* for the same layer in + * different places, and not all of them are stable. Sometimes, these names change without notice. If this list isn't + * up-to-date, it's not my fault. Still, please file an issue. */ +const std::vector gerbolyze::kicad_default_layers ({ + /* Copper */ + "F.Cu", + "In1.Cu", "In2.Cu", "In3.Cu", "In4.Cu", "In5.Cu", "In6.Cu", "In7.Cu", "In8.Cu", + "In9.Cu", "In10.Cu", "In11.Cu", "In12.Cu", "In13.Cu", "In14.Cu", "In15.Cu", "In16.Cu", + "In17.Cu", "In18.Cu", "In19.Cu", "In20.Cu", "In21.Cu", "In22.Cu", "In23.Cu", + "In24.Cu", "In25.Cu", "In26.Cu", "In27.Cu", "In28.Cu", "In29.Cu", "In30.Cu", + "B.Cu", + + /* Technical layers */ + "B.Adhes", "F.Adhes", + "B.Paste", "F.Paste", + "B.SilkS", "F.SilkS", + "B.Mask", "F.Mask", + + /* User layers */ + "Dwgs.User", + "Cmts.User", + "Eco1.User", "Eco2.User", + "Edge.Cuts", + "Margin", + + /* Footprint layers */ + "F.CrtYd", "B.CrtYd", + "F.Fab", "B.Fab", + + /* Layers for user scripting etc. */ + "User.1", "User.2", "User.3", "User.4", "User.5", "User.6", "User.7", "User.8", "User.9", + }); diff --git a/svg-flatten/src/svg_geom.cpp b/svg-flatten/src/svg_geom.cpp new file mode 100644 index 0000000..f836567 --- /dev/null +++ b/svg-flatten/src/svg_geom.cpp @@ -0,0 +1,160 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "svg_geom.h" + +#include +#include +#include +#include +#include +#include +#include "svg_import_defs.h" + +using namespace ClipperLib; +using namespace std; + +/* Get bounding box of a Clipper Paths */ +IntRect gerbolyze::get_paths_bounds(const Paths &paths) { + if (paths.empty()) { + return {0, 0, 0, 0}; + } + + if (paths[0].empty()) { + return {0, 0, 0, 0}; + } + + IntPoint p0 = paths[0][0]; + cInt x0=p0.X, y0=p0.Y, x1=p0.X, y1=p0.Y; + + for (const Path &p : paths) { + for (const IntPoint ip : p) { + if (ip.X < x0) + x0 = ip.X; + + if (ip.Y < y0) + y0 = ip.Y; + + if (ip.X > x1) + x1 = ip.X; + + if (ip.Y > y1) + y1 = ip.Y; + } + } + + return {x0, y0, x1, y1}; +} + +enum ClipperLib::PolyFillType gerbolyze::clipper_fill_rule(const pugi::xml_node &node) { + string val(node.attribute("fill-rule").value()); + if (val == "evenodd") + return ClipperLib::pftEvenOdd; + else + return ClipperLib::pftNonZero; /* default */ +} + +enum ClipperLib::EndType gerbolyze::clipper_end_type(const pugi::xml_node &node) { + string val(node.attribute("stroke-linecap").value()); + if (val == "round") + return ClipperLib::etOpenRound; + + if (val == "square") + return ClipperLib::etOpenSquare; + + return ClipperLib::etOpenButt; +} + +enum ClipperLib::JoinType gerbolyze::clipper_join_type(const pugi::xml_node &node) { + string val(node.attribute("stroke-linejoin").value()); + if (val == "round") + return ClipperLib::jtRound; + + if (val == "bevel") + return ClipperLib::jtSquare; + + return ClipperLib::jtMiter; +} + +static void dehole_polytree_worker(PolyNode &ptree, Paths &out, queue &todo) { + for (int i=0; iIsHole()); + + /* First, recursively process inner polygons. */ + for (int j=0; jChildCount(); j++) { + PolyNode *child = nod->Childs[j]; + assert(child); + assert(child->IsHole()); + + if (child->ChildCount() > 0) { + dehole_polytree_worker(*child, out, todo); + } + } + + if (nod->ChildCount() == 0) { + out.push_back(nod->Contour); + + } else { + /* Do not add children's children, those were handled in the recursive call above */ + Clipper c; + c.AddPath(nod->Contour, ptSubject, /* closed= */ true); + for (int k=0; kChildCount(); k++) { + c.AddPath(nod->Childs[k]->Contour, ptSubject, /* closed= */ true); + } + + /* Find a viable cut: Cut from top-left bounding box corner, through two subsequent points on the hole + * outline and to top-right bbox corner. */ + IntRect bbox = c.GetBounds(); + Path tri = { { bbox.left, bbox.top }, nod->Childs[0]->Contour[0], nod->Childs[0]->Contour[1], { bbox.right, bbox.top } }; + c.AddPath(tri, ptClip, true); + + c.StrictlySimple(true); + /* Execute twice, once for intersection fragment and once for difference fragment. Note that this will yield + * at least two, but possibly more polygons. */ + c.Execute(ctDifference, todo.emplace(), pftNonZero); + c.Execute(ctIntersection, todo.emplace(), pftNonZero); + } + } +} + +/* Take a Clipper polytree, i.e. a description of a set of polygons, their holes and their inner polygons, and remove + * all holes from it. We remove holes by splitting each polygon that has a hole into two or more pieces so that the hole + * is no more. These pieces perfectly fit each other so there is no visual or functional difference. + */ +void gerbolyze::dehole_polytree(PolyTree &ptree, Paths &out) { + queue todo; + dehole_polytree_worker(ptree, out, todo); + while (!todo.empty()) { + dehole_polytree_worker(todo.front(), out, todo); + todo.pop(); + } +} + + +/* Intersect two clip paths. Both must share a coordinate system. */ +void gerbolyze::combine_clip_paths(Paths &in_a, Paths &in_b, Paths &out) { + Clipper c; + c.StrictlySimple(true); + c.AddPaths(in_a, ptClip, /* closed */ true); + c.AddPaths(in_b, ptSubject, /* closed */ true); + /* Nonzero fill since both input clip paths must already have been preprocessed by clipper. */ + c.Execute(ctIntersection, out, pftNonZero); +} + diff --git a/svg-flatten/src/svg_geom.h b/svg-flatten/src/svg_geom.h new file mode 100644 index 0000000..1e9f5cd --- /dev/null +++ b/svg-flatten/src/svg_geom.h @@ -0,0 +1,34 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace gerbolyze { + + ClipperLib::IntRect get_paths_bounds(const ClipperLib::Paths &paths); + enum ClipperLib::PolyFillType clipper_fill_rule(const pugi::xml_node &node); + enum ClipperLib::EndType clipper_end_type(const pugi::xml_node &node); + enum ClipperLib::JoinType clipper_join_type(const pugi::xml_node &node); + void dehole_polytree(ClipperLib::PolyTree &ptree, ClipperLib::Paths &out); + void combine_clip_paths(ClipperLib::Paths &in_a, ClipperLib::Paths &in_b, ClipperLib::Paths &out); + +} /* namespace gerbolyze */ + diff --git a/svg-flatten/src/svg_import_defs.h b/svg-flatten/src/svg_import_defs.h new file mode 100644 index 0000000..e25c52c --- /dev/null +++ b/svg-flatten/src/svg_import_defs.h @@ -0,0 +1,49 @@ +/* + * This program source code file is part of KICAD, a free EDA CAD application. + * + * Copyright (C) 2016 CERN + * @author Janito V. Ferreira Filho + * Copyright (C) 2018-2019 KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef SVG_IMPORT_DEFS_H +#define SVG_IMPORT_DEFS_H + +#include + +template +constexpr T ipow(T num, unsigned int pow) +{ + return (pow >= sizeof(unsigned int)*8) ? 0 : + pow == 0 ? 1 : num * ipow(num, pow-1); +} + +constexpr int CAIRO_PRECISION = 7; +constexpr double clipper_scale = ipow(10.0, CAIRO_PRECISION); + +#define JC_VORONOI_IMPLEMENTATION +#define JCV_REAL_TYPE double +#define JCV_ATAN2 atan2 +#define JCV_SQRT sqrt +#define JCV_FLT_MAX DBL_MAX +#define JCV_PI 3.141592653589793115997963468544185161590576171875 +//define JCV_EDGE_INTERSECT_THRESHOLD 1.0e-10F + +#endif /* SVG_IMPORT_DEFS_H */ diff --git a/svg-flatten/src/svg_import_util.cpp b/svg-flatten/src/svg_import_util.cpp new file mode 100644 index 0000000..cb60482 --- /dev/null +++ b/svg-flatten/src/svg_import_util.cpp @@ -0,0 +1,66 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include "base64.h" +#include "svg_import_util.h" + +using namespace std; + +/* Read a double value formatted like usvg formats doubles from an SVG attribute */ +double gerbolyze::usvg_double_attr(const pugi::xml_node &node, const char *attr, double default_value) { + const auto *val = node.attribute(attr).value(); + if (*val == '\0') + return default_value; + + return atof(val); +} + +/* Read an url from an usvg attribute */ +string gerbolyze::usvg_id_url(string attr) { + if (attr.rfind("url(#", 0) == string::npos) + return string(); + + attr = attr.substr(strlen("url(#")); + attr = attr.substr(0, attr.size()-1); + return attr; +} + +gerbolyze::RelativeUnits gerbolyze::map_str_to_units(string str, gerbolyze::RelativeUnits default_val) { + if (str == "objectBoundingBox") + return SVG_ObjectBoundingBox; + else if (str == "userSpaceOnUse") + return SVG_UserSpaceOnUse; + return default_val; +} + +/* Cf. https://tools.ietf.org/html/rfc2397 */ +string gerbolyze::parse_data_iri(const string &data_url) { + if (data_url.rfind("data:", 0) == string::npos) /* check if url starts with "data:" */ + return string(); + + size_t foo = data_url.find("base64,"); + if (foo == string::npos) /* check if this is actually a data URL */ + return string(); + + size_t b64_begin = data_url.find_first_not_of(" ", foo + strlen("base64,")); + assert(b64_begin != string::npos); + + return base64_decode(data_url.substr(b64_begin)); +} + diff --git a/svg-flatten/src/svg_import_util.h b/svg-flatten/src/svg_import_util.h new file mode 100644 index 0000000..6c67205 --- /dev/null +++ b/svg-flatten/src/svg_import_util.h @@ -0,0 +1,53 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "svg_import_defs.h" + +namespace gerbolyze { + +/* Coordinate system selection for things like "patternContentUnits" */ +enum RelativeUnits { + SVG_UnknownUnits = 0, + SVG_UserSpaceOnUse, + SVG_ObjectBoundingBox, +}; + +double usvg_double_attr(const pugi::xml_node &node, const char *attr, double default_value=0.0); +std::string usvg_id_url(std::string attr); +RelativeUnits map_str_to_units(std::string str, RelativeUnits default_val=SVG_UnknownUnits); +std::string parse_data_iri(const std::string &data_url); + +} /* namespace gerbolyze */ + diff --git a/svg-flatten/src/svg_path.cpp b/svg-flatten/src/svg_path.cpp new file mode 100644 index 0000000..8b5be9e --- /dev/null +++ b/svg-flatten/src/svg_path.cpp @@ -0,0 +1,269 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "svg_import_defs.h" +#include "svg_path.h" +#include "flatten.hpp" + +using namespace std; + +static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clipper &c_stroke, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) { + istringstream in(path_data); + + string cmd; + gerbolyze::d2p a, b, c, d; + + ClipperLib::Path in_poly; + + bool first = true; + bool has_closed = false; + int num_subpaths = 0; + while (!in.eof()) { + in >> cmd; + assert (!in.fail()); + assert(!first || cmd == "M"); + + if (cmd == "Z") { /* Close path */ + c_stroke.AddPath(in_poly, ClipperLib::ptSubject, true); + c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); + + has_closed = true; + in_poly.clear(); + num_subpaths += 1; + + } else if (cmd == "M") { /* Move to */ + if (!first && !in_poly.empty()) { + c_stroke.AddPath(in_poly, ClipperLib::ptSubject, false); + c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); + num_subpaths += 1; + in_poly.clear(); + } + + in >> a[0] >> a[1]; + assert (!in.fail()); /* guaranteed by usvg */ + + /* We need to transform all points ourselves here, and cannot use the transform feature of cairo_to_clipper: + * Our transform may contain offsets, and clipper only passes its data into cairo's transform functions + * after scaling up to its internal fixed-point ints, but it does not scale the transform accordingly. This + * means a scale/rotation we set before calling clipper works out fine, but translations get lost as they + * get scaled by something like 1e-6. + */ + a = mat.doc2phys(a); + + in_poly.emplace_back(ClipperLib::IntPoint{ + (ClipperLib::cInt)round(a[0]*clipper_scale), + (ClipperLib::cInt)round(a[1]*clipper_scale) + }); + + } else if (cmd == "L") { /* Line to */ + in >> a[0] >> a[1]; + assert (!in.fail()); /* guaranteed by usvg */ + + a = mat.doc2phys(a); + in_poly.emplace_back(ClipperLib::IntPoint{ + (ClipperLib::cInt)round(a[0]*clipper_scale), + (ClipperLib::cInt)round(a[1]*clipper_scale) + }); + + } else { /* Curve to */ + assert(cmd == "C"); /* guaranteed by usvg */ + in >> b[0] >> b[1]; /* first control point */ + in >> c[0] >> c[1]; /* second control point */ + in >> d[0] >> d[1]; /* end point */ + assert (!in.fail()); /* guaranteed by usvg */ + + b = mat.doc2phys(b); + c = mat.doc2phys(c); + d = mat.doc2phys(d); + + gerbolyze::curve4_div c4div(distance_tolerance_mm); + c4div.run(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]); + + for (auto &pt : c4div.points()) { + in_poly.emplace_back(ClipperLib::IntPoint{ + (ClipperLib::cInt)round(pt[0]*clipper_scale), + (ClipperLib::cInt)round(pt[1]*clipper_scale) + }); + } + + a = d; /* set last point to curve end point */ + } + + first = false; + } + + if (!in_poly.empty()) { + c_stroke.AddPath(in_poly, ClipperLib::ptSubject, false); + c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); + num_subpaths += 1; + } + + return {has_closed, num_subpaths > 1}; +} + +void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::PolyTree &ptree_stroke, ClipperLib::PolyTree &ptree_fill, double curve_tolerance) { + auto *path_data = node.attribute("d").value(); + auto fill_rule = clipper_fill_rule(node); + + /* For open paths, clipper does not correctly remove self-intersections. Thus, we pass everything into + * clipper twice: Once with all paths set to "closed" to compute fill areas, and once with correct + * open/closed properties for stroke offsetting. */ + ClipperLib::Clipper c_stroke; + ClipperLib::Clipper c_fill; + c_stroke.StrictlySimple(true); + c_fill.StrictlySimple(true); + auto res = flatten_path(mat, c_stroke, c_fill, path_data, curve_tolerance); + bool has_closed = res.first, has_multiple = res.second; + + if (!has_closed && !has_multiple) { + /* FIXME: Workaround! + * + * When we render silkscreen layers from gerbv's output, we get a lot of two-point paths (lines). Many of these are + * horizontal. Now, clipper seems to have a bug (probably related to its scan-line algorithm) that makes it + * misbehave here: + * + * It seems that when the input paths are all perfectly colinear and horizontal, so that the resulting bounding box + * has zero height, clipper doesn't output anything. At least for open input paths. + * + * Since there is no way to get paths out of a Clipper once they're Add'ed, we work around this by just doing an + * intersection with a maximum-size rectangle instead, that seems to work. + * + * TODO: Fix clipper instead. + */ + auto le_min = -ClipperLib::loRange; + auto le_max = ClipperLib::hiRange; + ClipperLib::Path p = {{le_min, le_min}, {le_max, le_min}, {le_max, le_max}, {le_min, le_max}}; + + c_stroke.AddPath(p, ClipperLib::ptClip, /* closed= */ true); + c_stroke.Execute(ClipperLib::ctIntersection, ptree_stroke, fill_rule, ClipperLib::pftNonZero); + + c_fill.AddPath(p, ClipperLib::ptClip, /* closed= */ true); + c_fill.Execute(ClipperLib::ctIntersection, ptree_fill, fill_rule, ClipperLib::pftNonZero); + + } else { + /* We cannot clip the polygon here since that would produce incorrect results for our stroke. */ + c_stroke.Execute(ClipperLib::ctUnion, ptree_stroke, fill_rule, ClipperLib::pftNonZero); + c_fill.Execute(ClipperLib::ctUnion, ptree_fill, fill_rule, ClipperLib::pftNonZero); + } +} + +void gerbolyze::parse_dasharray(const pugi::xml_node &node, vector &out) { + out.clear(); + + string val(node.attribute("stroke-dasharray").value()); + if (val.empty() || val == "none") + return; + + istringstream desc_stream(val); + while (!desc_stream.eof()) { + /* usvg says the array only contains unitless (px) values. I don't know what resvg does with percentages inside + * dash arrays. We just assume everything is a unitless number here. In case usvg passes through percentages, + * well, bad luck. They are a kind of weird thing inside a dash array in the first place. */ + double d; + desc_stream >> d; + out.push_back(d); + } + + assert(out.size() % 2 == 0); /* according to resvg spec */ +} + +/* Take a Clipper path in clipper-scaled document units, and apply the given SVG dash array to it. Do this by walking + * the path from start to end while emitting dashes. */ +void gerbolyze::dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const vector dasharray, double dash_offset) { + out.clear(); + if (dasharray.empty() || in.size() < 2) { + out.push_back(in); + return; + } + + size_t dash_idx = 0; + size_t num_dashes = dasharray.size(); + while (dash_offset > dasharray[dash_idx]) { + dash_offset -= dasharray[dash_idx]; + dash_idx = (dash_idx + 1) % num_dashes; + } + + double dash_remaining = dasharray[dash_idx] - dash_offset; + + ClipperLib::Path current_dash; + current_dash.push_back(in[0]); + double dbg_total_len = 0.0; + for (size_t i=1; i dasharray[dash_idx]) { + offset += dasharray[dash_idx]; + + double dash_frac = offset/dist; + double x = x1 + (x2 - x1) * dash_frac, + y = y1 + (y2 - y1) * dash_frac; + ClipperLib::IntPoint intermediate {(ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale)}; + + /* end this dash */ + current_dash.push_back(intermediate); + if (dash_idx%2 == 0) { /* dash */ + out.push_back(current_dash); + } /* else space */ + dash_idx = (dash_idx + 1) % num_dashes; + + /* start next dash */ + current_dash.clear(); + current_dash.push_back(intermediate); + } + + dash_remaining = dasharray[dash_idx] - (dist - offset); + current_dash.push_back(p2); + } + } +} + diff --git a/svg-flatten/src/svg_path.h b/svg-flatten/src/svg_path.h new file mode 100644 index 0000000..611f517 --- /dev/null +++ b/svg-flatten/src/svg_path.h @@ -0,0 +1,30 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "svg_geom.h" +#include "geom2d.hpp" + +namespace gerbolyze { +void load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::PolyTree &ptree_stroke, ClipperLib::PolyTree &ptree_fill, double curve_tolerance); +void parse_dasharray(const pugi::xml_node &node, std::vector &out); +void dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const std::vector dasharray, double dash_offset=0.0); +} + diff --git a/svg-flatten/src/svg_pattern.cpp b/svg-flatten/src/svg_pattern.cpp new file mode 100644 index 0000000..a122975 --- /dev/null +++ b/svg-flatten/src/svg_pattern.cpp @@ -0,0 +1,109 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include "svg_import_util.h" +#include "svg_pattern.h" +#include "svg_import_defs.h" +#include "svg_geom.h" +#include + +using namespace std; + +gerbolyze::Pattern::Pattern(const pugi::xml_node &node, SVGDocument &doc) : _node(node), doc(&doc) { + /* Read pattern attributes from SVG node */ + cerr << "creating pattern for node with id \"" << node.attribute("id").value() << "\"" << endl; + x = usvg_double_attr(node, "x"); + y = usvg_double_attr(node, "y"); + w = usvg_double_attr(node, "width"); + h = usvg_double_attr(node, "height"); + + patternTransform = xform2d(node.attribute("patternTransform").value()); + + bool invert_success = false; + patternTransform_inv = xform2d(patternTransform).invert(&invert_success); + if (!invert_success) { + cerr << "Warning: Cannot invert patternTransform matrix on pattern \"" << node.attribute("id").value() << "\"." << endl; + } + + string vb_s(node.attribute("viewBox").value()); + has_vb = !vb_s.empty(); + if (has_vb) { + istringstream vb_stream(vb_s); + vb_stream >> vb_x >> vb_y >> vb_w >> vb_h; + } + + patternUnits = map_str_to_units(node.attribute("patternUnits").value(), SVG_ObjectBoundingBox); + patternContentUnits = map_str_to_units(node.attribute("patternContentUnits").value(), SVG_UserSpaceOnUse); +} + +/* Tile pattern into gerber. Note that this function may be called several times in case the pattern is + * referenced from multiple places, so we must not clobber any of the object's state. */ +void gerbolyze::Pattern::tile (xform2d &mat, const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip) { + assert(doc); + + /* Transform x, y, w, h from pattern coordinate space into parent coordinates by applying the inverse + * patternTransform. This is necessary so we iterate over the correct bounds when tiling below */ + d2p pos_xf = patternTransform_inv.doc2phys(d2p{x, y}); + double inst_x = pos_xf[0], inst_y = pos_xf[1]; + double inst_w = patternTransform_inv.doc2phys_dist(w); + double inst_h = patternTransform_inv.doc2phys_dist(h); + + ClipperLib::IntRect clip_bounds = get_paths_bounds(clip); + double bx = clip_bounds.left / clipper_scale; + double by = clip_bounds.top / clipper_scale; + double bw = (clip_bounds.right - clip_bounds.left) / clipper_scale; + double bh = (clip_bounds.bottom - clip_bounds.top) / clipper_scale; + + if (patternUnits == SVG_ObjectBoundingBox) { + inst_x *= bw; + inst_y *= bh; + inst_w *= bw; + inst_h *= bh; + } + + /* Switch to pattern coordinates */ + xform2d local_xf(mat); + local_xf.translate(bx, by); + local_xf.transform(patternTransform); + + /* Iterate over all pattern tiles in pattern coordinates */ + for (double inst_off_x = fmod(inst_x, inst_w) - inst_w; + inst_off_x < bw + inst_w; + inst_off_x += inst_w) { + + for (double inst_off_y = fmod(inst_y, inst_h) - inst_h; + inst_off_y < bh + inst_h; + inst_off_y += inst_h) { + + xform2d elem_xf(local_xf); + /* Change into this individual tile's coordinate system */ + elem_xf.translate(inst_off_x, inst_off_y); + if (has_vb) { + elem_xf.translate(vb_x, vb_y); + elem_xf.scale(inst_w / vb_w, inst_h / vb_h); + } else if (patternContentUnits == SVG_ObjectBoundingBox) { + elem_xf.scale(bw, bh); + } + + /* Export the pattern tile's content like a group */ + doc->export_svg_group(elem_xf, rset, _node, clip); + } + } +} + diff --git a/svg-flatten/src/svg_pattern.h b/svg-flatten/src/svg_pattern.h new file mode 100644 index 0000000..56444dd --- /dev/null +++ b/svg-flatten/src/svg_pattern.h @@ -0,0 +1,54 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include + +#include "svg_import_util.h" +#include "geom2d.hpp" + +namespace gerbolyze { + +class SVGDocument; +class RenderSettings; + +class Pattern { +public: + Pattern() {} + Pattern(const pugi::xml_node &node, SVGDocument &doc); + + void tile (xform2d &mat, const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip); + +private: + double x, y, w, h; + double vb_x, vb_y, vb_w, vb_h; + bool has_vb; + xform2d patternTransform; + xform2d patternTransform_inv; + enum RelativeUnits patternUnits; + enum RelativeUnits patternContentUnits; + const pugi::xml_node _node; + SVGDocument *doc = nullptr; +}; + +} /* namespace gerbolyze */ + diff --git a/svg-flatten/src/vec_core.cpp b/svg-flatten/src/vec_core.cpp new file mode 100644 index 0000000..6951e4f --- /dev/null +++ b/svg-flatten/src/vec_core.cpp @@ -0,0 +1,533 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include "svg_import_util.h" +#include "vec_core.h" +#include "svg_import_defs.h" +#include "jc_voronoi.h" + +using namespace gerbolyze; +using namespace std; + +ImageVectorizer *gerbolyze::makeVectorizer(const std::string &name) { + if (name == "poisson-disc") + return new VoronoiVectorizer(POISSON_DISC, /* relax */ true); + else if (name == "hex-grid") + return new VoronoiVectorizer(HEXGRID, /* relax */ false); + else if (name == "square-grid") + return new VoronoiVectorizer(SQUAREGRID, /* relax */ false); + else if (name == "binary-contours") + return new OpenCVContoursVectorizer(); + else if (name == "dev-null") + return new DevNullVectorizer(); + + return nullptr; +} + +/* From jcv voronoi README */ +static void voronoi_relax_points(const jcv_diagram* diagram, jcv_point* points) { + const jcv_site* sites = jcv_diagram_get_sites(diagram); + for (int i=0; inumsites; i++) { + const jcv_site* site = &sites[i]; + jcv_point sum = site->p; + int count = 1; + + const jcv_graphedge* edge = site->edges; + + while (edge) { + sum.x += edge->pos[0].x; + sum.y += edge->pos[0].y; + count++; + edge = edge->next; + } + + points[site->index].x = sum.x / count; + points[site->index].y = sum.y / count; + } +} + +void gerbolyze::parse_img_meta(const pugi::xml_node &node, double &x, double &y, double &width, double &height) { + /* Read XML node attributes */ + x = usvg_double_attr(node, "x", 0.0); + y = usvg_double_attr(node, "y", 0.0); + width = usvg_double_attr(node, "width", 0.0); + height = usvg_double_attr(node, "height", 0.0); + assert (width > 0 && height > 0); + cerr << "image elem: w="< img_vec(img_data.begin(), img_data.end()); + cv::Mat data_mat(img_vec, true); + cv::Mat img = cv::imdecode(data_mat, cv::ImreadModes::IMREAD_GRAYSCALE | cv::ImreadModes::IMREAD_ANYDEPTH); + data_mat.release(); + + if (img.empty()) { + cerr << "Warning: Could not decode content of image element with id \"" << node.attribute("id").value() << "\"" << endl; + } + + return img; +} + +void gerbolyze::draw_bg_rect(xform2d &mat, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink) { + /* For our output to look correct, we have to paint the image's bounding box in black as background for our halftone + * blobs. We cannot simply draw a rect here, though. Instead we have to first intersect the bounding box with the + * clip path we get from the caller. + * + * First, setup the bounding box rectangle in our local px coordinate space. */ + ClipperLib::Path rect_path; + for (auto &elem : vector> {{0, 0}, {width, 0}, {width, height}, {0, height}}) { + d2p xf(mat.doc2phys(d2p{elem.first, elem.second})); + rect_path.push_back({ + (ClipperLib::cInt)round(xf[0] * clipper_scale), + (ClipperLib::cInt)round(xf[1] * clipper_scale) + }); + } + + /* Intersect the bounding box with the caller's clip path */ + ClipperLib::Clipper c; + c.AddPath(rect_path, ClipperLib::ptSubject, /* closed */ true); + if (!clip_path.empty()) { + c.AddPaths(clip_path, ClipperLib::ptClip, /* closed */ true); + } + + ClipperLib::Paths rect_out; + c.StrictlySimple(true); + c.Execute(ClipperLib::ctIntersection, rect_out, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + + /* draw into gerber. */ + for (const auto &poly : rect_out) { + vector> out; + for (const auto &p : poly) + out.push_back(std::array{ + ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale + }); + sink << GRB_POL_CLEAR << out; + } +} + + + +/* Render image into gerber file. + * + * This function renders an image into a number of vector primitives emulating the images grayscale brightness by + * differently sized vector shaped giving an effect similar to halftone printing used in newspapers. + * + * On a high level, this function does this in four steps: + * 1. It preprocesses the source image at the pixel level. This involves several tasks: + * 1.1. It converts the image to grayscale. + * 1.2. It scales the image up or down to match the given minimum feature size. + * 1.3. It applies a blur depending on the given minimum feature size to prevent aliasing artifacts. + * 2. It randomly spread points across the image using poisson disc sampling. This yields points that have a fairly even + * average distance to each other across the image, and that have a guaranteed minimum distance that depends on + * minimum feature size. + * 3. It calculates a voronoi map based on this set of points and it calculats the polygon shape of each cell of the + * voronoi map. + * 4. It scales each of these voronoi cell polygons to match the input images brightness at the spot covered by this + * cell. + */ +void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px) { + double x, y, width, height; + parse_img_meta(node, x, y, width, height); + cv::Mat img = read_img_opencv(node); + if (img.empty()) + return; + + /* Set up target transform using SVG transform and x/y attributes */ + xform2d local_xf(mat); + local_xf.transform(xform2d(node.attribute("transform").value())); + local_xf.translate(x, y); + + double orig_rows = img.rows; + double orig_cols = img.cols; + double scale_x = (double)width / orig_cols; + double scale_y = (double)height / orig_rows; + double off_x = 0; + double off_y = 0; + handle_aspect_ratio(node.attribute("preserveAspectRatio").value(), + scale_x, scale_y, off_x, off_y, orig_cols, orig_rows); + + /* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */ + min_feature_size_px = local_xf.doc2phys_dist(min_feature_size_px); + + draw_bg_rect(local_xf, width, height, clip_path, sink); + + /* Set up a poisson-disc sampled point "grid" covering the image. Calculate poisson disc parameters from given + * minimum feature size. */ + double grayscale_overhead = 0.8; /* fraction of distance between two adjacent cell centers that is reserved for + grayscale interpolation. Larger values -> better grayscale resolution, + larger cells. */ + double center_distance = min_feature_size_px * 2.0 * (1.0 / (1.0-grayscale_overhead)); + vector *grid_centers = get_sampler(m_grid_type)(scale_x * orig_cols, scale_y*orig_rows, center_distance); + //vector *grid_centers = sample_poisson_disc(width, height, min_feature_size_px * 2.0 * 2.0); + //vector *grid_centers = sample_hexgrid(width, height, center_distance); + //vector *grid_centers = sample_squaregrid(width, height, center_distance); + + /* Target factor between given min_feature_size and intermediate image pixels, + * i.e. px ^= min_feature_size */ + double scale_featuresize_factor = 3.0; + /* TODO: support for preserveAspectRatio attribute */ + double px_w = width / min_feature_size_px * scale_featuresize_factor; + double px_h = height / min_feature_size_px * scale_featuresize_factor; + + /* Scale intermediate image (step 1.2) to have pixels per min_feature_size. */ + cv::Mat scaled(cv::Size{(int)round(px_w), (int)round(px_h)}, img.type()); + cv::resize(img, scaled, scaled.size(), 0, 0); + cerr << "scaled " << img.cols << ", " << img.rows << " -> " << scaled.cols << ", " << scaled.rows << endl; + img.release(); + + /* Blur image with a kernel larger than our minimum feature size to avoid aliasing. */ + cv::Mat blurred(scaled.size(), scaled.type()); + int blur_size = (int)ceil(fmax(scaled.cols / width, scaled.rows / height) * center_distance); + if (blur_size%2 == 0) + blur_size += 1; + cerr << "blur size " << blur_size << endl; + cv::GaussianBlur(scaled, blurred, {blur_size, blur_size}, 0, 0); + scaled.release(); + + /* Calculate voronoi diagram for the grid generated above. */ + jcv_diagram diagram; + memset(&diagram, 0, sizeof(jcv_diagram)); + cerr << "adjusted scale " << scale_x << " " << scale_y << endl; + cerr << "voronoi clip rect " << (scale_x * orig_cols) << " " << (scale_y * orig_rows) << endl; + jcv_rect rect {{0.0, 0.0}, {scale_x * orig_cols, scale_y * orig_rows}}; + jcv_point *pts = reinterpret_cast(grid_centers->data()); /* hackety hack */ + jcv_diagram_generate(grid_centers->size(), pts, &rect, 0, &diagram); + /* Relax points, i.e. wiggle them around a little bit to equalize differences between cell sizes a little bit. */ + if (m_relax) + voronoi_relax_points(&diagram, pts); + memset(&diagram, 0, sizeof(jcv_diagram)); + jcv_diagram_generate(grid_centers->size(), pts, &rect, 0, &diagram); + + /* For each voronoi cell calculated above, find the brightness of the blurred image pixel below its center. We do + * not have to average over the entire cell's area here: The blur is doing a good approximation of that while being + * simpler and faster. + * + * We do this step before generating the cell poygons below because we have to look up a cell's neighbor's fill + * factor during gap filling for minimum feature size preservation. */ + vector fill_factors(diagram.numsites); /* Factor to be multiplied with site polygon radius to yield target + fill level */ + const jcv_site* sites = jcv_diagram_get_sites(&diagram); + int j = 0; + for (int i=0; i( + (int)round(center.y / (scale_y * orig_rows / blurred.rows)), + (int)round(center.x / (scale_x * orig_cols / blurred.cols))) / 255.0; + /* FIXME: This is a workaround for a memory corruption bug that happens with the square-grid setting. When using + * square-grid on a fairly small test image, sometimes sites[i].index will be out of bounds here. + */ + if (sites[i].index < (int)fill_factors.size()) + fill_factors[sites[i].index] = sqrt(pxd); + } + + /* Minimum gap between adjacent scaled site polygons. */ + double min_gap_px = min_feature_size_px; + vector adjusted_fill_factors; + adjusted_fill_factors.reserve(32); /* Vector to hold adjusted fill factors for each edge for gap filling */ + /* now iterate over all voronoi cells again to generate each cell's scaled polygon halftone blob. */ + for (int i=0; ineighbor != nullptr) { /* nullptr -> edge is on the voronoi map's border */ + double rad = sqrt(pow(center.x - e->neighbor->p.x, 2) + pow(center.y - e->neighbor->p.y, 2)) / 2.0; + double fill_factor_theirs = fill_factors[e->neighbor->index]; + double gap_px = (1.0 - fill_factor_ours) * rad + (1.0 - fill_factor_theirs) * rad; + + if (gap_px > min_gap_px) { + /* all good. gap is wider than minimum. */ + } else if (gap_px > 0.5 * min_gap_px) { + /* gap is narrower than minimum, but more than half of minimum width. */ + /* force gap open, distribute adjustment evenly on left/right */ + double fill_factor_adjustment = (min_gap_px - gap_px) / 2.0 / rad; + adjusted_fill_factor -= fill_factor_adjustment; + } else { + /* gap is less than half of minimum width. Force gap closed. */ + adjusted_fill_factor = 1.0; + } + } + adjusted_fill_factors.push_back(adjusted_fill_factor); + e = e->next; + } + + /* Now, generate the actual halftone blob polygon */ + ClipperLib::Path cell_path; + double last_fill_factor = adjusted_fill_factors.back(); + e = sites[i].edges; + j = 0; + while (e) { + double fill_factor = adjusted_fill_factors[j]; + if (last_fill_factor != fill_factor) { + /* Fill factor was adjusted since last edge, so generate one extra point so we have a nice radial + * "step". */ + d2p p = local_xf.doc2phys(d2p{ + off_x + center.x + (e->pos[0].x - center.x) * fill_factor, + off_y + center.y + (e->pos[0].y - center.y) * fill_factor + }); + cell_path.push_back({ + (ClipperLib::cInt)round(p[0] * clipper_scale), + (ClipperLib::cInt)round(p[1] * clipper_scale) + }); + } + + /* Emit endpoint of current edge */ + d2p p = local_xf.doc2phys(d2p{ + off_x + center.x + (e->pos[1].x - center.x) * fill_factor, + off_y + center.y + (e->pos[1].y - center.y) * fill_factor + }); + cell_path.push_back({ + (ClipperLib::cInt)round(p[0] * clipper_scale), + (ClipperLib::cInt)round(p[1] * clipper_scale) + }); + + j += 1; + last_fill_factor = fill_factor; + e = e->next; + } + + /* Now, clip the halftone blob generated above against the given clip path. We do this individually for each + * blob since this way is *much* faster than throwing a million blobs at once at poor clipper. */ + ClipperLib::Paths polys; + ClipperLib::Clipper c; + c.AddPath(cell_path, ClipperLib::ptSubject, /* closed */ true); + if (!clip_path.empty()) { + c.AddPaths(clip_path, ClipperLib::ptClip, /* closed */ true); + } + c.StrictlySimple(true); + c.Execute(ClipperLib::ctIntersection, polys, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + + /* Export halftone blob to gerber. */ + for (const auto &poly : polys) { + vector> out; + for (const auto &p : poly) + out.push_back(std::array{ + ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale + }); + sink << GRB_POL_DARK << out; + } + } + + blurred.release(); + jcv_diagram_free( &diagram ); + delete grid_centers; +} + +void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_y, double &off_x, double &off_y, double cols, double rows) { + + if (spec.empty()) { + spec = "xMidYMid meet"; + } + + auto idx = spec.find(" "); + string par_align = spec; + string par_meet = "meet"; + if (idx != string::npos) { + par_align = spec.substr(0, idx); + par_meet = spec.substr(idx+1); + } + + if (par_align != "none") { + double scale = scale_x; + if (par_meet == "slice") { + scale = std::max(scale_x, scale_y); + } else { + scale = std::min(scale_x, scale_y); + } + + std::regex reg("x(Min|Mid|Max)Y(Min|Mid|Max)"); + std::smatch match; + + cerr << "data: " <<" "<< scale_x << "/" << scale_y << ": " << scale << endl; + off_x = (scale_x - scale) * cols; + off_y = (scale_y - scale) * rows; + cerr << rows <<","<> contours; + vector hierarchy; + cv::findContours(img, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_TC89_KCOS); + + queue> child_stack; + child_stack.push({ 0, true }); + + while (!child_stack.empty()) { + bool dark = child_stack.front().second; + for (int i=child_stack.front().first; i>=0; i = hierarchy[i][0]) { + if (hierarchy[i][2] >= 0) { + child_stack.push({ hierarchy[i][2], !dark }); + } + + sink << (dark ? GRB_POL_DARK : GRB_POL_CLEAR); + + bool is_clockwise = cv::contourArea(contours[i], true) > 0; + if (!is_clockwise) + std::reverse(contours[i].begin(), contours[i].end()); + + ClipperLib::Path out; + for (const auto &p : contours[i]) { + d2p q = local_xf.doc2phys(d2p{ + off_x + (double)p.x * scale_x, + off_y + (double)p.y * scale_y + }); + out.push_back({ + (ClipperLib::cInt)round(q[0] * clipper_scale), + (ClipperLib::cInt)round(q[1] * clipper_scale) + }); + } + + ClipperLib::Clipper c; + c.AddPath(out, ClipperLib::ptSubject, /* closed */ true); + if (!clip_path.empty()) { + c.AddPaths(clip_path, ClipperLib::ptClip, /* closed */ true); + } + c.StrictlySimple(true); + ClipperLib::Paths polys; + c.Execute(ClipperLib::ctIntersection, polys, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + + /* Draw into gerber. */ + for (const auto &poly : polys) { + vector> out; + for (const auto &p : poly) + out.push_back(std::array{ + ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale + }); + sink << out; + } + } + + child_stack.pop(); + } +} + +gerbolyze::VectorizerSelectorizer::VectorizerSelectorizer(const string default_vectorizer, const string defs) + : m_default(default_vectorizer) { + istringstream foo(defs); + string elem; + while (std::getline(foo, elem, ',')) { + size_t pos = elem.find_first_of("="); + if (pos == string::npos) { + cerr << "Error parsing vectorizer selection string at element \"" << elem << "\"" << endl; + continue; + } + + const string parsed_id = elem.substr(0, pos); + const string mapping = elem.substr(pos+1); + m_map[parsed_id] = mapping; + } + + cerr << "parsed " << m_map.size() << " vectorizers" << endl; + for (auto &elem : m_map) { + cerr << " " << elem.first << " -> " << elem.second << endl; + } +} + +ImageVectorizer *gerbolyze::VectorizerSelectorizer::select(const pugi::xml_node &img) { + const string id = img.attribute("id").value(); + cerr << "selecting vectorizer for image \"" << id << "\"" << endl; + if (m_map.count(id) > 0) { + cerr << " -> found" << endl; + return makeVectorizer(m_map[id]); + } + + cerr << " -> default" << endl; + return makeVectorizer(m_default); +} + diff --git a/svg-flatten/src/vec_core.h b/svg-flatten/src/vec_core.h new file mode 100644 index 0000000..0d4da12 --- /dev/null +++ b/svg-flatten/src/vec_core.h @@ -0,0 +1,57 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include "vec_grid.h" + +namespace gerbolyze { + + class VoronoiVectorizer : public ImageVectorizer { + public: + VoronoiVectorizer(grid_type grid, bool relax=true) : m_relax(relax), m_grid_type(grid) {} + + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px); + private: + double m_relax; + grid_type m_grid_type; + }; + + class OpenCVContoursVectorizer : public ImageVectorizer { + public: + OpenCVContoursVectorizer() {} + + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px); + }; + + class DevNullVectorizer : public ImageVectorizer { + public: + DevNullVectorizer() {} + + virtual void vectorize_image(xform2d &, const pugi::xml_node &, ClipperLib::Paths &, PolygonSink &, double) {} + }; + + void parse_img_meta(const pugi::xml_node &node, double &x, double &y, double &width, double &height); + std::string read_img_data(const pugi::xml_node &node); + void draw_bg_rect(xform2d &mat, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink); + void handle_aspect_ratio(std::string spec, double &scale_x, double &scale_y, double &off_x, double &off_y, double cols, double rows); +} + diff --git a/svg-flatten/src/vec_grid.cpp b/svg-flatten/src/vec_grid.cpp new file mode 100644 index 0000000..7c27543 --- /dev/null +++ b/svg-flatten/src/vec_grid.cpp @@ -0,0 +1,99 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "poisson_disk_sampling.h" + +#include "vec_grid.h" + +using namespace std; +using namespace gerbolyze; + +sampling_fun gerbolyze::get_sampler(enum grid_type type) { + switch(type) { + case POISSON_DISC: + return sample_poisson_disc; + case HEXGRID: + return sample_hexgrid; + case SQUAREGRID: + return sample_squaregrid; + default: + return sample_poisson_disc; + } +} + +vector *gerbolyze::sample_poisson_disc(double w, double h, double center_distance) { + d2p top_left {0, 0}; + d2p bottom_right {w, h}; + return new auto(thinks::PoissonDiskSampling(center_distance/2.5, top_left, bottom_right)); +} + +vector *gerbolyze::sample_hexgrid(double w, double h, double center_distance) { + double radius = center_distance / 2.0 / (sqrt(3) / 2.0); /* radius of hexagon */ + double pitch_v = 1.5 * radius; + double pitch_h = center_distance; + + /* offset of first hexagon to make sure the entire area is covered. We use slightly larger values here to avoid + * corner cases during clipping in the voronoi map generator. The inaccuracies this causes at the edges are + * negligible. */ + double off_x = 0.5001 * center_distance; + double off_y = 0.5001 * radius; + + /* NOTE: The voronoi generator is not quite stable when points lie outside the bounds. Thus, floor(). */ + long long int points_x = floor(w / pitch_h); + long long int points_y = floor(h / pitch_v); + + vector *out = new vector(); + out->reserve((points_x+1) * points_y); + + /* This may generate up to one extra row of points. We don't care since these points will simply be clipped during + * voronoi map generation. */ + for (long long int y_i=0; y_ipush_back(d2p{off_x + x_i * pitch_h, off_y + y_i * pitch_v}); + } + + for (long long int x_i=0; x_ipush_back(d2p{off_x + (x_i - 0.5) * pitch_h, off_y + (y_i + 1) * pitch_v}); + } + } + + return out; +} + +vector *gerbolyze::sample_squaregrid(double w, double h, double center_distance) { + /* offset of first square to make sure the entire area is covered. We use slightly larger values here to avoid + * corner cases during clipping in the voronoi map generator. The inaccuracies this causes at the edges are + * negligible. */ + double off_x = 0.5 * center_distance; + double off_y = 0.5 * center_distance; + + long long int points_x = ceil(w / center_distance); + long long int points_y = ceil(h / center_distance); + + vector *out = new vector(); + out->reserve(points_x * points_y); + + for (long long int y_i=0; y_ipush_back({off_x + x_i*center_distance, off_y + y_i*center_distance}); + } + } + + return out; +} + diff --git a/svg-flatten/src/vec_grid.h b/svg-flatten/src/vec_grid.h new file mode 100644 index 0000000..f2ed55a --- /dev/null +++ b/svg-flatten/src/vec_grid.h @@ -0,0 +1,41 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace gerbolyze { + +enum grid_type { + POISSON_DISC, + HEXGRID, + SQUAREGRID +}; + +sampling_fun get_sampler(enum grid_type type); + +std::vector *sample_poisson_disc(double w, double h, double center_distance); +std::vector *sample_hexgrid(double w, double h, double center_distance); +std::vector *sample_squaregrid(double w, double h, double center_distance); + +} /* namespace gerbolyze */ + diff --git a/upstream/CavalierContours b/upstream/CavalierContours new file mode 160000 index 0000000..b955785 --- /dev/null +++ b/upstream/CavalierContours @@ -0,0 +1 @@ +Subproject commit b955785cc3db9689704d6135cbb60177fab835bb diff --git a/upstream/argagg b/upstream/argagg new file mode 160000 index 0000000..79e4adf --- /dev/null +++ b/upstream/argagg @@ -0,0 +1 @@ +Subproject commit 79e4adfa2c6e2bfbe63da05cc668eb9ad5596748 diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/Changes.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Changes.htm new file mode 100644 index 0000000..c05e9b3 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Changes.htm @@ -0,0 +1,1269 @@ + + + + + + + + + + + + + + + + + + + + Changes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Changes

+ + + + + + +

See Also

+

Deprecated, Rounding, Clipper.Constructor, Clipper.Execute, Clipper.ZFillFunction, ClipperBase.AddPath, ClipperBase.AddPaths, ClipperOffset, ClipperOffset.Execute, PolyNode, PolyTree, Area, CleanPolygon, CleanPolygons, ClosedPathsFromPolyTree, MinkowskiDiff, MinkowskiSum, OffsetPaths, OpenPathsFromPolyTree, Orientation, PointInPolygon, PolyTreeToPaths, SimplifyPolygon, SimplifyPolygons, Defines, CInt, InitOptions, IntPoint, Path, Paths, PolyFillType, ZFillCallback

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/Deprecated.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Deprecated.htm new file mode 100644 index 0000000..7a8f254 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Deprecated.htm @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + Deprecated + + + + + + + + + + + + + + + + + + + + + +

Deprecated

+ + +

The precompiler directive 'use_deprecated' allows users to update the Clipper library without being forced to make immediate changes to code that accesses the library. Depricated code will be removed in a future update. (Enabled by default.)

Deprecated types and functions:

All deprecated code has been removed from version 6.2.0. +

+ + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/Example.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Example.htm new file mode 100644 index 0000000..ded5dd5 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Example.htm @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + Example + + + + + + + + + + + + + + + + + + + + + +

Example

+ + + + + + + + + +
Delphi Code Sample: +
+ +
+  uses
+    graphics32, clipper;
+  
+  function GetEllipsePoints(bounds: TIntRect): TPath;
+  begin
+    //code to create an elliptical polygon here
+  end;
+	
+  procedure DrawPolygons(polys: TPaths; color: TColor32);
+  begin
+    //code to display the polygons here
+  end;
+	
+  var
+    sub, clp, sol: TPaths;
+  begin
+
+    //set up the subject and clip polygons ...
+    setlength(sub, 3);
+    sub[0] := GetEllipsePoints(IntRect(100,100,300,300));
+    sub[1] := GetEllipsePoints(IntRect(125,130,275,180));
+    sub[2] := GetEllipsePoints(IntRect(125,220,275,270));
+	
+    setlength(clp, 1);
+    clp[0] := GetEllipsePoints(IntRect(140,70,220,320));
+
+    //display the subject and clip polygons ...
+    DrawPolygons(sub, 0x8033FFFF);
+    DrawPolygons(clp, 0x80FFFF33);
+    
+    //get the intersection of the subject and clip polygons ...
+    with TClipper.Create do
+    try
+      AddPaths(sub, ptSubject, true);
+      AddPaths(clp, ptClip, true);
+      Execute(ctIntersection, sol, pftEvenOdd, pftEvenOdd);
+    finally
+      free;
+    end;
+    
+    //finally draw the intersection polygons ...
+    DrawPolygons(sol, 0x40808080);
+        
+ +
+
 
+ + + + + + + + + + +
C++ Code Sample: +
+ +
+  #include "clipper.hpp"
+  
+  ...
+
+  //from clipper.hpp ...
+  //typedef long long cInt;
+  //struct IntPoint {cInt X; cInt Y;};
+  //typedef std::vector<IntPoint> Path;
+  //typedef std::vector<Polygon> Paths;
+
+  using namespace ClipperLib;
+
+  void GetEllipsePoints(IntRect& bounds, Path& p)
+  {/* ... */}
+  
+  void DrawPolygons(Paths& p, unsigned color)
+  {/* ... */}
+  
+  int main()
+  {
+    //set up the subject and clip polygons ...
+    Paths sub(3);
+    GetEllipsePoints(IntRect(100,100,300,300), sub[0]);
+    GetEllipsePoints(IntRect(125,130,275,180), sub[1]);
+    GetEllipsePoints(IntRect(125,220,275,270), sub[2]);
+    
+    Paths clp(1);
+    GetEllipsePoints(IntRect(140,70,220,320), clp[0]);
+    
+    //display the subject and clip polygons ...
+    DrawPolygons(sub, 0x8033FFFF);
+    DrawPolygons(clp, 0x80FFFF33);
+    
+    //get the intersection of the subject and clip polygons ...
+    Clipper clpr;
+    clpr.AddPaths(sub, ptSubject, true);
+    clpr.AddPaths(clp, ptClip, true);
+    Paths solution;
+    clpr.Execute(ctIntersection, solution, pftEvenOdd, pftEvenOdd);
+    
+    //finally draw the intersection polygons ...
+    DrawPolygons(solution, 0x40808080);
+  }
+        
+ + +
+
 
+ + + + + + + + + +
C# Code Sample: +
+ +
+  ...
+  using ClipperLib;
+	
+  ...
+  using Path = List<IntPoint>;
+  using Paths = List<List<IntPoint>>;
+  
+  static Path GetEllipsePoints(IntRect bounds)
+  {/* ... */}
+  
+  static void DrawPolygons(Path p, uint color)
+  {/* ... */}
+  
+  static void Main(string[] args)
+  {
+    Paths subjs = new Paths(3);
+    subjs.Add(GetEllipsePoints(new IntRect(100,100,300,300)));
+    subjs.Add(GetEllipsePoints(new IntRect(125,130,275,180)));
+    subjs.Add(GetEllipsePoints(new IntRect(125,220,275,270)));
+    
+    Paths clips = new Paths(1);
+    clips.Add(GetEllipsePoints(new IntRect(140,70,220,320)));
+    
+    DrawPolygons(subjs, 0x8033FFFF);
+    DrawPolygons(clips, 0x80FFFF33);
+    
+    Paths solution = new Paths();
+    Clipper c = new Clipper();
+    c.AddPaths(subjs, PolyType.ptSubject, true);
+    c.AddPaths(clips, PolyType.ptClip, true);
+    c.Execute(ClipType.ctIntersection, solution);
+    
+    DrawPolygons(solution, 0x40808080);
+  }
+        
+ +
+ +
 
+ + + + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/FAQ.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/FAQ.htm new file mode 100644 index 0000000..91459b9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/FAQ.htm @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + FAQ + + + + + + + + + + + + + + + + + + + + + +

FAQ

+ + +

Why does Clipper use integer coordinates, not floats?

+ +

This has been done to preserve numerical robustness. Early versions of the library did use floating point coordinates, but it became apparent that floating point imprecision was always going to cause occasional errors.

+ + +

How do I use floating point coordinates with Clipper?

+ +

It's a simple task to multiply your floating point coordinates by a scaling factor (that's typically a power of 10 depending on the desired precision). Then with the solution paths, divide the returned coordinates by this same scaling factor. Clipper accepts integer coordinates as large as ±4.6e18, so it can accommodate very large scaling.

+ + +

Does Clipper handle polygons with holes?

+ +

'Holes' are defined by the specified polygon filling rule. (See also Clipper.Execute)

+ + +

Some polygons in the solution share a common edge. Is this a bug?

+ +

No, though this should happen rarely as of version 6. (See Clipper.Execute for more about this.)

+ + +

I have lots of polygons that I want to 'union'. Can I do this in one operation?

+ +

Yes. Just add all the polygons as subject polygons to the Clipper object. (You don't have to assign both subject and clip polygons.)

+ + +

The polygons produced by ClipperOffset have tiny artefacts? Could this be a bug?

+ +

Make sure the input polygons don't self-intersect. Tiny self-intersections can sometimes be produced by previous clipping operations. These can be cleaned up using the CleanPolygon and CleanPolygons functions. Also, make sure the supplied polygons don't overlap. If they do, offset these separately. Finally, the precision of the input coordinates may be a problem. Because the Clipper Library only operates on integer coordinates, you may need to scale your coordinates (eg by a factor of 10) to improve precision.

+ + +

Is there an easy way to reverse polygon orientations?

+ +

Yes, see ReversePaths.

+ +
 
+ + +

My drawings contain lots of beziers, ellipses and arcs. How can I perform clipping operations on these?

+ +

You'll have to convert them to 'flattened' paths. For an example of how this can be done (and even reconstructed back into beziers, arcs etc), see the CurvesDemo application included in this library.

+ + +

See Also

+

Clipper.Execute, ClipperOffset, CleanPolygon, CleanPolygons, ReversePaths, PolyFillType

+ + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/License.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/License.htm new file mode 100644 index 0000000..928dda8 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/License.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + License + + + + + + + + + + + + + + + + + + + + + +

License

+ + +

The Clipper Library (including Delphi, C++ & C# source code, other accompanying code, examples and documentation), hereafter called "the Software", has been released under the following license, terms and conditions:

+ + +

Boost Software License - Version 1.0 - August 17th, 2003
http://www.boost.org/LICENSE_1_0.txt

+ + +

Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the Software covered by this license to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following:

+ + +

The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor.

+ + +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ + + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/Rounding.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Rounding.htm new file mode 100644 index 0000000..cb4d62b --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/Rounding.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + Rounding + + + + + + + + + + + + + + + + + + + + + +

Rounding

+ + +

By using an integer type for polygon coordinates, the Clipper Library has been able to avoid problems of numerical robustness that can cause havoc with geometric computations. Problems associated with integer rounding and their possible solutions are discussed below.

It's important to stress at the outset that rounding causes vertices to move fractions of a unit away from their 'true' positions. Nevertheless, the resulting imprecision can be very effectively managed by appropriate scaling.

The Clipper Library supports scaling to very high degrees of precision by accepting integer coordinate values in the range ±0x3FFFFFFFFFFFFFFF ( 4.6e+18).

Another complication of using a discrete numbers (as opposed to real numbers) is that very occasionally tiny self-intersection artefacts arise. In the unscaled image on the left (where one unit equals one pixel), the area of intersection of two polygons has been highlighted in bright green.

+ + +



A 30X 'close up' of the lower points of intersection of these same two polygons shows the presence of a tiny self-intersecting artefact. The three 'black dots' highlight the actual points of intersection (with their fractional coordinates displayed). The 'red dots' show where these points of intersection are located once rounding is applied. With a little care you can see that rounding reverses the orientation of these vertices and causes a tiny self-intersecting artefact.

Although these tiny self-intersections are uncommon, if it's deemed necessary, they are best removed with CleanPolygons. (Setting Clipper's StrictlySimple property to true would also address this self-intersection but the tiny (sub-unit) polygon 'artefact' with incorrect orientation would still appear in the solution.)

+ + +



In this final example, the single polygon on the left also has a tiny self-intersection. However, the clipping algorithm sees this vertex (88,50) as simply 'touching' rather than intersecting the right edge of the polygon (though only by a fraction of a unit). Since this intersection won't normally be detected, the clipping solution (eg following a union operation) will still contain this tiny self-intersection. Setting Clipper's StrictlySimple property to true avoids this uncommon problem.

+
+ + +

See Also

+

Clipper.StrictlySimple, CleanPolygons

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Overview/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Overview/_Body.htm new file mode 100644 index 0000000..5135549 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Overview/_Body.htm @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + Overview + + + + + + + + + + + + + + + + + + + + + + +

Overview

+ + +

The Clipper Library performs clipping, and offsetting of both lines and polygons.

A number of features set Clipper apart from other clipping libraries: +

    + +
  • it accepts all types of polygons including self-intersecting ones
  • + +
  • it supports multiple polygon filling rules (EvenOdd, NonZero, Positive, Negative)
  • + +
  • it's very fast relative to other libraries
  • + +
  • it's numerically robust
  • + +
  • it also performs line and polygon offsetting
  • + +
  • it's free to use in both freeware and commercial applications
  • + +

+ + +

Current Version: 6.2.1

Author & copyright:
Angus Johnson. Copyright © 2010-2014
License, terms and conditions: Boost Software License

+ + +

Terminology:
+

    + +
  • Clipping: commonly refers to the process of cutting away from a set of 2-dimensional geometric shapes those parts that are outside a rectangular 'clipping' window. This can be achieved by intersecting subject paths (lines and polygons) with a clipping rectangle. In a more general sense, the clipping window need not be rectangular but can be any type of polygon, even multiple polygons. Also, while clipping typically refers to an intersection operation, in this documentation it will refer to any one of the four boolean operations (intersection, union, difference and exclusive-or).
  • + +
  • Path: is an ordered sequence of vertices defining a single geometric contour that's either a line (an open path) or a polygon (a closed path).
  • + +
  • Line: or polyline is an open path containing 2 or more vertices.
  • + +
  • Polygon: commonly refers to a two-dimensional region bounded by an outer non-intersecting closed contour. That region may also contain a number of 'holes'. In this documentation however, polygon will simply refer to a closed path.
  • + +
  • Contour: synonymous with path.
  • + +
  • Hole: is a closed region within a polygon that's not part of the polygon. A 'hole polygon' is a closed path that forms the outer boundaries of a hole.
  • + +
  • Polygon Filling Rule: the filling rule, together with a list of closed paths, defines those regions (bounded by paths) that are inside (ie regions 'brush filled' in a graphical display) and those which are outside (ie 'holes').
  • + +

+ + +

Distribution package contents:

The ZIP package contains the Clipper library's source code, a Windows CHM help file, HTML help, and a number of compiled demo applications (with full source code). The library was initially written in Delphi Pascal (and compiles with Delphi version 7 or above) but now contains C++, C# and Python translations too. The library's source code in each language is about 5000 lines. The Delphi code contains reasonably extensive comments, but comments are fewer in the C++ and C# code. The included sample applications show how Clipper can be used with the different languages using a number of graphics display libraries including - AGG, Cairo, OpenGL, Graphics32, GDI+ and SVG.

Download Link:

SourceForge

References:

The Library is based on but significantly extends Bala Vatti's polygon clipping algorithm as described in "A generic solution to polygon clipping", Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.

A section in "Computer graphics and geometric modeling: implementation and algorithms" by By Max K. Agoston (Springer, 2005) discussing Vatti Polygon Clipping was also helpful in creating the initial Clipper implementation.

The paper titled "Polygon Offsetting by Computing Winding Numbers" by Chen & McMains (Paper no. DETC2005-85513, ASME 2005. Pages 565-575) contains helpful discussion on the complexities of polygon offsetting together with some solutions.

+ + +

See Also

+

Source, License, Clipper, ClipperOffset, ClipType, PolyFillType

+ + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Constructor.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Constructor.htm new file mode 100644 index 0000000..f51279f --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Constructor.htm @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + Constructor + + + + + + + + + + + + + + + + + + + + + + +

Clipper.Constructor

+ + +

Del.» constructor TClipper.Create(InitOptions: TInitOptions = []);

+ +

C++ » Clipper::Clipper(int initOptions = 0) : ClipperBase();

+ +

C#  » public Clipper(initOptions = 0): base() {};

+ +

The Clipper constructor creates an instance of the Clipper class. One or more InitOptions may be passed as a parameter to set the corresponding properties. (These properties can still be set or reset after construction.)

Examples:

+ + + + + + +
+ +
+  //C++ constructor setting StrictlySimple and PreserveCollinear properties ...
+  Clipper clipper(ioStrictlySimple | ioPreserveCollinear);
+
+  //C# constructor setting StrictlySimple and PreserveCollinear properties ...
+  Clipper clipper = new Clipper(Clipper.ioStrictlySimple | Clipper.ioPreserveCollinear);
+
+  //Delphi constructor setting StrictlySimple and PreserveCollinear properties ...
+  clipper := TClipper.Create([ioStrictlySimple, ioPreserveCollinear]);
+          
+ +

 

+ + + + + +

See Also

+

PreserveCollinear, ReverseSolution, StrictlySimple, InitOptions

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Execute.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Execute.htm new file mode 100644 index 0000000..b551a19 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Methods/Execute.htm @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + Execute + + + + + + + + + + + + + + + + + + + + + + +

Clipper.Execute

+ + +

Del.»
function Execute(clipType: TClipType;
  out solution: TPaths;
  subjFillType: TPolyFillType = pftEvenOdd;
  clipFillType: TPolyFillType = pftEvenOdd): boolean; overload;

function Execute(clipType: TClipType;
  out solution: TPolyTree;
  subjFillType: TPolyFillType = pftEvenOdd;
  clipFillType: TPolyFillType = pftEvenOdd): boolean; overload;

+ + +

C++ »
bool Execute(ClipType clipType,
  Paths &solution,
  PolyFillType subjFillType = pftEvenOdd,
  PolyFillType clipFillType = pftEvenOdd);

bool Execute(ClipType clipType,
  PolyTree &solution,
  PolyFillType subjFillType = pftEvenOdd,
  PolyFillType clipFillType = pftEvenOdd);

+ +

C#  »
public bool Execute(ClipType clipType,
  Paths solution,
  PolyFillType subjFillType,
  PolyFillType clipFillType);

public bool Execute(ClipType clipType,
  PolyTree solution,
  PolyFillType subjFillType,
  PolyFillType clipFillType);

+ +
+ +

Once subject and clip paths have been assigned (via AddPath and/or AddPaths), Execute can then perform the clipping operation (intersection, union, difference or XOR) specified by the clipType parameter.

The solution parameter can be either a Paths or PolyTree structure. The Paths structure is simpler than the PolyTree stucture. Because of this it is quicker to populate and hence clipping performance is a little better (it's roughly 10% faster). However, the PolyTree data structure provides more information about the returned paths which may be important to users. Firstly, the PolyTree structure preserves nested parent-child polygon relationships (ie outer polygons owning/containing holes and holes owning/containing other outer polygons etc). Also, only the PolyTree structure can differentiate between open and closed paths since each PolyNode has an IsOpen property. (The Path structure has no member indicating whether it's open or closed.) For this reason, when open paths are passed to a Clipper object, the user must use a PolyTree object as the solution parameter, otherwise an exception will be raised.

When a PolyTree object is used in a clipping operation on open paths, two ancilliary functions have been provided to quickly separate out open and closed paths from the solution - OpenPathsFromPolyTree and ClosedPathsFromPolyTree. PolyTreeToPaths is also available to convert path data to a Paths structure (irrespective of whether they're open or closed).

There are several things to note about the solution paths returned: +

    + +
  • they aren't in any specific order
  • + +
  • they should never overlap or be self-intersecting (but see notes on rounding)
  • + +
  • holes will be oriented opposite outer polygons
  • + +
  • the solution fill type can be considered either EvenOdd or NonZero since it will comply with either filling rule
  • + +
  • polygons may rarely share a common edge (though this is now very rare as of version 6)
  • + +


+ + +

The subjFillType and clipFillType parameters define the polygon fill rule to be applied to the polygons (ie closed paths) in the subject and clip paths respectively. (It's usual though obviously not essential that both sets of polygons use the same fill rule.)

Execute can be called multiple times without reassigning subject and clip polygons (ie when different clipping operations are required on the same polygon sets).

+ + +

See Also

+

Example, Rounding, ClipperBase.AddPath, ClipperBase.AddPaths, PolyNode.IsOpen, PolyTree, ClosedPathsFromPolyTree, OpenPathsFromPolyTree, PolyTreeToPaths, ClipType, Path, Paths, PolyFillType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/PreserveCollinear.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/PreserveCollinear.htm new file mode 100644 index 0000000..c198c67 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/PreserveCollinear.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + PreserveCollinear + + + + + + + + + + + + + + + + + + + + + + +

Clipper.PreserveCollinear

+ + +

Del.» property PreserveCollinear: boolean; override;

+ +

C++ » void PreserveCollinear(bool value);

+ +

C#  » public bool PreserveCollinear { get {} set {} };

+ + +


By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before clipping. When enabled the PreserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution.

+ + + +

See Also

+

Constructor

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ReverseSolution.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ReverseSolution.htm new file mode 100644 index 0000000..4294395 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ReverseSolution.htm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + ReverseSolution + + + + + + + + + + + + + + + + + + + + + + +

Clipper.ReverseSolution

+ + +

Del.» property ReverseSolution: boolean; override;

+ +

C++ » void ReverseSolution(bool value);

+ +

C#  » public bool ReverseSolution { get {} set {} };

+ + +

When this property is set to true, polygons returned in the solution parameter of the Execute() method will have orientations opposite to their normal orientations.

+ + + + + +

See Also

+

Execute, Orientation

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm new file mode 100644 index 0000000..b6918cc --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + StrictlySimple + + + + + + + + + + + + + + + + + + + + + + +

Clipper.StrictlySimple

+ + +

Del.» property StrictlySimple: boolean; override;

+ +

C++ » void StrictlySimple(bool value);

+ +

C#  » public bool StrictlySimple { get {} set {} };

+ + +


Terminology:
+

    + +
  • A simple polygon is one that does not self-intersect.
  • + +
  • A weakly simple polygon is a simple polygon that contains 'touching' vertices, or 'touching' edges.
  • + +
  • A strictly simple polygon is a simple polygon that does not contain 'touching' vertices, or 'touching' edges.
  • + +

+ +

Vertices 'touch' if they share the same coordinates (and are not adjacent). An edge touches another if one of its end vertices touches another edge excluding its adjacent edges, or if they are co-linear and overlapping (including adjacent edges).

Polygons returned by clipping operations (see Clipper.Execute()) should always be simple polygons. When the StrictlySimply property is enabled, polygons returned will be strictly simple, otherwise they may be weakly simple. It's computationally expensive ensuring polygons are strictly simple and so this property is disabled by default.

Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress.


+ + +

In the image above, the two examples show weakly simple polygons being broken into two strictly simple polygons. (The outlines with arrows are intended to aid visualizing vertex order.)

See also the article on Simple Polygons on Wikipedia.

+ + + +

See Also

+

Execute, SimplifyPolygons

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ZFillFunction.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ZFillFunction.htm new file mode 100644 index 0000000..290061e --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/ZFillFunction.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + ZFillFunction + + + + + + + + + + + + + + + + + + + + + + +

Clipper.ZFillFunction

+ + +

Del.» property ZFillFunction: TZFillCallback read FZFillCallback write FZFillCallback;

+ +

C++ » void ZFillFunction(ZFillCallback zFillFunc);

+ +

C#  » public ZFillCallback ZFillFunction { get; set; };

+
+ +

This property is only exposed when the pre-processor directive use_xyz has been defined. If this is the case, a 'Z' member will be included in the IntPoint structure where users can store custom data. While most vertices in a clipping solution will correspond to input (subject and clip) vertices, there will also be new vertices wherever edges intersect. This property assigns a custom callback function to the Clipper object so that custom 'Z' values can be assigned to these intersection vertices. (Note that 'Z' values in the solution at non-intersecting vertices will simply be copied from matching input vertices along with the X and Y values.)

It is up to the library user to assign 'Z' values for new intersection vertices (otherwise these values will remain 0). The four vertices that define the intersecting line segments will be passed to the callback function (together with the new intersection vertex) to aid the user in determining appropriate Z values.

+

+ + +

See Also

+

Defines, IntPoint, ZFillCallback

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/_Body.htm new file mode 100644 index 0000000..330ecf1 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/Clipper/_Body.htm @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + Clipper + + + + + + + + + + + + + + + + + + + + + + + +

Clipper

+

Hierarchy

+

+

   |

+

ClipperBase

+
+ +

The Clipper class encapsulates boolean operations on polygons (intersection, union, difference and XOR), which is also called polygon clipping.

Input polygons, both subject and clip sets, are passed to a Clipper object by its AddPath and AddPaths methods, and the clipping operation is performed by calling its Execute method. Multiple boolean operations can be performed on the same input polygon sets by repeat calls to Execute.

+ +

Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fields + Methods + Properties +
In Clipper: +
+ Constructor + PreserveCollinear +
+ Execute + ReverseSolution +
+ + StrictlySimple +
+ + ZFillFunction +
In ClipperBase: +
+ AddPath + +
+ AddPaths + +
+ Clear + +
+ GetBounds + +
+

See Also

+

Overview, ClipType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPath.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPath.htm new file mode 100644 index 0000000..de8c944 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPath.htm @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + AddPath + + + + + + + + + + + + + + + + + + + + + + +

ClipperBase.AddPath

+ + +

Del.» function AddPath(const path: TPath; polyType: TPolyType; Closed: Boolean): Boolean;

+ +

C++ » bool AddPath(const Path &pg, PolyType polyType, bool closed);

+ +

C#  » public virtual bool AddPath(Path pg, PolyType polyType, bool closed);

+
+ + +

Any number of subject and clip paths can be added to a clipping task, either individually via the AddPath() method, or as groups via the AddPaths() method, or even using both methods.

'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both, but 'clipping' paths must always be closed. Clipper allows polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons.

With closed paths, orientation should conform with the filling rule that will be passed via Clippper's Execute method.

+ + + +

Path Coordinate range:
Path coordinates must be between ± 0x3FFFFFFFFFFFFFFF (± 4.6e+18), otherwise a range error will be thrown when attempting to add the path to the Clipper object. If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by avoiding large integer math. If the preprocessor directive use_int32 is defined (allowing a further increase in performance of 20-30%), then the maximum range is restricted to ± 32,767.

+ + +

Return Value:
The function will return false if the path is invalid for clipping. A path is invalid for clipping when: +

    + +
  • it has less than 2 vertices
  • + +
  • it has 2 vertices but is not an open path
  • + +
  • the vertices are all co-linear and it is not an open path
  • + +

+ +
+ + + + +

See Also

+

Example, Clipper.Execute, AddPaths, Orientation, Defines, Path, PolyFillType, PolyType

+ + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPaths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPaths.htm new file mode 100644 index 0000000..73e1186 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/AddPaths.htm @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + AddPaths + + + + + + + + + + + + + + + + + + + + + + +

ClipperBase.AddPaths

+ + +

Del.» function AddPaths(const paths: TPaths; polyType: TPolyType; Closed: Boolean): boolean;

+ +

C++ » bool AddPaths(const Paths &ppg, PolyType polyType, bool closed);

+ +

C#  » public virtual bool AddPaths(Paths ppg, PolyType polyType, bool closed);

+
+ +

Any number of subject and clip paths can be added to a clipping task, either individually via the AddPath() method, or as groups via the AddPaths() method, or even using both methods.

'Subject' paths may be either open (lines) or closed (polygons) or even a mixture of both, but 'clipping' paths must always be closed. Clipper allows polygons to clip both lines and other polygons, but doesn't allow lines to clip either lines or polygons.

With closed paths, orientation should conform with the filling rule that will be passed via Clippper's Execute method.

+ + + +

Path Coordinate range:
Path coordinates must be between ± 0x3FFFFFFFFFFFFFFF (± 4.6e+18), otherwise a range error will be thrown when attempting to add the path to the Clipper object. If coordinates can be kept between ± 0x3FFFFFFF (± 1.0e+9), a modest increase in performance (approx. 15-20%) over the larger range can be achieved by avoiding large integer math. If the preprocessor directive use_int32 is defined (allowing a further increase in performance of 20-30%), then the maximum range is restricted to ± 32,767.

+ + +

Return Value:
The function will return false if the path is invalid for clipping. A path is invalid for clipping when: +

    + +
  • it has less than 2 vertices
  • + +
  • it has 2 vertices but is not an open path
  • + +
  • the vertices are all co-linear and it is not an open path
  • + +

+ +
+ + + + +

See Also

+

Example, Clipper.Execute, AddPaths, Orientation, Defines, Paths, PolyFillType, PolyType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/Clear.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/Clear.htm new file mode 100644 index 0000000..e23900b --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/Clear.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + Clear + + + + + + + + + + + + + + + + + + + + + + +

ClipperBase.Clear

+ + +

Del.» procedure Clear;

+ +

C++ » virtual void Clear();

+ +

C#  » public void Clear() {};

+ +

The Clear method removes any existing subject and clip polygons allowing the Clipper object to be reused for clipping operations on different polygon sets.

+ + + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/GetBounds.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/GetBounds.htm new file mode 100644 index 0000000..efb949a --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/Methods/GetBounds.htm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + GetBounds + + + + + + + + + + + + + + + + + + + + + + +

ClipperBase.GetBounds

+ + +

Del.» function GetBounds: TIntRect;

+ +

C++ » IntRect GetBounds();

+ +

C#  » public IntRect GetBounds() {...};

+
+ +

This method returns the axis-aligned bounding rectangle of all polygons that have been added to the Clipper object.

+ +
+ + + +

See Also

+

Example, IntRect

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/_Body.htm new file mode 100644 index 0000000..64a6058 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperBase/_Body.htm @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + ClipperBase + + + + + + + + + + + + + + + + + + + + + + + +

ClipperBase

+ +

ClipperBase is an abstract base class for Clipper. A ClipperBase object should not be instantiated directly.

+ + +

Reference

+ + + + + + + + + + + + + + + + + + +
Methods +
In ClipperBase: +
AddPath +
AddPaths +
Clear +
GetBounds +
+

See Also

+

Clipper

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPath.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPath.htm new file mode 100644 index 0000000..6aa26b8 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPath.htm @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + AddPath + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.AddPath

+ + +

Del.» procedure AddPath(const Path: TPath; JoinType: TJoinType; EndType: TEndType);

+ +

C++ » void AddPath(const Path& path, JoinType jointype, EndType endtype);

+ +

C#  » public void AddPath(Path path, JoinType jointype, EndType endtype);

+
+ + +

Adds a Path to a ClipperOffset object in preparation for offsetting.

Any number of paths can be added, and each has its own JoinType and EndType. All 'outer' Paths must have the same orientation, and any 'hole' paths must have reverse orientation. Closed paths must have at least 3 vertices. Open paths may have as few as one vertex. Open paths can only be offset with positive deltas.

+ +

See Also

+

EndType, JoinType, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPaths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPaths.htm new file mode 100644 index 0000000..b0fe3bc --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/AddPaths.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + AddPaths + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.AddPaths

+ + +

Del.» procedure AddPaths(const Paths: TPaths; JoinType: TJoinType; EndType: TEndType);

+ +

C++ » void AddPaths(const Paths& paths, JoinType jointype, EndType endtype);

+ +

C#  » public void AddPaths(Paths paths, JoinType jointype, EndType endtype);

+
+ + +

Adds Paths to a ClipperOffset object in preparation for offsetting.

Any number of paths can be added, and each path has its own JoinType and EndType. All 'outer' Paths must have the same orientation, and any 'hole' paths must have reverse orientation. Closed paths must have at least 3 vertices. Open paths may have as few as one vertex. Open paths can only be offset with positive deltas.

+ + +

See Also

+

EndType, JoinType, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Clear.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Clear.htm new file mode 100644 index 0000000..7df15a6 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Clear.htm @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + Clear + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.Clear

+ + +

Del.» procedure Clear;

+ +

C++ » void Clear();

+ +

C#  » public void Clear();

+
+ + +

This method clears all paths from the ClipperOffset object, allowing new paths to be assigned.

+ + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Constructor.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Constructor.htm new file mode 100644 index 0000000..59eed6e --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Constructor.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + Constructor + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.Constructor

+ + +

Del.» constructor Create(MiterLimit: Double = 2; RoundPrecision: Double = 0.25);

+ +

C++ » ClipperOffset( double miterLimit = 2.0, double roundPrecision = 0.25);

+ +

C#  » public ClipperOffset( double miterLimit = 2.0, double roundPrecision = 0.25);

+
+ + +

The ClipperOffset constructor takes 2 optional parameters: MiterLimit and ArcTolerance. Thes two parameters corresponds to properties of the same name. MiterLimit is only relevant when JoinType is jtMiter, and ArcTolerance is only relevant when JoinType is jtRound or when EndType is etOpenRound.

+ + +

See Also

+

ArcTolerance, MiterLimit

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Execute.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Execute.htm new file mode 100644 index 0000000..5ac748d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Methods/Execute.htm @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + Execute + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.Execute

+ + +

Del.» procedure Execute(out solution: TPaths; Delta: Double); overload;

+ +

C++ » void Execute(Paths& solution, double delta);

+ +

C#  » public void Execute(ref Paths solution, double delta);

+
+ +

Del.» procedure Execute(out PolyTree: TPolyTree; Delta: Double); overload;

+ +

C++ » void Execute(PolyTree& polytree, double delta);

+ +

C#  » public void Execute(ref PolyTree polytree, double delta);

+
+ + +

This method takes two parameters. The first is the structure that receives the result of the offset operation (either a PolyTree or a Paths structure). The second parameter is the amount to which the supplied paths will be offset. Negative delta values shrink polygons and positive delta expand them.

This method can be called multiple times, offsetting the same paths by different amounts (ie using different deltas).

+ + + + + + +
+ +
+#include "clipper.hpp"  
+...
+using namespace ClipperLib;
+
+int main()
+{
+  Path subj;
+  Paths solution;
+  subj << 
+    IntPoint(348,257) << IntPoint(364,148) << IntPoint(362,148) << 
+    IntPoint(326,241) << IntPoint(295,219) << IntPoint(258,88) << 
+    IntPoint(440,129) << IntPoint(370,196) << IntPoint(372,275);
+  ClipperOffset co;
+  co.AddPath(subj, jtRound, etClosedPolygon);
+  co.Execute(solution, -7.0);
+  
+  //draw solution ...
+  DrawPolygons(solution, 0x4000FF00, 0xFF009900);
+}
+          
+ + +

 

+ + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm new file mode 100644 index 0000000..9cfc79c --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + ArcTolerance + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.ArcTolerance

+ +

Del.» property ArcTolerance: double; //read and write

+ +

C++ » double ArcTolerance;

+ +

C#  » public double ArcTolerance {get; set;}

+
+ +

Firstly, this field/property is only relevant when JoinType = jtRound and/or EndType = etRound.

Since flattened paths can never perfectly represent arcs, this field/property specifies a maximum acceptable imprecision ('tolerance') when arcs are approximated in an offsetting operation. Smaller values will increase 'smoothness' up to a point though at a cost of performance and in creating more vertices to construct the arc.

The default ArcTolerance is 0.25 units. This means that the maximum distance the flattened path will deviate from the 'true' arc will be no more than 0.25 units (before rounding).

Reducing tolerances below 0.25 will not improve smoothness since vertex coordinates will still be rounded to integer values. The only way to achieve sub-integer precision is through coordinate scaling before and after offsetting (see example below).

It's important to make ArcTolerance a sensible fraction of the offset delta (arc radius). Large tolerances relative to the offset delta will produce poor arc approximations but, just as importantly, very small tolerances will substantially slow offsetting performance while providing unnecessary degrees of precision. This is most likely to be an issue when offsetting polygons whose coordinates have been scaled to preserve floating point precision.

Example: Imagine a set of polygons (defined in floating point coordinates) that is to be offset by 10 units using round joins, and the solution is to retain floating point precision up to at least 6 decimal places.
To preserve this degree of floating point precision, and given that Clipper and ClipperOffset both operate on integer coordinates, the polygon coordinates will be scaled up by 108 + (and rounded to integers) prior to offsetting. Both offset delta and ArcTolerance will also need to be scaled by this same factor. If ArcTolerance was left unscaled at the default 0.25 units, every arc in the solution would contain a fraction of 44 THOUSAND vertices while the final arc imprecision would be 0.25 × 10-8 + units (ie once scaling was reversed). However, if 0.1 units was an acceptable imprecision in the final unscaled solution, then ArcTolerance should be set to 0.1 × scaling_factor (0.1 × 108 + ). Now if scaling is applied equally to both ArcTolerance and to Delta Offset, then in this example the number of vertices (steps) defining each arc would be a fraction of 23.

The formula for the number of steps in a full circular arc is ... Pi / acos(1 - arc_tolerance / abs(delta))

+ +

See Also

+

offset_triginometry2

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/MiterLimit.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/MiterLimit.htm new file mode 100644 index 0000000..3bdf826 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/MiterLimit.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + MiterLimit + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset.MiterLimit

+ + +

Del.» property MiterLimit: double; //read and write

+ +

C++ » double MiterLimit;

+ +

C#  » public double MiterLimit {get; set;}

+
+ + +

This property sets the maximum distance in multiples of delta that vertices can be offset from their original positions before squaring is applied. (Squaring truncates a miter by 'cutting it off' at 1 × delta distance from the original vertex.)

The default value for MiterLimit is 2 (ie twice delta). This is also the smallest MiterLimit that's allowed. If mitering was unrestricted (ie without any squaring), then offsets at very acute angles would generate unacceptably long 'spikes'.

An example of an offsetting 'spike' at a narrow angle that's a consequence of using a large MiterLimit (25) ...

+ + + +

See Also

+

JoinType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/_Body.htm new file mode 100644 index 0000000..337ed71 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/_Body.htm @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + ClipperOffset + + + + + + + + + + + + + + + + + + + + + + + +

ClipperOffset

+
+ +

The ClipperOffset class encapsulates the process of offsetting (inflating/deflating) both open and closed paths using a number of different join types and end types.

(This class replaces the now deprecated OffsetPaths function which was less flexible.)

Preconditions for offsetting:
1. The orientations of closed paths must be consistent such that outer polygons share the same orientation, and any holes have the opposite orientation (ie non-zero filling). Open paths must be oriented with closed outer polygons.
2. Polygons must not self-intersect.

Limitations:
When offsetting, small artefacts may appear where polygons overlap. To avoid these artefacts, offset overlapping polygons separately.


+ + + + + + +
+ +
+#include "clipper.hpp"  
+...
+using namespace ClipperLib;
+
+int main()
+{
+  Path subj;
+  Paths solution;
+  subj << 
+    IntPoint(348,257) << IntPoint(364,148) << IntPoint(362,148) << 
+    IntPoint(326,241) << IntPoint(295,219) << IntPoint(258,88) << 
+    IntPoint(440,129) << IntPoint(370,196) << IntPoint(372,275);
+  ClipperOffset co;
+  co.AddPath(subj, jtRound, etClosedPolygon);
+  co.Execute(solution, -7.0);
+  
+  //draw solution ...
+  DrawPolygons(solution, 0x4000FF00, 0xFF009900);
+}
+          
+ + +

 

+ +

Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Methods + Properties +
In ClipperOffset: +
AddPath + ArcTolerance +
AddPaths + MiterLimit +
Clear + +
Constructor + +
Execute + +
+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Methods/GetNext.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Methods/GetNext.htm new file mode 100644 index 0000000..973c8bc --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Methods/GetNext.htm @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + GetNext + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.GetNext

+ + +

Del.» function GetNext: TPolyNode;

+ +

C++ » PolyNode* GetNext();

+ +

C#  » public PolyNode GetNext();

+ + +

The returned Polynode will be the first child if any, otherwise the next sibling, otherwise the next sibling of the Parent etc.

A PolyTree can be traversed very easily by calling GetFirst() followed by GetNext() in a loop until the returned object is a null pointer ...

+ + + + + + +
+ +
+  PolyTree polytree;
+  //call to Clipper.Execute method here which fills 'polytree'
+  
+  PolyNode* polynode = polytree.GetFirst();
+  while (polynode)
+  {
+    //do stuff with polynode here
+	
+    polynode = polynode->GetNext();
+  }
+  
+          
+ +

+ + + +

See Also

+

PolyTree.GetFirst

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/ChildCount.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/ChildCount.htm new file mode 100644 index 0000000..4cff1f3 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/ChildCount.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + ChildCount + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.ChildCount

+ + +

Del.» property ChildCount: Integer; //read only

+ +

C++ » ChildCount(); //read only

+ +

C#  » public int ChildCount; //read only

+ +
+ +

Returns the number of PolyNode Childs directly owned by the PolyNode object.

+ + +

See Also

+

Childs

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Childs.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Childs.htm new file mode 100644 index 0000000..92e68c4 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Childs.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + Childs + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.Childs

+ + +

Del.» property Childs[index: Integer]: TPolyNode; //read only

+ +

C++ » std::vector < PolyNode* > Childs;//public field

+ +

C#  » public List < PolyNode > Childs; //read only property

+ + +

A read-only list of PolyNode.
Outer PolyNode childs contain hole PolyNodes, and hole PolyNode childs contain nested outer PolyNodes.

+ + + + +

See Also

+

ChildCount

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Contour.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Contour.htm new file mode 100644 index 0000000..cd4c161 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Contour.htm @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + Contour + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.Contour

+ +

Del.» property Contour: TPath; //read only

+ +

C++ » Path Contour; //public field

+ +

C#  » public Path Contour; //read only property

+ + +

Returns a path list which contains any number of vertices.

+ + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsHole.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsHole.htm new file mode 100644 index 0000000..fb1375d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsHole.htm @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + IsHole + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.IsHole

+ + +

Del.» IsHole: Boolean; //read only

+ +

C++ » bool IsHole; //field

+ +

C#  » public bool IsHole; //read only property

+ + +

Returns true when the PolyNode's polygon (Contour) is a hole.

Children of outer polygons are always holes, and children of holes are always (nested) outer polygons.
The IsHole property of a PolyTree object is undefined but its children are always top-level outer polygons.

+ +

See Also

+

Overview, Contour, PolyTree

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsOpen.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsOpen.htm new file mode 100644 index 0000000..be9886d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/IsOpen.htm @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + IsOpen + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.IsOpen

+ + +

Del.» IsOpen: Boolean; //read only

+ +

C++ » bool IsOpen; //field

+ +

C#  » public bool IsOpen; //read only property

+ + +

Returns true when the PolyNode's Contour results from a clipping operation on an open contour (path). Only top-level PolyNodes can contain open contours.

+ +

See Also

+

Contour

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Parent.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Parent.htm new file mode 100644 index 0000000..ae17bf5 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/Properties/Parent.htm @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + Parent + + + + + + + + + + + + + + + + + + + + + + +

PolyNode.Parent

+ +

Del.» Parent: TPolyNode; //read only

+ +

C++ » PolyNode* Parent; //field

+ +

C#  » public PolyNode Parent; //read only property

+ + +

Returns the parent PolyNode.

The PolyTree object (which is also a PolyNode) does not have a parent and will return a null pointer.

+ + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/_Body.htm new file mode 100644 index 0000000..9f6eb0f --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyNode/_Body.htm @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + PolyNode + + + + + + + + + + + + + + + + + + + + + + + +

PolyNode

+
+ +

PolyNodes are encapsulated within a PolyTree container, and together provide a data structure representing the parent-child relationships of polygon contours returned by Clipper's Execute method.

A PolyNode object represents a single polygon. Its IsHole property indicates whether it's an outer or a hole. PolyNodes may own any number of PolyNode children (Childs), where children of outer polygons are holes, and children of holes are (nested) outer polygons.

+ +

Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fields + Methods + Properties +
In PolyNode: +
+ GetNext + ChildCount +
+ + Childs +
+ + Contour +
+ + IsHole +
+ + IsOpen +
+ + Parent +
+

See Also

+

Overview, Clipper.Execute, PolyTree

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/Clear.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/Clear.htm new file mode 100644 index 0000000..d30fb15 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/Clear.htm @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + Clear + + + + + + + + + + + + + + + + + + + + + + +

PolyTree.Clear

+ + +

Del.» procedure Clear;

+ +

C++ » void Clear();

+ +

C#  » public void Clear();

+
+ + +

This method clears any PolyNode children contained by PolyTree the object.

Clear does not need to be called explicitly. The Clipper.Execute method that accepts a PolyTree parameter will automatically clear the PolyTree object before propagating it with new PolyNodes. Likewise, PolyTree's destructor will also automatically clear any contained PolyNodes.

+ +

See Also

+

Clipper.Execute

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/GetFirst.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/GetFirst.htm new file mode 100644 index 0000000..27011d4 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Methods/GetFirst.htm @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + GetFirst + + + + + + + + + + + + + + + + + + + + + + +

PolyTree.GetFirst

+ + +

Del.» function GetFirst: TPolyNode;

+ +

C++ » PolyNode* GetFirst();

+ +

C#  » public PolyNode GetFirst();

+ + +

This method returns the first outer polygon contour if any, otherwise a null pointer.

This function is almost equivalent to calling Childs[0] except that when a PolyTree object is empty (has no children), calling Childs[0] would raise an out of range exception.

+ + + + + + +

See Also

+

PolyNode.GetNext, PolyNode.ChildCount, PolyNode.Childs

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Properties/Total.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Properties/Total.htm new file mode 100644 index 0000000..7a6efe2 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/Properties/Total.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + Total + + + + + + + + + + + + + + + + + + + + + + +

PolyTree.Total

+ + +

Del.» property Total: Integer; //read only

+ +

C++ » Total(); //read only

+ +

C#  » public int Total; //read only

+ +
+ +

Returns the total number of PolyNodes (polygons) contained within the PolyTree. This value is not to be confused with ChildCount which returns the number of immediate children only (Childs) contained by PolyTree.

+ + +

See Also

+

PolyNode.ChildCount, PolyNode.Childs

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/_Body.htm new file mode 100644 index 0000000..175df23 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Classes/PolyTree/_Body.htm @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + PolyTree + + + + + + + + + + + + + + + + + + + + + + + +

PolyTree

+

Hierarchy

+

+

   |

+

PolyNode

+
+ +

PolyTree is intended as a read-only data structure that should only be used to receive solutions from clipping and offsetting operations. It's an alternative to the Paths data structure which also receives these solutions. PolyTree's two major advantages over the Paths structure are: it properly represents the parent-child relationships of the returned polygons; it differentiates between open and closed paths. However, since PolyTree is a more complex structure than the Paths structure, and since it's more computationally expensive to process (the Execute method being roughly 5-10% slower), it should used only be when parent-child polygon relationships are needed, or when open paths are being 'clipped'.

An empty PolyTree object can be passed as the solution parameter in Clipper.Execute and in ClipperOffset.Execute. Once the clipping or offseting operation is completed, the method returns with the PolyTree structure filled with data representing the solution.

A PolyTree object is a container for any number of PolyNode children, with each contained PolyNode representing a single polygon contour (either an outer or hole polygon). PolyTree itself is a specialized PolyNode whose immediate children represent the top-level outer polygons of the solution. (Its own Contour property is always empty.) The contained top-level PolyNodes may contain their own PolyNode children representing hole polygons that may also contain children representing nested outer polygons etc. Children of outers will always be holes, and children of holes will always be outers.

PolyTrees can also contain open paths. Open paths will always be represented by top level PolyNodes. Two functions are provided to quickly separate out open and closed paths from a polytree - OpenPathsFromPolyTree and ClosedPathsFromPolyTree.

+ + + + + + + + + + +
+ + + + +
+
+    polytree: 
+    Contour = ()
+    ChildCount = 1
+    Childs[0]: 
+        Contour = ((10,10),(100,10),(100,100),(10,100))
+        IsHole = False
+        ChildCount = 1
+        Childs[0]: 
+            Contour = ((20,20),(20,90),(90,90),(90,20))
+            IsHole = True
+            ChildCount = 2
+            Childs[0]: 
+                Contour = ((30,30),(50,30),(50,50),(30,50))
+                IsHole = False
+                ChildCount = 0
+            Childs[1]: 
+                Contour = ((60,60),(80,60),(80,80),(60,80))
+                IsHole = False
+                ChildCount = 0
+
+            
+ +

+ +

Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fields + Methods + Properties +
In PolyTree: +
+ Clear + Total +
+ GetFirst + +
In PolyNode: +
+ GetNext + ChildCount +
+ + Childs +
+ + Contour +
+ + IsHole +
+ + IsOpen +
+ + Parent +
+

See Also

+

Overview, Clipper.Execute, ClipperOffset.Execute, PolyNode, ClosedPathsFromPolyTree, OpenPathsFromPolyTree, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Area.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Area.htm new file mode 100644 index 0000000..be30baf --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Area.htm @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + Area + + + + + + + + + + + + + + + + + + + + + +

Area

+ + +

Del.» function Area(const pts: TPath): double;

+ +

C++ » double Area(const Path &poly);

+ +

C#  » public static double Area(Path poly);

+ + +

This function returns the area of the supplied polygon. It's assumed that the path is closed and does not self-intersect. Depending on orientation, this value may be positive or negative. If Orientation is true, then the area will be positive and conversely, if Orientation is false, then the area will be negative.

+
+ + +

See Also

+

Orientation, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygon.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygon.htm new file mode 100644 index 0000000..5a857fe --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygon.htm @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + CleanPolygon + + + + + + + + + + + + + + + + + + + + + +

CleanPolygon

+ + +

Del.» function CleanPolygon(const Poly: TPath; Distance: double = 1.415): TPath;

+ +

C++ » void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415);

+ +

C++ » void CleanPolygon(Path &poly, double distance = 1.415);

+ +

C#  » public static Path CleanPolygon(Path poly, double distance = 1.415);

+
+ + +

Removes vertices: +

    + +
  • that join co-linear edges, or join edges that are almost co-linear (such that if the vertex was moved no more than the specified distance the edges would be co-linear)
  • + +
  • that are within the specified distance of an adjacent vertex
  • + +
  • that are within the specified distance of a semi-adjacent vertex together with their out-lying vertices
  • + +

+ +

Vertices are semi-adjacent when they are separated by a single (out-lying) vertex.

The distance parameter's default value is approximately √2 so that a vertex will be removed when adjacent or semi-adjacent vertices having their corresponding X and Y coordinates differing by no more than 1 unit. (If the egdes are semi-adjacent the out-lying vertex will be removed too.)

C++ only: This function is overloaded. In the first definition, the in_poly and out_poly parameters can reference the same Path object though in that case the calling code might be clearer if the second definition (accepting a single Paths parameter) is used.

   

+ + + + + +

See Also

+

CleanPolygons, SimplifyPolygon, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygons.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygons.htm new file mode 100644 index 0000000..7ec56ed --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/CleanPolygons.htm @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + CleanPolygons + + + + + + + + + + + + + + + + + + + + + +

CleanPolygons

+ + +

Del.» function CleanPolygons(const Polys: TPaths; Distance: double = 1.415): TPaths;

+ +

C++ » void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance = 1.415);

+ +

C++ » void CleanPolygons(Paths &polys, double distance = 1.415);

+ +

C#  » public static Paths CleanPolygons(Paths polys, double distance = 1.415);

+
+ + +

Removes vertices: +

    + +
  • that join co-linear edges, or join edges that are almost co-linear (such that if the vertex was moved no more than the specified distance the edges would be co-linear)
  • + +
  • that are within the specified distance of an adjacent vertex
  • + +
  • that are within the specified distance of a semi-adjacent vertex together with their out-lying vertices
  • + +

+ +

Vertices are semi-adjacent when they are separated by a single (out-lying) vertex.

The distance parameter's default value is approximately √2 so that a vertex will be removed when adjacent or semi-adjacent vertices having their corresponding X and Y coordinates differing by no more than 1 unit. (If the egdes are semi-adjacent the out-lying vertex will be removed too.)

C++ only: This function is overloaded. In the first definition, the in_polys and out_polys parameters can reference the same Paths object though in that case the calling code might be clearer if the second definition (accepting a single Paths parameter) is used.

   

+ + + + +

See Also

+

CleanPolygon, SimplifyPolygons

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ClosedPathsFromPolyTree.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ClosedPathsFromPolyTree.htm new file mode 100644 index 0000000..e050135 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ClosedPathsFromPolyTree.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + ClosedPathsFromPolyTree + + + + + + + + + + + + + + + + + + + + + +

ClosedPathsFromPolyTree

+ + +

Del.» function ClosedPathsFromPolyTree(PolyTree: TPolyTree): TPaths;

+ +

C++ » void ClosedPathsFromPolyTree(PolyTree& polytree, Paths& paths);

+ +

C#  » public static void ClosedPathsFromPolyTree(PolyTree polytree, Paths paths);

+
+ + +

This function filters out open paths from the PolyTree structure and returns only closed paths in a Paths structure.

+ +

See Also

+

PolyTree, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiDiff.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiDiff.htm new file mode 100644 index 0000000..0a3ee4b --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiDiff.htm @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + MinkowskiDiff + + + + + + + + + + + + + + + + + + + + + +

MinkowskiDiff

+ + +

Del.» function MinkowskiDiff(const Poly1: TPath; const Poly2: TPath): TPaths;

+ +

C++ » void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution);

+ +

C#  » public static Paths MinkowskiDiff(Path poly1, Path poly2);

+
+ + +

Minkowski Difference is performed by subtracting each point in a polygon from the set of points in an open or closed path. A key feature of Minkowski Difference is that when it's applied to two polygons, the resulting polygon will contain the coordinate space origin whenever the two polygons touch or overlap. (This function is often used to determine when polygons collide.)

In the image on the left the blue polygon is the 'minkowski difference' of the two red boxes. The black dot represents the coordinate space origin.

+ + + + +

See Also

+

MinkowskiSum, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiSum.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiSum.htm new file mode 100644 index 0000000..0d4429d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/MinkowskiSum.htm @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + MinkowskiSum + + + + + + + + + + + + + + + + + + + + + +

MinkowskiSum

+ + +

Del.» function MinkowskiSum(const Pattern: TPath; const Path: TPath; PathIsClosed: Boolean): TPaths; overload;

+ +

Del.» function MinkowskiSum(const Pattern: TPath; const Paths: TPaths; PathFillType: TPolyFillType; PathIsClosed: Boolean): TPaths; overload;

+ +

C++ » void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed);

+ +

C++ » void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, PolyFillType pathFillType, bool pathIsClosed);

+ +

C#  » public static Paths MinkowskiSum(Path pattern, Path path, bool pathIsClosed);

+ +

C#  » public static Paths MinkowskiSum(Path pattern, Paths paths, PolyFillType pathFillType, bool pathIsClosed);

+
+ + +

Minkowski Addition is performed by adding each point in a polygon 'pattern' to the set of points in an open or closed path. The resulting polygon (or polygons) defines the region that the 'pattern' would pass over in moving from the beginning to the end of the 'path'.

+ + +

+

+      Path path = new Path();
+      Path pattern = new Path();
+      Paths solution = new Paths();
+
+      //Greek capital sigma (sum sign) ... 
+      Int64[] ints1 = new Int64[] { 300, 400, 100, 400, 200, 300, 100, 200, 300, 200 };
+      path = IntsToPolygon(ints1);
+
+      //diagonal brush pattern ...
+      Int64[] ints2 = new Int64[] { 4, -6, 6, -6, -4, 6, -6, 6 };
+      pattern = IntsToPolygon(ints2);
+
+      solution = Clipper.MinkowskiSum(pattern, path, false);
+      //move 'pattern' to the end of 'path' ...
+      pattern = TranslatePath(pattern, 300, 200);
+
+      //Display solution ± pattern ...
+    

+ + + +

See Also

+

MinkowskiDiff

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OffsetPaths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OffsetPaths.htm new file mode 100644 index 0000000..a0787b7 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OffsetPaths.htm @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + OffsetPaths + + + + + + + + + + + + + + + + + + + + + +

OffsetPaths

+ + +

Del.» function OffsetPaths(const polys: Paths; const delta: double; JoinType: TJoinType = jtSquare; EndType: TEndType = etClosed; Limit: double = 0.0): TPaths;

+ +

C++ » void OffsetPaths(const Paths &in_polys, Paths &out_polys, double delta, JoinType jointype = jtSquare, EndType endtype = etClosed, double limit = 0.0);

+ +

C#  » public static Paths OffsetPaths(Paths polys, double delta, JoinType jointype = JoinType.jtSquare, EndType endtype = EndType.etClosed, double limit = 0.0);

+
+ +

Deprecated. (See ClipperOffset.)

+ +

See Also

+

ClipperOffset

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OpenPathsFromPolyTree.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OpenPathsFromPolyTree.htm new file mode 100644 index 0000000..3ce9fa9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/OpenPathsFromPolyTree.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + OpenPathsFromPolyTree + + + + + + + + + + + + + + + + + + + + + +

OpenPathsFromPolyTree

+ + +

Del.» function OpenPathsFromPolyTree(PolyTree: TPolyTree): TPaths;

+ +

C++ » void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths);

+ +

C#  » public static void OpenPathsFromPolyTree(PolyTree polytree, Paths paths);

+
+ + +

This function filters out closed paths from the PolyTree structure and returns only open paths in a Paths structure.

+ +

See Also

+

PolyTree, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Orientation.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Orientation.htm new file mode 100644 index 0000000..6dc02c9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/Orientation.htm @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + Orientation + + + + + + + + + + + + + + + + + + + + + +

Orientation

+ + +

Del.» function Orientation(const poly: TPath): boolean;

+ +

C++ » bool Orientation(const Path &poly); // Function in the ClipperLib namespace.

+ +

C#  » public static bool Orientation(Path poly); // Static method of the Clipper class in the ClipperLib namespace.

+ + +

Orientation is only important to closed paths. Given that vertices are declared in a specific order, orientation refers to the direction (clockwise or counter-clockwise) that these vertices progress around a closed path.

Orientation is also dependent on axis direction:
+

    + +
  • On Y-axis positive upward displays, Orientation will return true if the polygon's orientation is counter-clockwise.
  • + +
  • On Y-axis positive downward displays, Orientation will return true if the polygon's orientation is clockwise.
  • + +

+ + +



Notes:
+

    + +
  • Self-intersecting polygons have indeterminate orientations in which case this function won't return a meaningful value.
  • + +
  • The majority of 2D graphic display libraries (eg GDI, GDI+, XLib, Cairo, AGG, Graphics32) and even the SVG file format have their coordinate origins at the top-left corner of their respective viewports with their Y axes increasing downward. However, some display libraries (eg Quartz, OpenGL) have their coordinate origins undefined or in the classic bottom-left position with their Y axes increasing upward.
  • + +
  • For Non-Zero filled polygons, the orientation of holes must be opposite that of outer polygons.
  • + +
  • For closed paths (polygons) in the solution returned by Clipper's Execute method, their orientations will always be true for outer polygons and false for hole polygons (unless the ReverseSolution property has been enabled).
  • + +

+
+ +

See Also

+

Overview, Clipper.ReverseSolution, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PointInPolygon.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PointInPolygon.htm new file mode 100644 index 0000000..6ae3214 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PointInPolygon.htm @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + PointInPolygon + + + + + + + + + + + + + + + + + + + + + +

PointInPolygon

+ + +

Del.» function PointInPolygon(const Pt: TIntPoint; const poly: TPath): Integer;

+ +

C++ » int PointInPolygon(const IntPoint pt, const Path &poly); // Function in the ClipperLib namespace.

+ +

C#  » public static int PointInPolygon(IntPoint pt, Path poly); // Static method of the Clipper class.

+ + +

Returns 0 when false, -1 when pt is on poly and +1 when pt is in poly.

It's assumed that 'poly' is closed and does not self-intersect.

+ +
+ +

See Also

+

IntPoint, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PolyTreeToPaths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PolyTreeToPaths.htm new file mode 100644 index 0000000..a4762d2 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/PolyTreeToPaths.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + PolyTreeToPaths + + + + + + + + + + + + + + + + + + + + + +

PolyTreeToPaths

+ + +

Del.» function PolyTreeToPaths(PolyTree: TPolyTree): TPaths;

+ +

C++ » void PolyTreeToPaths(PolyTree& polytree, Paths& paths);

+ +

C#  » public static Paths PolyTreeToPaths(PolyTree polytree);

+
+ + +

This function converts a PolyTree structure into a Paths structure.

+ +

See Also

+

PolyTree, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePath.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePath.htm new file mode 100644 index 0000000..d0fe8ec --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePath.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + ReversePath + + + + + + + + + + + + + + + + + + + + + +

ReversePath

+ + +

Del.» function ReversePath(const polys: TPath): TPath;

+ +

C++ » void ReversePath(const Path &p);

+ +

C#  » //Call Path.Reverse().

+
+ +

Reverses the vertex order (and hence orientation) in the specified path.

+ + +

See Also

+

Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePaths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePaths.htm new file mode 100644 index 0000000..19ebbad --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/ReversePaths.htm @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + ReversePaths + + + + + + + + + + + + + + + + + + + + + +

ReversePaths

+ + +

Del.» function ReversePaths(const p: TPaths): TPaths;

+ +

C++ » void ReversePaths(const Paths &p);

+ +

C#  » void ReversePaths( Paths p );

+
+ + +

Reverses the vertex order (and hence orientation) in each contained path.

+ + + + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm new file mode 100644 index 0000000..3462f82 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + SimplifyPolygon + + + + + + + + + + + + + + + + + + + + + +

SimplifyPolygon

+ + +

Del.» function SimplifyPolygon(const Poly: TPath; FillType: TPolyFillType = pftEvenOdd): TPaths;

+ +

C++ » void SimplifyPolygon(const Path &in_poly, Paths &out_polys,
        PolyFillType fillType = pftEvenOdd);

+ +

C#  » public static Paths SimplifyPolygon(Path poly,
        PolyFillType fillType = PolyFillType.pftEvenOdd);

+
+ + +

Removes self-intersections from the supplied polygon (by performing a boolean union operation using the nominated PolyFillType).
Polygons with non-contiguous duplicate vertices (ie 'touching') will be split into two polygons.

Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress.


+ + + + + +

See Also

+

Clipper.StrictlySimple, CleanPolygon, Path, PolyFillType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygons.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygons.htm new file mode 100644 index 0000000..69c791b --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygons.htm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + SimplifyPolygons + + + + + + + + + + + + + + + + + + + + + +

SimplifyPolygons

+ + +

Del.» function SimplifyPolygons(const polys: TPaths;
        FillType: TPolyFillType = pftEvenOdd): TPaths;

+ +

C++ » void SimplifyPolygons(const Paths &in_polys, Paths &out_polys,
        PolyFillType fillType = pftEvenOdd);

+ +

C++ » void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd);

+ +

C#  » public static Polygons SimplifyPolygons(Paths polys,
        PolyFillType fillType = PolyFillType.pftEvenOdd);

+
+ + +

Removes self-intersections from the supplied polygons (by performing a boolean union operation using the nominated PolyFillType).
Polygons with non-contiguous duplicate vertices (ie 'vertices are touching') will be split into two polygons.

C++ only: This function is overloaded. In the first definition, the in_polys and out_polys parameters can reference the same Paths object though in that case the calling code might be clearer if the second definition (accepting a single Paths parameter) is used.

Note: There's currently no guarantee that polygons will be strictly simple since 'simplifying' is still a work in progress.


+ + + + +

See Also

+

Clipper.StrictlySimple, CleanPolygons, PolyFillType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/CInt.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/CInt.htm new file mode 100644 index 0000000..1af7f15 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/CInt.htm @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + CInt + + + + + + + + + + + + + + + + + + + + + +

CInt

+ + +

Del.»
{$IFDEF use_int32}
 cInt = Int32;
{$ELSE}
  cInt = Int64;
{$ENDIF}

+ +

C++ »
#ifdef use_int32
  typedef int cInt;
#else
  typedef signed long long cInt;
#endif

+ +

C#  »
#if use_int32
  using cInt = Int32;
#else
  using cInt = Int64;
#endif

+ + +

cInt is the integer type used by the Clipper Library to represent vertex coordinate values. (See also IntPoint.)

The library uses integers instead of floating point values to preserve numerical robustness. (Very early versions of the library used floating point coordinates, but it became apparent that floating point imprecision was always going to cause occasional errors.)

By default cInt represents a signed 64bit integer and polygon coordinates can have any value in the range ± 9.2e+18. This accommodates the scaling of floating point coordinate values to very large integers so that very high degrees of precision can be retained during clipping. However, if coordinate values can be kept within the range ± 3.0e+9, then by avoiding large integer math, a modest ~10% improvement in clipping performance is achieved.

If the preprocessor directive use_int32 is defined, cInt will represent a signed 32bit integer. This improves clipping performance by 20-30% but the trade-off is that coordinate values are restricted to the much narrower range of ± 46340.

+ +

See Also

+

Defines, IntPoint

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ClipType.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ClipType.htm new file mode 100644 index 0000000..aabc455 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ClipType.htm @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + ClipType + + + + + + + + + + + + + + + + + + + + + +

ClipType

+ + +

Del.» type TClipType = (ctIntersection, ctUnion, ctDifference, ctXor);

+ +

C++ » enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };

+ +

C#  » public enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };

+
+ + +

There are four boolean operations - AND, OR, NOT & XOR.

Given that subject and clip polygon brush 'filling' is defined both by their vertices and their respective filling rules, the four boolean operations can be applied to polygons to define new filling regions: +

    + +
  • AND (intersection) - create regions where both subject and clip polygons are filled
  • + +
  • OR (union) - create regions where either subject or clip polygons (or both) are filled
  • + +
  • NOT (difference) - create regions where subject polygons are filled except where clip polygons are filled
  • + +
  • XOR (exclusive or) - create regions where either subject or clip polygons are filled but not where both are filled
  • + +

+ + +


     

All polygon clipping is performed with a Clipper object with the specific boolean operation indicated by the ClipType parameter passed in its Execute method.


+ + +

With regard to open paths (polylines), clipping rules generally match those of closed paths (polygons).
However, when there are both polyline and polygon subjects, the following clipping rules apply: +

    + +
  • union operations - polylines will be clipped by any overlapping polygons so that non-overlapped portions will be returned in the solution together with the union-ed polygons
  • + +
  • intersection, difference and xor operations - polylines will be clipped only by 'clip' polygons and there will be not interaction between polylines and subject polygons.
  • + +


+ + +

Example of clipping behaviour when mixing polyline and polygon subjects:

+ + + +

See Also

+

Overview, Clipper, Clipper.Execute, PolyFillType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/EndType.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/EndType.htm new file mode 100644 index 0000000..3c1f21d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/EndType.htm @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + EndType + + + + + + + + + + + + + + + + + + + + + +

EndType

+ +

Del.» type TEndType = (etClosedPolygon, etClosedLine, etOpenSquare, etOpenRound, etOpenButt);

+ +

C++ » enum EndType {etClosedPolygon, etClosedLine, etOpenSquare, etOpenRound, etOpenButt};

+ +

C#  » public enum EndType {etClosedPolygon, etClosedLine, etOpenSquare, etOpenRound, etOpenButt};

+
+ +

The EndType enumerator has 5 values: +

    + +
  • etClosedPolygon: Ends are joined using the JoinType value and the path filled as a polygon
  • + +
  • etClosedLine: Ends are joined using the JoinType value and the path filled as a polyline
  • + +
  • etOpenSquare: Ends are squared off and extended delta units
  • + +
  • etOpenRound: Ends are rounded off and extended delta units
  • + +
  • etOpenButt: Ends are squared off with no extension.
  • + +
  • etOpenSingle: Offsets an open path in a single direction. Planned for a future update.
  • + +

+ + +

Note: With etClosedPolygon and etClosedLine types, the path closure will be the same regardless of whether or not the first and last vertices in the path match.




+ + +

See Also

+

ClipperOffset.AddPath, ClipperOffset.AddPaths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/InitOptions.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/InitOptions.htm new file mode 100644 index 0000000..0c41d2a --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/InitOptions.htm @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + InitOptions + + + + + + + + + + + + + + + + + + + + + +

InitOptions

+ + +

Del.» type TInitOption = (ioReverseSolution, ioStrictlySimple, ioPreserveCollinear);

+ +

C++ » enum InitOptions {
        ioReverseSolution  = 1,
        ioStrictlySimple   = 2,
        ioPreserveCollinear = 4};

+ +

C#  » public const int ioReverseSolution  = 1;
      public const int ioStrictlySimple   = 2;
      public const int ioPreserveCollinear = 4;

+
+ + +

+ + + + + +

See Also

+

Clipper.Constructor, Clipper.PreserveCollinear, Clipper.ReverseSolution, Clipper.StrictlySimple

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntPoint.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntPoint.htm new file mode 100644 index 0000000..7fbb7ff --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntPoint.htm @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + IntPoint + + + + + + + + + + + + + + + + + + + + + +

IntPoint

+ +

Del.» TIntPoint = record X, Y: cInt; end;

+ +

C++ » struct IntPoint { cInt X; cInt Y; ... };

+ +

C#  » public class IntPoint { public cInt X; { get; set; } public cInt Y; { get; set; } ... };

+
+ + +

The IntPoint structure is used to represent all vertices in the Clipper Library. An integer storage type has been deliberately chosen to preserve numerical robustness. (Early versions of the library used floating point coordinates, but it became apparent that floating point imprecision would always cause occasional errors.)

A sequence of IntPoints are contained within a Path structure to represent a single contour.

As of version 6, IntPoint now has an optional third member 'Z'. This can be enabled by exposing (ie uncommenting) the PreProcessor define 'use_xyz'. When the Z member is used, its values will be copied to corresponding verticies in solutions to clipping operations. However, at points of intersection where there's no corresponding Z value, the value will be assigned zero unless a new value is provided by a user supplied callback function.

Users wishing to clip or offset polygons containing floating point coordinates need to use appropriate scaling when converting these values to and from IntPoints.

See also the notes on rounding.

+ + + + +

See Also

+

Rounding, Clipper.ZFillFunction, Defines, CInt, Path, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntRect.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntRect.htm new file mode 100644 index 0000000..baaa12b --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/IntRect.htm @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + IntRect + + + + + + + + + + + + + + + + + + + + + +

IntRect

+ +

Del.»
TIntRect = record left, top, right, bottom: cInt; end;

+ +

C++ »
struct IntRect { cInt left; cInt top; cInt right; cInt bottom; ... };

+ +

C#  »
public class IntRect {
  public cInt left; { get; set; }
  public cInt top; { get; set; }
  public cInt right; { get; set; }
  public cInt bottom; { get; set; } ... };

+ +

Structure returned by Clipper's GetBounds method.


+ + +

See Also

+

ClipperBase.GetBounds, CInt

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/JoinType.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/JoinType.htm new file mode 100644 index 0000000..1820138 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/JoinType.htm @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + JoinType + + + + + + + + + + + + + + + + + + + + + +

JoinType

+ +

Del.» type TJoinType = (jtSquare, jtRound, jtMiter);

+ +

C++ » enum JoinType {jtSquare, jtRound, jtMiter};

+ +

C#  » public enum JoinType {jtSquare, jtRound, jtMiter};

+
+ +

When adding paths to a ClipperOffset object via the AddPaths method, the joinType parameter may be one of three types - jtMiter, jtSquare or jtRound.



+

    + +
  • jtMiter: There's a necessary limit to mitered joins since offsetting edges that join at very acute angles will produce excessively long and narrow 'spikes'. To contain these potential spikes, the ClippOffset object's MiterLimit property specifies a maximum distance that vertices will be offset (in multiples of delta). For any given edge join, when miter offsetting would exceed that maximum distance, 'square' joining is applied.
  • + +
  • jtRound: While flattened paths can never perfectly trace an arc, they are approximated by a series of arc chords (see ClipperObject's ArcTolerance property).
  • + +
  • jtSquare: Squaring is applied uniformally at all convex edge joins at 1 × delta.
  • + +

+ +

See Also

+

ClipperOffset, ClipperOffset.AddPaths, ClipperOffset.ArcTolerance, ClipperOffset.MiterLimit

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Path.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Path.htm new file mode 100644 index 0000000..f4d8748 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Path.htm @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + Path + + + + + + + + + + + + + + + + + + + + + +

Path

+ +

Del.» TPath = array of TIntPoint;

+ +

C++ » typedef std::vector<IntPoint> Path;

+ +

C#  » using Path = List<IntPoint>;

+
+ +

This structure contains a sequence of IntPoint vertices defining a single contour (see also terminology). Paths may be open and represent a series of line segments bounded by 2 or more vertices, or they may be closed and represent polygons. Whether or not a path is open depends on context. Closed paths may be 'outer' contours or 'hole' contours. Which they are depends on orientation.

Multiple paths can be grouped into a Paths structure.

+ +

See Also

+

Overview, Example, ClipperBase.AddPath, PolyTree, Orientation, IntPoint, Paths

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Paths.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Paths.htm new file mode 100644 index 0000000..417b069 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/Paths.htm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + Paths + + + + + + + + + + + + + + + + + + + + + +

Paths

+ + +

Del.» TPaths = array of TPath;

+ +

C++ » typedef std::vector< Path > Paths;

+ +

C#  » using Paths = List<List< IntPoint >>;

+
+ +

This structure is fundamental to the Clipper Library. It's a list or array of one or more Path structures. (The Path structure contains an ordered list of vertices that make a single contour.)

Paths may open (a series of line segments), or they may closed (polygons). Whether or not a path is open depends on context. Closed paths may be 'outer' contours or 'hole' contours. Which they are depends on orientation.

+
+ + + + + + +

See Also

+

Clipper.Execute, ClipperBase.AddPath, ClipperBase.AddPaths, OffsetPaths, IntPoint, Path

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyFillType.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyFillType.htm new file mode 100644 index 0000000..7a2f93d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyFillType.htm @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + PolyFillType + + + + + + + + + + + + + + + + + + + + + +

PolyFillType

+ + +

Del.» type TPolyFillType = (pftEvenOdd, pftNonZero, pftPositive, pftNegative);

+ +

C++ » enum PolyFillType {pftEvenOdd, pftNonZero, pftPositive, pftNegative};

+ +

C#  » public enum PolyFillType {pftEvenOdd, pftNonZero, pftPositive, pftNegative};

+
+ + +

Filling indicates those regions that are inside a closed path (ie 'filled' with a brush color or pattern in a graphical display) and those regions that are outside. The Clipper Library supports 4 filling rules: Even-Odd, Non-Zero, Positive and Negative.

The simplest filling rule is Even-Odd filling (sometimes called alternate filling). Given a group of closed paths start from a point outside the paths and progress along an imaginary line through the paths. When the first path is crossed the encountered region is filled. When the next path is crossed the encountered region is not filled. Likewise, each time a path is crossed, filling starts if it had stopped and stops if it had started.

With the exception of Even-Odd filling, all other filling rules rely on edge direction and winding numbers to determine filling. Edge direction is determined by the order in which vertices are declared when constructing a path. Edge direction is used to determine the winding number of each polygon subregion.

The winding number for each polygon sub-region can be derived by:

    + +
  1. starting with a winding number of zero and
  2. + +
  3. from a point (P1) that's outside all polygons, draw an imaginary line to a point that's inside a given sub-region (P2)
  4. + +
  5. while traversing the line from P1 to P2, for each path that crosses the imaginary line from right to left increment the winding number, and for each path that crosses the line from left to right decrement the winding number.
  6. + +
  7. Once you arrive at the given sub-region you have its winding number.
  8. + +

+ +


Even-Odd (Alternate): Odd numbered sub-regions are filled, while even numbered sub-regions are not.
Non-Zero (Winding): All non-zero sub-regions are filled.
Positive: All sub-regions with winding counts > 0 are filled.
Negative: All sub-regions with winding counts < 0 are filled.

Paths are added to a Clipper object using the AddPath or AddPaths methods and the filling rules (for subject and clip polygons separately) are specified in the Execute method.

Polygon regions are defined by one or more closed paths which may or may not intersect. A single polygon region can be defined by a single non-intersecting path or by multiple non-intersecting paths where there's typically an 'outer' path and one or more inner 'hole' paths. Looking at the three shapes in the image above, the middle shape consists of two concentric rectangles sharing the same clockwise orientation. With even-odd filling, where orientation can be disregarded, the inner rectangle would create a hole in the outer rectangular polygon. There would be no hole with non-zero filling. In the concentric rectangles on the right, where the inner rectangle is orientated opposite to the outer, a hole will be rendered with either even-odd or non-zero filling. A single path can also define multiple subregions if it self-intersects as in the example of the 5 pointed star shape below.

        

By far the most widely used fill rules are Even-Odd (aka Alternate) and Non-Zero (aka Winding). Most graphics rendering libraries (AGG, Android Graphics, Cairo, GDI+, OpenGL, Quartz 2D etc) and vector graphics storage formats (SVG, Postscript, Photoshop etc) support both these rules. However some libraries (eg Java's Graphics2D) only support one fill rule. Android Graphics and OpenGL are the only libraries (that I'm aware of) that support multiple filling rules.

It's useful to note that edge direction has no affect on a winding number's odd-ness or even-ness. (This is why orientation is ignored when the Even-Odd rule is employed.)

The direction of the Y-axis does affect polygon orientation and edge direction. However, changing Y-axis orientation will only change the sign of winding numbers, not their magnitudes, and has no effect on either Even-Odd or Non-Zero filling.

+ +

See Also

+

Clipper.Execute, ClipperBase.AddPath, ClipperBase.AddPaths, Orientation

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyType.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyType.htm new file mode 100644 index 0000000..48fc46c --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/PolyType.htm @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + PolyType + + + + + + + + + + + + + + + + + + + + + +

PolyType

+ + +

Del.» type TPolyType = (ptSubject, ptClip);

+ +

C++ » enum PolyType { ptSubject, ptClip };

+ +

C#  » public enum PolyType { ptSubject, ptClip };

+
+ +

Boolean (clipping) operations are mostly applied to two sets of Polygons, represented in this library as subject and clip polygons. Whenever Polygons are added to the Clipper object, they must be assigned to either subject or clip polygons.

UNION operations can be performed on one set or both sets of polygons, but all other boolean operations require both sets of polygons to derive meaningful solutions.

+ + + + +

See Also

+

ClipperBase.AddPath, ClipperBase.AddPaths, ClipType

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ZFillCallback.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ZFillCallback.htm new file mode 100644 index 0000000..95862c2 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/Types/ZFillCallback.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + ZFillCallback + + + + + + + + + + + + + + + + + + + + + +

ZFillCallback

+ + +

Del.» type TZFillCallback = procedure (const E1Bot, E1Top, E2Bot, E2Top: TIntPoint; var Pt: TIntPoint);

+ +

C++ » typedef void (*ZFillCallback)(const IntPoint& e1bot, IntPoint& e1top, IntPoint& e2bot, IntPoint& e2top, IntPoint& pt);

+ +

C#  » public delegate void ZFillCallback(IntPoint bot1, IntPoint top1, IntPoint bot2, IntPoint top2, ref IntPoint pt);

+
+ +

If the use_xyz pre-processor directive is enabled, then the IntPoint class will have an extra 'Z' member and the Clipper class's ZFillFunction property will be exposed so it can be assigned a custom callback function.

This custom callback procedure requires five IntPoint parameters: the first 2 parameters are the vertices that define one line segment involved in the intersection and the next two parameters the other line segment. (Since the Clipper library has been developed in an environment that uses an inverted Y axis display, e1bot and e2bot will always have Y values greater than or equal to their corresponding e1top and e2top Y values.) The last IntPoint parameter contain the actual coordinates at the intersection. This last parameter is passed by reference so that its Z member can be assigned with a custom value.

+ + +

See Also

+

Clipper.ZFillFunction, Defines

+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/_Body.htm new file mode 100644 index 0000000..8fd8877 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/Units/ClipperLib/_Body.htm @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + ClipperLib + + + + + + + + + + + + + + + + + + + + +

ClipperLib

+ + +

Filenames: clipper.pas; clipper.hpp and clipper.cpp; clipper.cs

Namespace: ClipperLib

+ + +

Contents

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Types + Classes + Functions +
CInt + Clipper + Area +
ClipType + ClipperBase + CleanPolygon +
EndType + ClipperOffset + CleanPolygons +
InitOptions + PolyNode + ClosedPathsFromPolyTree +
IntPoint + PolyTree + MinkowskiDiff +
IntRect + + MinkowskiSum +
JoinType + + OffsetPaths +
Path + + OpenPathsFromPolyTree +
Paths + + Orientation +
PolyFillType + + PointInPolygon +
PolyType + + PolyTreeToPaths +
ZFillCallback + + ReversePath +
+ + ReversePaths +
+ + SimplifyPolygon +
+ + SimplifyPolygons +
+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Docs/_Body.htm b/upstream/clipper-6.4.2/Documentation/Docs/_Body.htm new file mode 100644 index 0000000..16c5668 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Docs/_Body.htm @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + Graphics32 Help + + + + + + + + + + + + + + + + + + + + + +

The Clipper Library - Version 6

+ + + + + + + + + + + +
Library Overview + Changes + Example + FAQ + Rounding + Deprecated + License +
+ + + + + +
+ +

Classes (Hierarchy)

+

 ClipperBase

+

 Clipper

+

 ClipperOffset

+

 PolyNode

+

 PolyTree

+
+

Types

+ + + + + + + + + + + + + + + + + + + +
CInt + InitOptions + JoinType + PolyFillType +
ClipType + IntPoint + Path + PolyType +
EndType + IntRect + Paths + ZFillCallback +
+

Functions

+ + + + + + + + + + + + + + + + + + + + + + +
Area + ClosedPathsFromPolyTree + OffsetPaths + PointInPolygon + ReversePaths +
CleanPolygon + MinkowskiDiff + OpenPathsFromPolyTree + PolyTreeToPaths + SimplifyPolygon +
CleanPolygons + MinkowskiSum + Orientation + ReversePath + SimplifyPolygons +
+

Units

+ + + + +
ClipperLib +
+ + + + + + \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Images/_BranchEmpty.gif b/upstream/clipper-6.4.2/Documentation/Images/_BranchEmpty.gif new file mode 100644 index 0000000..1069e82 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_BranchEmpty.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_BranchRight.gif b/upstream/clipper-6.4.2/Documentation/Images/_BranchRight.gif new file mode 100644 index 0000000..e5622df Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_BranchRight.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_BranchVert.gif b/upstream/clipper-6.4.2/Documentation/Images/_BranchVert.gif new file mode 100644 index 0000000..b922f33 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_BranchVert.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_BranchVertRight.gif b/upstream/clipper-6.4.2/Documentation/Images/_BranchVertRight.gif new file mode 100644 index 0000000..ae0f3d1 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_BranchVertRight.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_Class.gif b/upstream/clipper-6.4.2/Documentation/Images/_Class.gif new file mode 100644 index 0000000..5e90e34 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_Class.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_Home.gif b/upstream/clipper-6.4.2/Documentation/Images/_Home.gif new file mode 100644 index 0000000..7ca8ba2 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_Home.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_Project_Logo.gif b/upstream/clipper-6.4.2/Documentation/Images/_Project_Logo.gif new file mode 100644 index 0000000..7e3baac Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_Project_Logo.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_Unit.gif b/upstream/clipper-6.4.2/Documentation/Images/_Unit.gif new file mode 100644 index 0000000..ef56412 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_Unit.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/_buttons.gif b/upstream/clipper-6.4.2/Documentation/Images/_buttons.gif new file mode 100644 index 0000000..7daf3cd Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/_buttons.gif differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clean1.png b/upstream/clipper-6.4.2/Documentation/Images/clean1.png new file mode 100644 index 0000000..8caa45b Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clean1.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clean2.png b/upstream/clipper-6.4.2/Documentation/Images/clean2.png new file mode 100644 index 0000000..0a922fb Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clean2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding.png b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding.png new file mode 100644 index 0000000..3134f77 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding2.png b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding2.png new file mode 100644 index 0000000..624a6ce Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding3.png b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding3.png new file mode 100644 index 0000000..4db46f2 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding3.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding4.png b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding4.png new file mode 100644 index 0000000..6ae5ad5 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/clipper_rounding4.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/cliptype.png b/upstream/clipper-6.4.2/Documentation/Images/cliptype.png new file mode 100644 index 0000000..ce9ec4d Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/cliptype.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/common_edges.png b/upstream/clipper-6.4.2/Documentation/Images/common_edges.png new file mode 100644 index 0000000..e8dfb0e Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/common_edges.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/difference.png b/upstream/clipper-6.4.2/Documentation/Images/difference.png new file mode 100644 index 0000000..c2bce6c Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/difference.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/endtypes1.png b/upstream/clipper-6.4.2/Documentation/Images/endtypes1.png new file mode 100644 index 0000000..7c49a7b Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/endtypes1.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/endtypes2.png b/upstream/clipper-6.4.2/Documentation/Images/endtypes2.png new file mode 100644 index 0000000..15bed36 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/endtypes2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/evenodd.png b/upstream/clipper-6.4.2/Documentation/Images/evenodd.png new file mode 100644 index 0000000..d2069ee Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/evenodd.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/int.png b/upstream/clipper-6.4.2/Documentation/Images/int.png new file mode 100644 index 0000000..7df1cea Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/int.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/intersection.png b/upstream/clipper-6.4.2/Documentation/Images/intersection.png new file mode 100644 index 0000000..fd08dd0 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/intersection.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/jointypes.png b/upstream/clipper-6.4.2/Documentation/Images/jointypes.png new file mode 100644 index 0000000..c431517 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/jointypes.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/kangaroo_small.png b/upstream/clipper-6.4.2/Documentation/Images/kangaroo_small.png new file mode 100644 index 0000000..25b2df8 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/kangaroo_small.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/line_clipping.png b/upstream/clipper-6.4.2/Documentation/Images/line_clipping.png new file mode 100644 index 0000000..8e1ad25 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/line_clipping.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/line_clipping2.png b/upstream/clipper-6.4.2/Documentation/Images/line_clipping2.png new file mode 100644 index 0000000..244ac93 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/line_clipping2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/linesdemo.png b/upstream/clipper-6.4.2/Documentation/Images/linesdemo.png new file mode 100644 index 0000000..a24b09c Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/linesdemo.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/minkowski.png b/upstream/clipper-6.4.2/Documentation/Images/minkowski.png new file mode 100644 index 0000000..f725d5c Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/minkowski.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/minkowski2.png b/upstream/clipper-6.4.2/Documentation/Images/minkowski2.png new file mode 100644 index 0000000..de894cb Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/minkowski2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/miterlimit.png b/upstream/clipper-6.4.2/Documentation/Images/miterlimit.png new file mode 100644 index 0000000..a2b7262 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/miterlimit.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/negative.png b/upstream/clipper-6.4.2/Documentation/Images/negative.png new file mode 100644 index 0000000..0272c50 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/negative.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/nonzero.png b/upstream/clipper-6.4.2/Documentation/Images/nonzero.png new file mode 100644 index 0000000..5892a7c Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/nonzero.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/offset1.png b/upstream/clipper-6.4.2/Documentation/Images/offset1.png new file mode 100644 index 0000000..9034c67 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/offset1.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/orientation.png b/upstream/clipper-6.4.2/Documentation/Images/orientation.png new file mode 100644 index 0000000..be91023 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/orientation.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/polytree.png b/upstream/clipper-6.4.2/Documentation/Images/polytree.png new file mode 100644 index 0000000..6a2e9b1 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/polytree.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/positive.png b/upstream/clipper-6.4.2/Documentation/Images/positive.png new file mode 100644 index 0000000..b3ec4df Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/positive.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/sample1.png b/upstream/clipper-6.4.2/Documentation/Images/sample1.png new file mode 100644 index 0000000..7c77a04 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/sample1.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/simplify.png b/upstream/clipper-6.4.2/Documentation/Images/simplify.png new file mode 100644 index 0000000..30b0e76 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/simplify.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/simplify2.png b/upstream/clipper-6.4.2/Documentation/Images/simplify2.png new file mode 100644 index 0000000..aa9bd78 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/simplify2.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/simplify3.png b/upstream/clipper-6.4.2/Documentation/Images/simplify3.png new file mode 100644 index 0000000..8919e2b Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/simplify3.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/union.png b/upstream/clipper-6.4.2/Documentation/Images/union.png new file mode 100644 index 0000000..6f7a3a6 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/union.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/winding_number.png b/upstream/clipper-6.4.2/Documentation/Images/winding_number.png new file mode 100644 index 0000000..ea22d56 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/winding_number.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/wn.png b/upstream/clipper-6.4.2/Documentation/Images/wn.png new file mode 100644 index 0000000..0fb0984 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/wn.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/xor.png b/upstream/clipper-6.4.2/Documentation/Images/xor.png new file mode 100644 index 0000000..9468349 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/xor.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Images/zfill.png b/upstream/clipper-6.4.2/Documentation/Images/zfill.png new file mode 100644 index 0000000..99a2e49 Binary files /dev/null and b/upstream/clipper-6.4.2/Documentation/Images/zfill.png differ diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/_theme_template.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/_theme_template.scss new file mode 100644 index 0000000..53f4df5 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/_theme_template.scss @@ -0,0 +1,120 @@ +$background: white !default; + +$line_alt1_background: $background !default; +$line_alt2_background: $background !default; + +$line_highlighted_background: #e0e0e0 !default; +$line_highlighted_number: black !default; + +$gutter_text: #afafaf !default; +$gutter_border_color: #6ce26c !default; +$gutter_border: 3px solid $gutter_border_color !default; + +$toolbar_collapsed_a: #00f !default; +$toolbar_collapsed_a_hover: #f00 !default; +$toolbar_collapsed_background: #fff !default; +$toolbar_collapsed_border: 1px solid $gutter_border_color !default; + +$toolbar_a: #fff !default; +$toolbar_a_hover: #000 !default; +$toolbar_background: $gutter_border_color !default; +$toolbar_border: none !default; + +$code_plain: black !default; +$code_comments: #008200 !default; +$code_string: blue !default; +$code_keyword: #006699 !default; +$code_preprocessor: gray !default; +$code_variable: #aa7700 !default; +$code_value: #009900 !default; +$code_functions: #ff1493 !default; +$code_constants: #0066cc !default; +$code_script: $code_keyword !default; +$code_script_background: none !default; +$code_color1: gray !default; +$code_color2: #ff1493 !default; +$code_color3: red !default; + +$caption_color: $code_plain !default; + +// Interface elements. +.syntaxhighlighter { + background-color: $background !important; + + // Highlighed line number + .line { + &.alt1 { background-color: $line_alt1_background !important; } + &.alt2 { background-color: $line_alt2_background !important; } + + // Highlighed line + &.highlighted { + &.alt1, &.alt2 { background-color: $line_highlighted_background !important; } + &.number { color: $line_highlighted_number !important; } + } + } + + table { + caption { + color: $caption_color !important; + } + } + + // Add border to the lines + .gutter { + color: $gutter_text !important; + .line { + border-right: $gutter_border !important; + + &.highlighted { + background-color: $gutter_border_color !important; + color: $background !important; + } + } + } + + &.printing .line .content { border: none !important; } + + &.collapsed { + overflow: visible !important; + + .toolbar { + color: $toolbar_collapsed_a !important; + background: $toolbar_collapsed_background !important; + border: $toolbar_collapsed_border !important; + + a { + color: $toolbar_collapsed_a !important; + &:hover { color: $toolbar_collapsed_a_hover !important; } + } + } + } + + .toolbar { + color: $toolbar_a !important; + background: $toolbar_background !important; + border: $toolbar_border !important; + a { + color: $toolbar_a !important; + &:hover { color: $toolbar_a_hover !important; } + } + } + + // Actual syntax highlighter colors. + .plain, .plain a { color: $code_plain !important; } + .comments, .comments a { color: $code_comments !important; } + .string, .string a { color: $code_string !important; } + .keyword { color: $code_keyword !important; } + .preprocessor { color: $code_preprocessor !important; } + .variable { color: $code_variable !important; } + .value { color: $code_value !important; } + .functions { color: $code_functions !important; } + .constants { color: $code_constants !important; } + .script { + font-weight: bold !important; + color: $code_script !important; + background-color: $code_script_background !important; + } + .color1, .color1 a { color: $code_color1 !important; } + .color2, .color2 a { color: $code_color2 !important; } + .color3, .color3 a { color: $code_color3 !important; } +} diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/config.rb b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/config.rb new file mode 100644 index 0000000..6f82de1 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/config.rb @@ -0,0 +1,14 @@ +environment = :production +project_type = :stand_alone +http_path = "/" +css_dir = "../styles" +sass_dir = "." +images_dir = "images" +sass_options = { + :line_numbers => false, + :debug_info => false +} + +# output_style = :compressed +# output_style = :compact +output_style = :expanded diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCore.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCore.scss new file mode 100644 index 0000000..a67e4f9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCore.scss @@ -0,0 +1,216 @@ +@mixin round_corners_custom($top, $right, $bottom, $left) { + -moz-border-radius: $top $right $bottom $left !important; + -webkit-border-radius: $top $right $bottom $left !important; +} + +@mixin round_corners($radius) { + @include round_corners_custom($radius, $radius, $radius, $radius); +} + +.syntaxhighlighter { + a, + div, + code, + table, + table td, + table tr, + table tbody, + table thead, + table caption, + textarea { + @include round_corners(0); + + background: none !important; + border: 0 !important; + bottom: auto !important; + float: none !important; + height: auto !important; + left: auto !important; + line-height: 1.1em !important; + margin: 0 !important; + outline: 0 !important; + overflow: visible !important; + padding: 0 !important; + position: static !important; + right: auto !important; + text-align: left !important; + top: auto !important; + vertical-align: baseline !important; + width: auto !important; + box-sizing: content-box !important; + font: { + family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; + weight: normal !important; + style: normal !important; + size: 1em !important; + } + min: { + // For IE8, FF & WebKit + height: inherit !important; + // For IE7 + height: auto !important; + } + } +} + +.syntaxhighlighter { + width: 100% !important; + margin: 1em 0 1em 0 !important; + + position: relative !important; + overflow: auto !important; + font-size: 1em !important; + + &.source { overflow: hidden !important; } + + // set up bold and italic + .bold { font-weight: bold !important; } + .italic { font-style: italic !important; } + + .line { white-space: pre !important; } + + // main table and columns + table { + width: 100% !important; + caption { + text-align: left !important; + padding: .5em 0 0.5em 1em !important; + } + + td.code { + width: 100% !important; + + .container { + position: relative !important; + + textarea { + box-sizing: border-box !important; + position: absolute !important; + left: 0 !important; + top: 0 !important; + width: 100% !important; + height: 100% !important; + border: none !important; + background: white !important; + padding-left: 1em !important; + overflow: hidden !important; + white-space: pre !important; + } + } + } + + // middle spacing between line numbers and lines + td.gutter .line { + text-align: right !important; + padding: 0 0.5em 0 1em !important; + } + + td.code .line { + padding: 0 1em !important; + } + } + + &.nogutter { + td.code { + .container textarea, .line { padding-left: 0em !important; } + } + } + + &.show { display: block !important; } + + // Adjust some properties when collapsed + &.collapsed { + table { display: none !important; } + + .toolbar { + padding: 0.1em 0.8em 0em 0.8em !important; + font-size: 1em !important; + position: static !important; + width: auto !important; + height: auto !important; + + span { + display: inline !important; + margin-right: 1em !important; + + a { + padding: 0 !important; + display: none !important; + &.expandSource { display: inline !important; } + } + } + } + } + + // Styles for the toolbar + .toolbar { + position: absolute !important; + right: 1px !important; + top: 1px !important; + width: 11px !important; + height: 11px !important; + font-size: 10px !important; + z-index: 10 !important; + + span.title { display: inline !important; } + + a { + display: block !important; + text-align: center !important; + text-decoration: none !important; + padding-top: 1px !important; + + &.expandSource { display: none !important; } + } + } + + &.ie { + font-size: .9em !important; + padding: 1px 0 1px 0 !important; + + .toolbar { + line-height: 8px !important; + a { + padding-top: 0px !important; + } + } + } + + // Print view. + // Colors are based on the default theme without background. + &.printing { + .line.alt1 .content, + .line.alt2 .content, + .line.highlighted .number, + .line.highlighted.alt1 .content, + .line.highlighted.alt2 .content { background: none !important; } + + // Gutter line numbers + .line { + .number { color: #bbbbbb !important; } + // Add border to the lines + .content { color: black !important; } + } + + // Toolbar when visible + .toolbar { display: none !important; } + a { text-decoration: none !important; } + .plain, .plain a { color: black !important; } + .comments, .comments a { color: #008200 !important; } + .string, .string a { color: blue !important; } + .keyword { + color: #006699 !important; + font-weight: bold !important; + } + .preprocessor { color: gray !important; } + .variable { color: #aa7700 !important; } + .value { color: #009900 !important; } + .functions { color: #ff1493 !important; } + .constants { color: #0066cc !important; } + .script { font-weight: bold !important; } + .color1, .color1 a { color: gray !important; } + .color2, .color2 a { color: #ff1493 !important; } + .color3, .color3 a { color: red !important; } + .break, .break a { color: black !important; } + } +} \ No newline at end of file diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDefault.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDefault.scss new file mode 100644 index 0000000..ff80c7f --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDefault.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeDefault.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDjango.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDjango.scss new file mode 100644 index 0000000..ef572e9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreDjango.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeDjango.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEclipse.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEclipse.scss new file mode 100644 index 0000000..9767f53 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEclipse.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeEclipse.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEmacs.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEmacs.scss new file mode 100644 index 0000000..5e466f3 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreEmacs.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeEmacs.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreFadeToGrey.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreFadeToGrey.scss new file mode 100644 index 0000000..4628595 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreFadeToGrey.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeFadeToGrey.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMDUltra.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMDUltra.scss new file mode 100644 index 0000000..10ad4c5 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMDUltra.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeMDUltra.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMidnight.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMidnight.scss new file mode 100644 index 0000000..e357eb2 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreMidnight.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeMidnight.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreRDark.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreRDark.scss new file mode 100644 index 0000000..5c26da3 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shCoreRDark.scss @@ -0,0 +1,2 @@ +@import "shCore.scss"; +@import "shThemeRDark.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDefault.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDefault.scss new file mode 100644 index 0000000..1574dae --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDefault.scss @@ -0,0 +1,7 @@ +// Default Syntax Highlighter theme. + +@import "_theme_template.scss"; + +.syntaxhighlighter { + .keyword { font-weight: bold !important; } +} diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDjango.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDjango.scss new file mode 100644 index 0000000..8e95c56 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeDjango.scss @@ -0,0 +1,36 @@ +// Django SyntaxHighlighter theme + +$background: #0a2b1d !default; + +$line_highlighted_background: #233729 !default; +$line_highlighted_number: white !default; + +$gutter_text: #497958 !default; +$gutter_border_color: #41a83e !default; + +$toolbar_collapsed_a: #96dd3b !default; +$toolbar_collapsed_a_hover: #fff !default; +$toolbar_collapsed_background: #000 !default; + +$toolbar_a: #fff !default; +$toolbar_a_hover: #ffe862 !default; + +$code_plain: #f8f8f8 !default; +$code_comments: #336442 !default; +$code_string: #9df39f !default; +$code_keyword: #96dd3b !default; +$code_preprocessor: #91bb9e !default; +$code_variable: #ffaa3e !default; +$code_value: #f7e741 !default; +$code_functions: #ffaa3e !default; +$code_constants: #e0e8ff !default; +$code_color1: #eb939a !default; +$code_color2: #91bb9e !default; +$code_color3: #edef7d !default; + +@import "_theme_template.scss"; + +.syntaxhighlighter { + .comments { font-style: italic !important; } + .keyword { font-weight: bold !important; } +} diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEclipse.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEclipse.scss new file mode 100644 index 0000000..193fb1d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEclipse.scss @@ -0,0 +1,48 @@ +// Eclipse IDE SyntaxHighlighter color theme +// (C) Code-House +// :http//blog.code-house.org/2009/10/xml-i-adnotacje-kod-ogolnego-przeznaczenia-i-jpa/ + +$background: #fff !default; + +$line_highlighted_background: #c3defe !default; +$line_highlighted_number: #fff !default; + +$gutter_text: #787878 !default; +$gutter_border_color: #d4d0c8 !default; + +$toolbar_collapsed_a: #3f5fbf !default; +$toolbar_collapsed_a_hover: #aa7700 !default; +$toolbar_collapsed_background: #fff !default; + +$toolbar_a: #a0a0a0 !default; +$toolbar_a_hover: red !default; + +$code_plain: black !default; +$code_comments: #3f5fbf !default; +$code_string: #2a00ff !default; +$code_keyword: #7f0055 !default; +$code_preprocessor: #646464 !default; +$code_variable: #aa7700 !default; +$code_value: #009900 !default; +$code_functions: #ff1493 !default; +$code_constants: #0066cc !default; +$code_color1: gray !default; +$code_color2: #ff1493 !default; +$code_color3: red !default; + +@import "_theme_template.scss"; + +.syntaxhighlighter { + .keyword { font-weight: bold !important; } + + .xml { + .keyword { + color: #3f7f7f !important; + font-weight: normal !important; } + .color1, .color1 a { color: #7f007f !important; } + .string { + font-style: italic !important; + color: #2a00ff !important; + } + } +} diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEmacs.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEmacs.scss new file mode 100644 index 0000000..11c9deb --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeEmacs.scss @@ -0,0 +1,32 @@ +// Emacs SyntaxHighlighter theme based on theme by Joshua Emmons +// http://www.skia.net/ + +$background: black !default; + +$line_highlighted_background: #2A3133 !default; +$line_highlighted_number: white !default; + +$gutter_text: #d3d3d3 !default; +$gutter_border_color: #990000 !default; + +$toolbar_collapsed_a: #ebdb8d !default; +$toolbar_collapsed_a_hover: #ff7d27 !default; +$toolbar_collapsed_background: black !default; + +$toolbar_a: #fff !default; +$toolbar_a_hover: #9ccff4 !default; + +$code_plain: #d3d3d3 !default; +$code_comments: #ff7d27 !default; +$code_string: #ff9e7b !default; +$code_keyword: aqua !default; +$code_preprocessor: #aec4de !default; +$code_variable: #ffaa3e !default; +$code_value: #009900 !default; +$code_functions: #81cef9 !default; +$code_constants: #ff9e7b !default; +$code_color1: #ebdb8d !default; +$code_color2: #ff7d27 !default; +$code_color3: #aec4de !default; + +@import "_theme_template.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeFadeToGrey.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeFadeToGrey.scss new file mode 100644 index 0000000..7963814 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeFadeToGrey.scss @@ -0,0 +1,36 @@ +// Fade to Grey SyntaxHighlighter theme based on theme by Brasten Sager +// :http//www.ibrasten.com/ + +$background: #121212 !default; + +$line_highlighted_background: #2C2C29 !default; +$line_highlighted_number: white !default; + +$gutter_text: #afafaf !default; +$gutter_border_color: #3185b9 !default; + +$toolbar_collapsed_a: #3185b9 !default; +$toolbar_collapsed_a_hover: #d01d33 !default; +$toolbar_collapsed_background: black !default; + +$toolbar_a: #fff !default; +$toolbar_a_hover: #96daff !default; + +$code_plain: white !default; +$code_comments: #696854 !default; +$code_string: #e3e658 !default; +$code_keyword: #d01d33 !default; +$code_preprocessor: #435a5f !default; +$code_variable: #898989 !default; +$code_value: #009900 !default; +$code_functions: #aaaaaa !default; +$code_constants: #96daff !default; +$code_color1: #ffc074 !default; +$code_color2: #4a8cdb !default; +$code_color3: #96daff !default; + +@import "_theme_template.scss"; + +.syntaxhighlighter { + .functions { font-weight: bold !important; } +} diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMDUltra.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMDUltra.scss new file mode 100644 index 0000000..0356fa6 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMDUltra.scss @@ -0,0 +1,32 @@ +// MDUltra SyntaxHighlighter theme based on Midnight Theme +// http://www.mddev.co.uk/ + +$background: #222222 !default; + +$line_highlighted_background: #253e5a !default; +$line_highlighted_number: white !default; + +$gutter_text: #38566f !default; +$gutter_border_color: #435a5f !default; + +$toolbar_collapsed_a: #428bdd !default; +$toolbar_collapsed_a_hover: lime !default; +$toolbar_collapsed_background: black !default; + +$toolbar_a: #aaaaff !default; +$toolbar_a_hover: #9ccff4 !default; + +$code_plain: lime !default; +$code_comments: #428bdd !default; +$code_string: lime !default; +$code_keyword: #aaaaff !default; +$code_preprocessor: #8aa6c1 !default; +$code_variable: aqua !default; +$code_value: #f7e741 !default; +$code_functions: #ff8000 !default; +$code_constants: yellow !default; +$code_color1: red !default; +$code_color2: yellow !default; +$code_color3: #ffaa3e !default; + +@import "_theme_template.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMidnight.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMidnight.scss new file mode 100644 index 0000000..a4dae02 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeMidnight.scss @@ -0,0 +1,32 @@ +// Midnight SyntaxHighlighter theme based on theme by J.D. Myers +// http://webdesign.lsnjd.com/ + +$background: #0f192a !default; + +$line_highlighted_background: #253e5a !default; +$line_highlighted_number: #38566f !default; + +$gutter_text: #afafaf !default; +$gutter_border_color: #435a5f !default; + +$toolbar_collapsed_a: #428bdd !default; +$toolbar_collapsed_a_hover: #1dc116 !default; +$toolbar_collapsed_background: #000 !default; + +$toolbar_a: #D1EDFF !default; +$toolbar_a_hover: #8aa6c1 !default; + +$code_plain: #d1edff !default; +$code_comments: #428bdd !default; +$code_string: #1dc116 !default; +$code_keyword: #b43d3d !default; +$code_preprocessor: #8aa6c1 !default; +$code_variable: #ffaa3e !default; +$code_value: #f7e741 !default; +$code_functions: #ffaa3e !default; +$code_constants: #e0e8ff !default; +$code_color1: #f8bb00 !default; +$code_color2: white !default; +$code_color3: #ffaa3e !default; + +@import "_theme_template.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeRDark.scss b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeRDark.scss new file mode 100644 index 0000000..3b67b15 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/compass/shThemeRDark.scss @@ -0,0 +1,32 @@ +// RDark SyntaxHighlighter theme based on theme by Radu Dineiu +// http://www.vim.org/scripts/script.php?script_id=1732 + +$background: #1b2426 !default; + +$line_highlighted_background: #323E41 !default; +$line_highlighted_number: #b9bdb6 !default; + +$gutter_text: #afafaf !default; +$gutter_border_color: #435a5f !default; + +$toolbar_collapsed_a: #5ba1cf !default; +$toolbar_collapsed_a_hover: #5ce638 !default; +$toolbar_collapsed_background: #000 !default; + +$toolbar_a: #fff !default; +$toolbar_a_hover: #e0e8ff !default; + +$code_plain: #b9bdb6 !default; +$code_comments: #878a85 !default; +$code_string: #5ce638 !default; +$code_keyword: #5ba1cf !default; +$code_preprocessor: #435a5f !default; +$code_variable: #ffaa3e !default; +$code_value: #009900 !default; +$code_functions: #ffaa3e !default; +$code_constants: #e0e8ff !default; +$code_color1: #e0e8ff !default; +$code_color2: white !default; +$code_color3: #ffaa3e !default; + +@import "_theme_template.scss"; diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/index.html b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/index.html new file mode 100644 index 0000000..60908f4 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/index.html @@ -0,0 +1,22 @@ + + + + + Hello SyntaxHighlighter + + + + + + + + +

Hello SyntaxHighlighter

+
+function helloSyntaxHighlighter()
+{
+	return "hi!";
+}
+
+ + diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shAutoloader.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shAutoloader.js new file mode 100644 index 0000000..4e29bdd --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shAutoloader.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(2(){1 h=5;h.I=2(){2 n(c,a){4(1 d=0;d|<|≥|>=|≤|<=|\*|\+|-|\/|÷|\^)/g, + css: 'color2' }, + + { regex: /\b(?:and|as|div|mod|not|or|return(?!\s&)(ing)?|equals|(is(n't| not)? )?equal( to)?|does(n't| not) equal|(is(n't| not)? )?(greater|less) than( or equal( to)?)?|(comes|does(n't| not) come) (after|before)|is(n't| not)?( in)? (back|front) of|is(n't| not)? behind|is(n't| not)?( (in|contained by))?|does(n't| not) contain|contain(s)?|(start|begin|end)(s)? with|((but|end) )?(consider|ignor)ing|prop(erty)?|(a )?ref(erence)?( to)?|repeat (until|while|with)|((end|exit) )?repeat|((else|end) )?if|else|(end )?(script|tell|try)|(on )?error|(put )?into|(of )?(it|me)|its|my|with (timeout( of)?|transaction)|end (timeout|transaction))\b/g, + css: 'keyword' }, + + { regex: /\b\d+(st|nd|rd|th)\b/g, // ordinals + css: 'keyword' }, + + { regex: /\b(?:about|above|against|around|at|below|beneath|beside|between|by|(apart|aside) from|(instead|out) of|into|on(to)?|over|since|thr(ough|u)|under)\b/g, + css: 'color3' }, + + { regex: /\b(?:adding folder items to|after receiving|choose( ((remote )?application|color|folder|from list|URL))?|clipboard info|set the clipboard to|(the )?clipboard|entire contents|display(ing| (alert|dialog|mode))?|document( (edited|file|nib name))?|file( (name|type))?|(info )?for|giving up after|(name )?extension|quoted form|return(ed)?|second(?! item)(s)?|list (disks|folder)|text item(s| delimiters)?|(Unicode )?text|(disk )?item(s)?|((current|list) )?view|((container|key) )?window|with (data|icon( (caution|note|stop))?|parameter(s)?|prompt|properties|seed|title)|case|diacriticals|hyphens|numeric strings|punctuation|white space|folder creation|application(s( folder)?| (processes|scripts position|support))?|((desktop )?(pictures )?|(documents|downloads|favorites|home|keychain|library|movies|music|public|scripts|sites|system|users|utilities|workflows) )folder|desktop|Folder Action scripts|font(s| panel)?|help|internet plugins|modem scripts|(system )?preferences|printer descriptions|scripting (additions|components)|shared (documents|libraries)|startup (disk|items)|temporary items|trash|on server|in AppleTalk zone|((as|long|short) )?user name|user (ID|locale)|(with )?password|in (bundle( with identifier)?|directory)|(close|open for) access|read|write( permission)?|(g|s)et eof|using( delimiters)?|starting at|default (answer|button|color|country code|entr(y|ies)|identifiers|items|name|location|script editor)|hidden( answer)?|open(ed| (location|untitled))?|error (handling|reporting)|(do( shell)?|load|run|store) script|administrator privileges|altering line endings|get volume settings|(alert|boot|input|mount|output|set) volume|output muted|(fax|random )?number|round(ing)?|up|down|toward zero|to nearest|as taught in school|system (attribute|info)|((AppleScript( Studio)?|system) )?version|(home )?directory|(IPv4|primary Ethernet) address|CPU (type|speed)|physical memory|time (stamp|to GMT)|replacing|ASCII (character|number)|localized string|from table|offset|summarize|beep|delay|say|(empty|multiple) selections allowed|(of|preferred) type|invisibles|showing( package contents)?|editable URL|(File|FTP|News|Media|Web) [Ss]ervers|Telnet hosts|Directory services|Remote applications|waiting until completion|saving( (in|to))?|path (for|to( (((current|frontmost) )?application|resource))?)|POSIX (file|path)|(background|RGB) color|(OK|cancel) button name|cancel button|button(s)?|cubic ((centi)?met(re|er)s|yards|feet|inches)|square ((kilo)?met(re|er)s|miles|yards|feet)|(centi|kilo)?met(re|er)s|miles|yards|feet|inches|lit(re|er)s|gallons|quarts|(kilo)?grams|ounces|pounds|degrees (Celsius|Fahrenheit|Kelvin)|print( (dialog|settings))?|clos(e(able)?|ing)|(de)?miniaturized|miniaturizable|zoom(ed|able)|attribute run|action (method|property|title)|phone|email|((start|end)ing|home) page|((birth|creation|current|custom|modification) )?date|((((phonetic )?(first|last|middle))|computer|host|maiden|related) |nick)?name|aim|icq|jabber|msn|yahoo|address(es)?|save addressbook|should enable action|city|country( code)?|formatte(r|d address)|(palette )?label|state|street|zip|AIM [Hh]andle(s)?|my card|select(ion| all)?|unsaved|(alpha )?value|entr(y|ies)|group|(ICQ|Jabber|MSN) handle|person|people|company|department|icon image|job title|note|organization|suffix|vcard|url|copies|collating|pages (across|down)|request print time|target( printer)?|((GUI Scripting|Script menu) )?enabled|show Computer scripts|(de)?activated|awake from nib|became (key|main)|call method|of (class|object)|center|clicked toolbar item|closed|for document|exposed|(can )?hide|idle|keyboard (down|up)|event( (number|type))?|launch(ed)?|load (image|movie|nib|sound)|owner|log|mouse (down|dragged|entered|exited|moved|up)|move|column|localization|resource|script|register|drag (info|types)|resigned (active|key|main)|resiz(e(d)?|able)|right mouse (down|dragged|up)|scroll wheel|(at )?index|should (close|open( untitled)?|quit( after last window closed)?|zoom)|((proposed|screen) )?bounds|show(n)?|behind|in front of|size (mode|to fit)|update(d| toolbar item)?|was (hidden|miniaturized)|will (become active|close|finish launching|hide|miniaturize|move|open|quit|(resign )?active|((maximum|minimum|proposed) )?size|show|zoom)|bundle|data source|movie|pasteboard|sound|tool(bar| tip)|(color|open|save) panel|coordinate system|frontmost|main( (bundle|menu|window))?|((services|(excluded from )?windows) )?menu|((executable|frameworks|resource|scripts|shared (frameworks|support)) )?path|(selected item )?identifier|data|content(s| view)?|character(s)?|click count|(command|control|option|shift) key down|context|delta (x|y|z)|key( code)?|location|pressure|unmodified characters|types|(first )?responder|playing|(allowed|selectable) identifiers|allows customization|(auto saves )?configuration|visible|image( name)?|menu form representation|tag|user(-| )defaults|associated file name|(auto|needs) display|current field editor|floating|has (resize indicator|shadow)|hides when deactivated|level|minimized (image|title)|opaque|position|release when closed|sheet|title(d)?)\b/g, + css: 'color3' }, + + { regex: new RegExp(this.getKeywords(specials), 'gm'), css: 'color3' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(ordinals), 'gm'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['applescript']; + + SyntaxHighlighter.brushes.AppleScript = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushBash.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushBash.js new file mode 100644 index 0000000..8c29696 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushBash.js @@ -0,0 +1,59 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'if fi then elif else for do done until while break continue case function return in eq ne ge le'; + var commands = 'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' + + 'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' + + 'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' + + 'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' + + 'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' + + 'import install join kill less let ln local locate logname logout look lpc lpr lprint ' + + 'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' + + 'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' + + 'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' + + 'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' + + 'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' + + 'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' + + 'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' + + 'vi watch wc whereis which who whoami Wget xargs yes' + ; + + this.regexList = [ + { regex: /^#!.*$/gm, css: 'preprocessor bold' }, + { regex: /\/[\w-\/]+/gm, css: 'plain' }, + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(this.getKeywords(commands), 'gm'), css: 'functions' } // commands + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['bash', 'shell']; + + SyntaxHighlighter.brushes.Bash = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCSharp.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCSharp.js new file mode 100644 index 0000000..50a8ebe --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCSharp.js @@ -0,0 +1,70 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abstract as base bool break byte case catch char checked class const ' + + 'continue decimal default delegate do double else enum event explicit ' + + 'extern false finally fixed float for foreach get goto if implicit in int ' + + 'interface internal is lock long namespace new null object operator out ' + + 'override params private protected public readonly ref return sbyte sealed set ' + + 'short sizeof stackalloc static string struct switch this throw true try ' + + 'typeof uint ulong unchecked unsafe ushort using virtual void while'; + + var clipper = 'Clipper Path Paths IntPoint List PolyType PolyFillType ClipType IntRect'; + + function fixComments(match, regexInfo) + { + var css = (match[0].indexOf("///") == 0) + ? 'color1' + : 'comments' + ; + + return [new SyntaxHighlighter.Match(match[0], match.index, css)]; + } + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, func : fixComments }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: /@"(?:[^"]|"")*"/g, css: 'string' }, // @-quoted strings + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /^\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // c# keyword + + { regex: new RegExp(this.getKeywords(clipper), 'gm'), css: 'color4'}, + + { regex: /\bpartial(?=\s+(?:class|interface|struct)\b)/g, css: 'keyword' }, // contextual keyword: 'partial' + { regex: /\byield(?=\s+(?:return|break)\b)/g, css: 'keyword' } // contextual keyword: 'yield' + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['c#', 'c-sharp', 'csharp']; + + SyntaxHighlighter.brushes.CSharp = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); + diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushColdFusion.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushColdFusion.js new file mode 100644 index 0000000..627dbb9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushColdFusion.js @@ -0,0 +1,100 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Jen + // http://www.jensbits.com/2009/05/14/coldfusion-brush-for-syntaxhighlighter-plus + + var funcs = 'Abs ACos AddSOAPRequestHeader AddSOAPResponseHeader AjaxLink AjaxOnLoad ArrayAppend ArrayAvg ArrayClear ArrayDeleteAt ' + + 'ArrayInsertAt ArrayIsDefined ArrayIsEmpty ArrayLen ArrayMax ArrayMin ArraySet ArraySort ArraySum ArraySwap ArrayToList ' + + 'Asc ASin Atn BinaryDecode BinaryEncode BitAnd BitMaskClear BitMaskRead BitMaskSet BitNot BitOr BitSHLN BitSHRN BitXor ' + + 'Ceiling CharsetDecode CharsetEncode Chr CJustify Compare CompareNoCase Cos CreateDate CreateDateTime CreateObject ' + + 'CreateODBCDate CreateODBCDateTime CreateODBCTime CreateTime CreateTimeSpan CreateUUID DateAdd DateCompare DateConvert ' + + 'DateDiff DateFormat DatePart Day DayOfWeek DayOfWeekAsString DayOfYear DaysInMonth DaysInYear DE DecimalFormat DecrementValue ' + + 'Decrypt DecryptBinary DeleteClientVariable DeserializeJSON DirectoryExists DollarFormat DotNetToCFType Duplicate Encrypt ' + + 'EncryptBinary Evaluate Exp ExpandPath FileClose FileCopy FileDelete FileExists FileIsEOF FileMove FileOpen FileRead ' + + 'FileReadBinary FileReadLine FileSetAccessMode FileSetAttribute FileSetLastModified FileWrite Find FindNoCase FindOneOf ' + + 'FirstDayOfMonth Fix FormatBaseN GenerateSecretKey GetAuthUser GetBaseTagData GetBaseTagList GetBaseTemplatePath ' + + 'GetClientVariablesList GetComponentMetaData GetContextRoot GetCurrentTemplatePath GetDirectoryFromPath GetEncoding ' + + 'GetException GetFileFromPath GetFileInfo GetFunctionList GetGatewayHelper GetHttpRequestData GetHttpTimeString ' + + 'GetK2ServerDocCount GetK2ServerDocCountLimit GetLocale GetLocaleDisplayName GetLocalHostIP GetMetaData GetMetricData ' + + 'GetPageContext GetPrinterInfo GetProfileSections GetProfileString GetReadableImageFormats GetSOAPRequest GetSOAPRequestHeader ' + + 'GetSOAPResponse GetSOAPResponseHeader GetTempDirectory GetTempFile GetTemplatePath GetTickCount GetTimeZoneInfo GetToken ' + + 'GetUserRoles GetWriteableImageFormats Hash Hour HTMLCodeFormat HTMLEditFormat IIf ImageAddBorder ImageBlur ImageClearRect ' + + 'ImageCopy ImageCrop ImageDrawArc ImageDrawBeveledRect ImageDrawCubicCurve ImageDrawLine ImageDrawLines ImageDrawOval ' + + 'ImageDrawPoint ImageDrawQuadraticCurve ImageDrawRect ImageDrawRoundRect ImageDrawText ImageFlip ImageGetBlob ImageGetBufferedImage ' + + 'ImageGetEXIFTag ImageGetHeight ImageGetIPTCTag ImageGetWidth ImageGrayscale ImageInfo ImageNegative ImageNew ImageOverlay ImagePaste ' + + 'ImageRead ImageReadBase64 ImageResize ImageRotate ImageRotateDrawingAxis ImageScaleToFit ImageSetAntialiasing ImageSetBackgroundColor ' + + 'ImageSetDrawingColor ImageSetDrawingStroke ImageSetDrawingTransparency ImageSharpen ImageShear ImageShearDrawingAxis ImageTranslate ' + + 'ImageTranslateDrawingAxis ImageWrite ImageWriteBase64 ImageXORDrawingMode IncrementValue InputBaseN Insert Int IsArray IsBinary ' + + 'IsBoolean IsCustomFunction IsDate IsDDX IsDebugMode IsDefined IsImage IsImageFile IsInstanceOf IsJSON IsLeapYear IsLocalHost ' + + 'IsNumeric IsNumericDate IsObject IsPDFFile IsPDFObject IsQuery IsSimpleValue IsSOAPRequest IsStruct IsUserInAnyRole IsUserInRole ' + + 'IsUserLoggedIn IsValid IsWDDX IsXML IsXmlAttribute IsXmlDoc IsXmlElem IsXmlNode IsXmlRoot JavaCast JSStringFormat LCase Left Len ' + + 'ListAppend ListChangeDelims ListContains ListContainsNoCase ListDeleteAt ListFind ListFindNoCase ListFirst ListGetAt ListInsertAt ' + + 'ListLast ListLen ListPrepend ListQualify ListRest ListSetAt ListSort ListToArray ListValueCount ListValueCountNoCase LJustify Log ' + + 'Log10 LSCurrencyFormat LSDateFormat LSEuroCurrencyFormat LSIsCurrency LSIsDate LSIsNumeric LSNumberFormat LSParseCurrency LSParseDateTime ' + + 'LSParseEuroCurrency LSParseNumber LSTimeFormat LTrim Max Mid Min Minute Month MonthAsString Now NumberFormat ParagraphFormat ParseDateTime ' + + 'Pi PrecisionEvaluate PreserveSingleQuotes Quarter QueryAddColumn QueryAddRow QueryConvertForGrid QueryNew QuerySetCell QuotedValueList Rand ' + + 'Randomize RandRange REFind REFindNoCase ReleaseComObject REMatch REMatchNoCase RemoveChars RepeatString Replace ReplaceList ReplaceNoCase ' + + 'REReplace REReplaceNoCase Reverse Right RJustify Round RTrim Second SendGatewayMessage SerializeJSON SetEncoding SetLocale SetProfileString ' + + 'SetVariable Sgn Sin Sleep SpanExcluding SpanIncluding Sqr StripCR StructAppend StructClear StructCopy StructCount StructDelete StructFind ' + + 'StructFindKey StructFindValue StructGet StructInsert StructIsEmpty StructKeyArray StructKeyExists StructKeyList StructKeyList StructNew ' + + 'StructSort StructUpdate Tan TimeFormat ToBase64 ToBinary ToScript ToString Trim UCase URLDecode URLEncodedFormat URLSessionFormat Val ' + + 'ValueList VerifyClient Week Wrap Wrap WriteOutput XmlChildPos XmlElemNew XmlFormat XmlGetNodeType XmlNew XmlParse XmlSearch XmlTransform ' + + 'XmlValidate Year YesNoFormat'; + + var keywords = 'cfabort cfajaximport cfajaxproxy cfapplet cfapplication cfargument cfassociate cfbreak cfcache cfcalendar ' + + 'cfcase cfcatch cfchart cfchartdata cfchartseries cfcol cfcollection cfcomponent cfcontent cfcookie cfdbinfo ' + + 'cfdefaultcase cfdirectory cfdiv cfdocument cfdocumentitem cfdocumentsection cfdump cfelse cfelseif cferror ' + + 'cfexchangecalendar cfexchangeconnection cfexchangecontact cfexchangefilter cfexchangemail cfexchangetask ' + + 'cfexecute cfexit cffeed cffile cfflush cfform cfformgroup cfformitem cfftp cffunction cfgrid cfgridcolumn ' + + 'cfgridrow cfgridupdate cfheader cfhtmlhead cfhttp cfhttpparam cfif cfimage cfimport cfinclude cfindex ' + + 'cfinput cfinsert cfinterface cfinvoke cfinvokeargument cflayout cflayoutarea cfldap cflocation cflock cflog ' + + 'cflogin cfloginuser cflogout cfloop cfmail cfmailparam cfmailpart cfmenu cfmenuitem cfmodule cfNTauthenticate ' + + 'cfobject cfobjectcache cfoutput cfparam cfpdf cfpdfform cfpdfformparam cfpdfparam cfpdfsubform cfpod cfpop ' + + 'cfpresentation cfpresentationslide cfpresenter cfprint cfprocessingdirective cfprocparam cfprocresult ' + + 'cfproperty cfquery cfqueryparam cfregistry cfreport cfreportparam cfrethrow cfreturn cfsavecontent cfschedule ' + + 'cfscript cfsearch cfselect cfset cfsetting cfsilent cfslider cfsprydataset cfstoredproc cfswitch cftable ' + + 'cftextarea cfthread cfthrow cftimer cftooltip cftrace cftransaction cftree cftreeitem cftry cfupdate cfwddx ' + + 'cfwindow cfxml cfzip cfzipparam'; + + var operators = 'all and any between cross in join like not null or outer some'; + + this.regexList = [ + { regex: new RegExp('--(.*)$', 'gm'), css: 'comments' }, // one line and multiline comments + { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // single quoted strings + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // functions + { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['coldfusion','cf']; + + SyntaxHighlighter.brushes.ColdFusion = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCpp.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCpp.js new file mode 100644 index 0000000..9f70d3a --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCpp.js @@ -0,0 +1,97 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Copyright 2006 Shin, YoungJin + + var datatypes = 'ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR ' + + 'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH ' + + 'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP ' + + 'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY ' + + 'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT ' + + 'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE ' + + 'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF ' + + 'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR ' + + 'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR ' + + 'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT ' + + 'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 ' + + 'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR ' + + 'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 ' + + 'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT ' + + 'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG ' + + 'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM ' + + 'char bool short int __int32 __int64 __int8 __int16 long float double __wchar_t ' + + 'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS ' + + 'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t ' + + '__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t ' + + 'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler ' + + 'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function ' + + 'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf ' + + 'va_list wchar_t wctrans_t wctype_t wint_t signed'; + + var keywords = 'break case catch class const __finally __exception __try ' + + 'const_cast continue private public protected __declspec ' + + 'default delete deprecated dllexport dllimport do dynamic_cast ' + + 'else enum explicit extern if for friend goto inline ' + + 'mutable naked namespace new noinline noreturn nothrow ' + + 'register reinterpret_cast return selectany ' + + 'sizeof static static_cast struct switch template this ' + + 'thread throw true false try typedef typeid typename union ' + + 'using uuid virtual void volatile whcar_t while'; + + var functions = 'assert isalnum isalpha iscntrl isdigit isgraph islower isprint' + + 'ispunct isspace isupper isxdigit tolower toupper errno localeconv ' + + 'setlocale acos asin atan atan2 ceil cos cosh exp fabs floor fmod ' + + 'frexp ldexp log log10 modf pow sin sinh sqrt tan tanh jmp_buf ' + + 'longjmp setjmp raise signal sig_atomic_t va_arg va_end va_start ' + + 'clearerr fclose feof ferror fflush fgetc fgetpos fgets fopen ' + + 'fprintf fputc fputs fread freopen fscanf fseek fsetpos ftell ' + + 'fwrite getc getchar gets perror printf putc putchar puts remove ' + + 'rename rewind scanf setbuf setvbuf sprintf sscanf tmpfile tmpnam ' + + 'ungetc vfprintf vprintf vsprintf abort abs atexit atof atoi atol ' + + 'bsearch calloc div exit free getenv labs ldiv malloc mblen mbstowcs ' + + 'mbtowc qsort rand realloc srand strtod strtol strtoul system ' + + 'wcstombs wctomb memchr memcmp memcpy memmove memset strcat strchr ' + + 'strcmp strcoll strcpy strcspn strerror strlen strncat strncmp ' + + 'strncpy strpbrk strrchr strspn strstr strtok strxfrm asctime ' + + 'clock ctime difftime gmtime localtime mktime strftime time'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /^ *#.*/gm, css: 'preprocessor' }, + { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'color1 bold' }, + { regex: new RegExp(this.getKeywords(functions), 'gm'), css: 'functions bold' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword bold' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['cpp', 'c']; + + SyntaxHighlighter.brushes.Cpp = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCss.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCss.js new file mode 100644 index 0000000..4297a9a --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushCss.js @@ -0,0 +1,91 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function getKeywordsCSS(str) + { + return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; + }; + + function getValuesCSS(str) + { + return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; + }; + + var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; + + var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double '+ + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; + + var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors + { regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g, css: 'value' }, // sizes + { regex: /!important/g, css: 'color3' }, // !important + { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values + { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts + ]; + + this.forHtmlScript({ + left: /(<|<)\s*style.*?(>|>)/gi, + right: /(<|<)\/\s*style\s*(>|>)/gi + }); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['css']; + + SyntaxHighlighter.brushes.CSS = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDelphi.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDelphi.js new file mode 100644 index 0000000..e1060d4 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDelphi.js @@ -0,0 +1,55 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abs addr and ansichar ansistring array as asm begin boolean byte cardinal ' + + 'case char class comp const constructor currency destructor div do double ' + + 'downto else end except exports extended false file finalization finally ' + + 'for function goto if implementation in inherited int64 initialization ' + + 'integer interface is label library longint longword mod nil not object ' + + 'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended ' + + 'pint64 pointer private procedure program property pshortstring pstring ' + + 'pvariant pwidechar pwidestring protected public published raise real real48 ' + + 'record repeat set shl shortint shortstring shr single smallint string then ' + + 'threadvar to true try type unit until uses val var varirnt while widechar ' + + 'widestring with word write writeln xor'; + + this.regexList = [ + { regex: /\(\*[\s\S]*?\*\)/gm, css: 'comments' }, // multiline comments (* *) + { regex: /{(?!\$)[\s\S]*?}/gm, css: 'comments' }, // multiline comments { } + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /\{\$[a-zA-Z]+ .+\}/g, css: 'color1' }, // compiler Directives and Region tags + { regex: /\b[\d\.]+\b/g, css: 'value' }, // numbers 12345 + { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // numbers $F5D3 + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['delphi', 'pascal', 'pas']; + + SyntaxHighlighter.brushes.Delphi = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDiff.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDiff.js new file mode 100644 index 0000000..e9b14fc --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushDiff.js @@ -0,0 +1,41 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + this.regexList = [ + { regex: /^\+\+\+.*$/gm, css: 'color2' }, + { regex: /^\-\-\-.*$/gm, css: 'color2' }, + { regex: /^\s.*$/gm, css: 'color1' }, + { regex: /^@@.*@@$/gm, css: 'variable' }, + { regex: /^\+[^\+]{1}.*$/gm, css: 'string' }, + { regex: /^\-[^\-]{1}.*$/gm, css: 'comments' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['diff', 'patch']; + + SyntaxHighlighter.brushes.Diff = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushErlang.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushErlang.js new file mode 100644 index 0000000..6ba7d9d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushErlang.js @@ -0,0 +1,52 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Jean-Lou Dupont + // http://jldupont.blogspot.com/2009/06/erlang-syntax-highlighter.html + + // According to: http://erlang.org/doc/reference_manual/introduction.html#1.5 + var keywords = 'after and andalso band begin bnot bor bsl bsr bxor '+ + 'case catch cond div end fun if let not of or orelse '+ + 'query receive rem try when xor'+ + // additional + ' module export import define'; + + this.regexList = [ + { regex: new RegExp("[A-Z][A-Za-z0-9_]+", 'g'), css: 'constants' }, + { regex: new RegExp("\\%.+", 'gm'), css: 'comments' }, + { regex: new RegExp("\\?[A-Za-z0-9_]+", 'g'), css: 'preprocessor' }, + { regex: new RegExp("[a-z0-9_]+:[a-z0-9_]+", 'g'), css: 'functions' }, + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['erl', 'erlang']; + + SyntaxHighlighter.brushes.Erland = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushGroovy.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushGroovy.js new file mode 100644 index 0000000..6ec5c18 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushGroovy.js @@ -0,0 +1,67 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Andres Almiray + // http://jroller.com/aalmiray/entry/nice_source_code_syntax_highlighter + + var keywords = 'as assert break case catch class continue def default do else extends finally ' + + 'if in implements import instanceof interface new package property return switch ' + + 'throw throws try while public protected private static'; + var types = 'void boolean byte char short int long float double'; + var constants = 'null'; + var methods = 'allProperties count get size '+ + 'collect each eachProperty eachPropertyName eachWithIndex find findAll ' + + 'findIndexOf grep inject max min reverseEach sort ' + + 'asImmutable asSynchronized flatten intersect join pop reverse subMap toList ' + + 'padRight padLeft contains eachMatch toCharacter toLong toUrl tokenize ' + + 'eachFile eachFileRecurse eachB yte eachLine readBytes readLine getText ' + + 'splitEachLine withReader append encodeBase64 decodeBase64 filterLine ' + + 'transformChar transformLine withOutputStream withPrintWriter withStream ' + + 'withStreams withWriter withWriterAppend write writeLine '+ + 'dump inspect invokeMethod print println step times upto use waitForOrKill '+ + 'getText'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /""".*"""/g, css: 'string' }, // GStrings + { regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'value' }, // numbers + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // goovy keyword + { regex: new RegExp(this.getKeywords(types), 'gm'), css: 'color1' }, // goovy/java type + { regex: new RegExp(this.getKeywords(constants), 'gm'), css: 'constants' }, // constants + { regex: new RegExp(this.getKeywords(methods), 'gm'), css: 'functions' } // methods + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['groovy']; + + SyntaxHighlighter.brushes.Groovy = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJScript.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJScript.js new file mode 100644 index 0000000..ff98dab --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJScript.js @@ -0,0 +1,52 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'break case catch continue ' + + 'default delete do else false ' + + 'for function if in instanceof ' + + 'new null return super switch ' + + 'this throw true try typeof var while with' + ; + + var r = SyntaxHighlighter.regexLib; + + this.regexList = [ + { regex: r.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings + { regex: r.multiLineSingleQuotedString, css: 'string' }, // single quoted strings + { regex: r.singleLineCComments, css: 'comments' }, // one line comments + { regex: r.multiLineCComments, css: 'comments' }, // multiline comments + { regex: /\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keywords + ]; + + this.forHtmlScript(r.scriptScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['js', 'jscript', 'javascript']; + + SyntaxHighlighter.brushes.JScript = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJava.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJava.js new file mode 100644 index 0000000..d692fd6 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJava.js @@ -0,0 +1,57 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abstract assert boolean break byte case catch char class const ' + + 'continue default do double else enum extends ' + + 'false final finally float for goto if implements import ' + + 'instanceof int interface long native new null ' + + 'package private protected public return ' + + 'short static strictfp super switch synchronized this throw throws true ' + + 'transient try void volatile while'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: /\/\*([^\*][\s\S]*)?\*\//gm, css: 'comments' }, // multiline comments + { regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm, css: 'preprocessor' }, // documentation comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers + { regex: /(?!\@interface\b)\@[\$\w]+\b/g, css: 'color1' }, // annotation @anno + { regex: /\@interface\b/g, css: 'color2' }, // @interface keyword + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // java keyword + ]; + + this.forHtmlScript({ + left : /(<|<)%[@!=]?/g, + right : /%(>|>)/g + }); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['java']; + + SyntaxHighlighter.brushes.Java = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJavaFX.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJavaFX.js new file mode 100644 index 0000000..1a150a6 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushJavaFX.js @@ -0,0 +1,58 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Patrick Webster + // http://patrickwebster.blogspot.com/2009/04/javafx-brush-for-syntaxhighlighter.html + var datatypes = 'Boolean Byte Character Double Duration ' + + 'Float Integer Long Number Short String Void' + ; + + var keywords = 'abstract after and as assert at before bind bound break catch class ' + + 'continue def delete else exclusive extends false finally first for from ' + + 'function if import in indexof init insert instanceof into inverse last ' + + 'lazy mixin mod nativearray new not null on or override package postinit ' + + 'protected public public-init public-read replace return reverse sizeof ' + + 'step super then this throw true try tween typeof var where while with ' + + 'attribute let private readonly static trigger' + ; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: /(-?\.?)(\b(\d*\.?\d+|\d+\.?\d*)(e[+-]?\d+)?|0x[a-f\d]+)\b\.?/gi, css: 'color2' }, // numbers + { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'variable' }, // datatypes + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['jfx', 'javafx']; + + SyntaxHighlighter.brushes.JavaFX = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPerl.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPerl.js new file mode 100644 index 0000000..d94a2e0 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPerl.js @@ -0,0 +1,72 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by David Simmons-Duffin and Marty Kube + + var funcs = + 'abs accept alarm atan2 bind binmode chdir chmod chomp chop chown chr ' + + 'chroot close closedir connect cos crypt defined delete each endgrent ' + + 'endhostent endnetent endprotoent endpwent endservent eof exec exists ' + + 'exp fcntl fileno flock fork format formline getc getgrent getgrgid ' + + 'getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr ' + + 'getnetbyname getnetent getpeername getpgrp getppid getpriority ' + + 'getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid ' + + 'getservbyname getservbyport getservent getsockname getsockopt glob ' + + 'gmtime grep hex index int ioctl join keys kill lc lcfirst length link ' + + 'listen localtime lock log lstat map mkdir msgctl msgget msgrcv msgsnd ' + + 'oct open opendir ord pack pipe pop pos print printf prototype push ' + + 'quotemeta rand read readdir readline readlink readpipe recv rename ' + + 'reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl ' + + 'semget semop send setgrent sethostent setnetent setpgrp setpriority ' + + 'setprotoent setpwent setservent setsockopt shift shmctl shmget shmread ' + + 'shmwrite shutdown sin sleep socket socketpair sort splice split sprintf ' + + 'sqrt srand stat study substr symlink syscall sysopen sysread sysseek ' + + 'system syswrite tell telldir time times tr truncate uc ucfirst umask ' + + 'undef unlink unpack unshift utime values vec wait waitpid warn write'; + + var keywords = + 'bless caller continue dbmclose dbmopen die do dump else elsif eval exit ' + + 'for foreach goto if import last local my next no our package redo ref ' + + 'require return sub tie tied unless untie until use wantarray while'; + + this.regexList = [ + { regex: new RegExp('#[^!].*$', 'gm'), css: 'comments' }, + { regex: new RegExp('^\\s*#!.*$', 'gm'), css: 'preprocessor' }, // shebang + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: new RegExp('(\\$|@|%)\\w+', 'g'), css: 'variable' }, + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['perl', 'Perl', 'pl']; + + SyntaxHighlighter.brushes.Perl = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPhp.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPhp.js new file mode 100644 index 0000000..95e6e43 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPhp.js @@ -0,0 +1,88 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var funcs = 'abs acos acosh addcslashes addslashes ' + + 'array_change_key_case array_chunk array_combine array_count_values array_diff '+ + 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+ + 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+ + 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+ + 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+ + 'array_push array_rand array_reduce array_reverse array_search array_shift '+ + 'array_slice array_splice array_sum array_udiff array_udiff_assoc '+ + 'array_udiff_uassoc array_uintersect array_uintersect_assoc '+ + 'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+ + 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+ + 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+ + 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+ + 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+ + 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+ + 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+ + 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+ + 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+ + 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+ + 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+ + 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+ + 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+ + 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+ + 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+ + 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+ + 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+ + 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+ + 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+ + 'parse_ini_file parse_str parse_url passthru pathinfo print readlink realpath rewind rewinddir rmdir '+ + 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+ + 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+ + 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+ + 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+ + 'strtoupper strtr strval substr substr_compare'; + + var keywords = 'abstract and array as break case catch cfunction class clone const continue declare default die do ' + + 'else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach ' + + 'function include include_once global goto if implements interface instanceof namespace new ' + + 'old_function or private protected public return require require_once static switch ' + + 'throw try use var while xor '; + + var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\$\w+/g, css: 'variable' }, // variables + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // common functions + { regex: new RegExp(this.getKeywords(constants), 'gmi'), css: 'constants' }, // constants + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keyword + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['php']; + + SyntaxHighlighter.brushes.Php = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPlain.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPlain.js new file mode 100644 index 0000000..9f7d9e9 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPlain.js @@ -0,0 +1,33 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['text', 'plain']; + + SyntaxHighlighter.brushes.Plain = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPowerShell.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPowerShell.js new file mode 100644 index 0000000..0be1752 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPowerShell.js @@ -0,0 +1,74 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributes by B.v.Zanten, Getronics + // http://confluence.atlassian.com/display/CONFEXT/New+Code+Macro + + var keywords = 'Add-Content Add-History Add-Member Add-PSSnapin Clear(-Content)? Clear-Item ' + + 'Clear-ItemProperty Clear-Variable Compare-Object ConvertFrom-SecureString Convert-Path ' + + 'ConvertTo-Html ConvertTo-SecureString Copy(-Item)? Copy-ItemProperty Export-Alias ' + + 'Export-Clixml Export-Console Export-Csv ForEach(-Object)? Format-Custom Format-List ' + + 'Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command ' + + 'Get-Content Get-Credential Get-Culture Get-Date Get-EventLog Get-ExecutionPolicy ' + + 'Get-Help Get-History Get-Host Get-Item Get-ItemProperty Get-Location Get-Member ' + + 'Get-PfxCertificate Get-Process Get-PSDrive Get-PSProvider Get-PSSnapin Get-Service ' + + 'Get-TraceSource Get-UICulture Get-Unique Get-Variable Get-WmiObject Group-Object ' + + 'Import-Alias Import-Clixml Import-Csv Invoke-Expression Invoke-History Invoke-Item ' + + 'Join-Path Measure-Command Measure-Object Move(-Item)? Move-ItemProperty New-Alias ' + + 'New-Item New-ItemProperty New-Object New-PSDrive New-Service New-TimeSpan ' + + 'New-Variable Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Pop-Location ' + + 'Push-Location Read-Host Remove-Item Remove-ItemProperty Remove-PSDrive Remove-PSSnapin ' + + 'Remove-Variable Rename-Item Rename-ItemProperty Resolve-Path Restart-Service Resume-Service ' + + 'Select-Object Select-String Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content ' + + 'Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-Location Set-PSDebug ' + + 'Set-Service Set-TraceSource Set(-Variable)? Sort-Object Split-Path Start-Service ' + + 'Start-Sleep Start-Transcript Stop-Process Stop-Service Stop-Transcript Suspend-Service ' + + 'Tee-Object Test-Path Trace-Command Update-FormatData Update-TypeData Where(-Object)? ' + + 'Write-Debug Write-Error Write(-Host)? Write-Output Write-Progress Write-Verbose Write-Warning'; + var alias = 'ac asnp clc cli clp clv cpi cpp cvpa diff epal epcsv fc fl ' + + 'ft fw gal gc gci gcm gdr ghy gi gl gm gp gps group gsv ' + + 'gsnp gu gv gwmi iex ihy ii ipal ipcsv mi mp nal ndr ni nv oh rdr ' + + 'ri rni rnp rp rsnp rv rvpa sal sasv sc select si sl sleep sort sp ' + + 'spps spsv sv tee cat cd cp h history kill lp ls ' + + 'mount mv popd ps pushd pwd r rm rmdir echo cls chdir del dir ' + + 'erase rd ren type % \\?'; + + this.regexList = [ + { regex: /#.*$/gm, css: 'comments' }, // one line comments + { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // variables $Computer1 + { regex: /\-[a-zA-Z]+\b/g, css: 'keyword' }, // Operators -not -and -eq + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(alias), 'gmi'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['powershell', 'ps']; + + SyntaxHighlighter.brushes.PowerShell = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPython.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPython.js new file mode 100644 index 0000000..ce77462 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushPython.js @@ -0,0 +1,64 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Gheorghe Milas and Ahmad Sherif + + var keywords = 'and assert break class continue def del elif else ' + + 'except exec finally for from global if import in is ' + + 'lambda not or pass print raise return try yield while'; + + var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' + + 'chr classmethod cmp coerce compile complex delattr dict dir ' + + 'divmod enumerate eval execfile file filter float format frozenset ' + + 'getattr globals hasattr hash help hex id input int intern ' + + 'isinstance issubclass iter len list locals long map max min next ' + + 'object oct open ord pow print property range raw_input reduce ' + + 'reload repr reversed round set setattr slice sorted staticmethod ' + + 'str sum super tuple type type unichr unicode vars xrange zip'; + + var special = 'None True False self cls class_'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, + { regex: /^\s*@\w+/gm, css: 'decorator' }, + { regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' }, + { regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' }, + { regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' }, + { regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' }, + { regex: /\b\d+\.?\w*/g, css: 'value' }, + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' } + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['py', 'python']; + + SyntaxHighlighter.brushes.Python = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushRuby.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushRuby.js new file mode 100644 index 0000000..ff82130 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushRuby.js @@ -0,0 +1,55 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Erik Peterson. + + var keywords = 'alias and BEGIN begin break case class def define_method defined do each else elsif ' + + 'END end ensure false for if in module new next nil not or raise redo rescue retry return ' + + 'self super then throw true undef unless until when while yield'; + + var builtins = 'Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload ' + + 'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ' + + 'ThreadGroup Thread Time TrueClass'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\b[A-Z0-9_]+\b/g, css: 'constants' }, // constants + { regex: /:[a-z][A-Za-z0-9_]*/g, css: 'color2' }, // symbols + { regex: /(\$|@@|@)\w+/g, css: 'variable bold' }, // $global, @instance, and @@class variables + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(this.getKeywords(builtins), 'gm'), css: 'color1' } // builtins + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['ruby', 'rails', 'ror', 'rb']; + + SyntaxHighlighter.brushes.Ruby = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSass.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSass.js new file mode 100644 index 0000000..aa04da0 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSass.js @@ -0,0 +1,94 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function getKeywordsCSS(str) + { + return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; + }; + + function getValuesCSS(str) + { + return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; + }; + + var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; + + var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero digits disc dotted double '+ + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; + + var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; + + var statements = '!important !default'; + var preprocessor = '@import @extend @debug @warn @if @for @while @mixin @include'; + + var r = SyntaxHighlighter.regexLib; + + this.regexList = [ + { regex: r.multiLineCComments, css: 'comments' }, // multiline comments + { regex: r.singleLineCComments, css: 'comments' }, // singleline comments + { regex: r.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: r.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors + { regex: /\b(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)\b/g, css: 'value' }, // sizes + { regex: /\$\w+/g, css: 'variable' }, // variables + { regex: new RegExp(this.getKeywords(statements), 'g'), css: 'color3' }, // statements + { regex: new RegExp(this.getKeywords(preprocessor), 'g'), css: 'preprocessor' }, // preprocessor + { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values + { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['sass', 'scss']; + + SyntaxHighlighter.brushes.Sass = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushScala.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushScala.js new file mode 100644 index 0000000..4b0b6f0 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushScala.js @@ -0,0 +1,51 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Yegor Jbanov and David Bernard. + + var keywords = 'val sealed case def true trait implicit forSome import match object null finally super ' + + 'override try lazy for var catch throw type extends class while with new final yield abstract ' + + 'else do if return protected private this package false'; + + var keyops = '[_:=><%#@]+'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // multi-line strings + { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double-quoted string + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /0x[a-f0-9]+|\d+(\.\d+)?/gi, css: 'value' }, // numbers + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(keyops, 'gm'), css: 'keyword' } // scala keyword + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['scala']; + + SyntaxHighlighter.brushes.Scala = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSql.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSql.js new file mode 100644 index 0000000..5c2cd88 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushSql.js @@ -0,0 +1,66 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var funcs = 'abs avg case cast coalesce convert count current_timestamp ' + + 'current_user day isnull left lower month nullif replace right ' + + 'session_user space substring sum system_user upper user year'; + + var keywords = 'absolute action add after alter as asc at authorization begin bigint ' + + 'binary bit by cascade char character check checkpoint close collate ' + + 'column commit committed connect connection constraint contains continue ' + + 'create cube current current_date current_time cursor database date ' + + 'deallocate dec decimal declare default delete desc distinct double drop ' + + 'dynamic else end end-exec escape except exec execute false fetch first ' + + 'float for force foreign forward free from full function global goto grant ' + + 'group grouping having hour ignore index inner insensitive insert instead ' + + 'int integer intersect into is isolation key last level load local max min ' + + 'minute modify move name national nchar next no numeric of off on only ' + + 'open option order out output partial password precision prepare primary ' + + 'prior privileges procedure public read real references relative repeatable ' + + 'restrict return returns revoke rollback rollup rows rule schema scroll ' + + 'second section select sequence serializable set size smallint static ' + + 'statistics table temp temporary then time timestamp to top transaction ' + + 'translation trigger true truncate uncommitted union unique update values ' + + 'varchar varying view when where with work'; + + var operators = 'all and any between cross in join like not null or outer some'; + + this.regexList = [ + { regex: /--(.*)$/gm, css: 'comments' }, // one line and multiline comments + { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'color2' }, // functions + { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['sql']; + + SyntaxHighlighter.brushes.Sql = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); + diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushVb.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushVb.js new file mode 100644 index 0000000..be845dc --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushVb.js @@ -0,0 +1,56 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'AddHandler AddressOf AndAlso Alias And Ansi As Assembly Auto ' + + 'Boolean ByRef Byte ByVal Call Case Catch CBool CByte CChar CDate ' + + 'CDec CDbl Char CInt Class CLng CObj Const CShort CSng CStr CType ' + + 'Date Decimal Declare Default Delegate Dim DirectCast Do Double Each ' + + 'Else ElseIf End Enum Erase Error Event Exit False Finally For Friend ' + + 'Function Get GetType GoSub GoTo Handles If Implements Imports In ' + + 'Inherits Integer Interface Is Let Lib Like Long Loop Me Mod Module ' + + 'MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing ' + + 'NotInheritable NotOverridable Object On Option Optional Or OrElse ' + + 'Overloads Overridable Overrides ParamArray Preserve Private Property ' + + 'Protected Public RaiseEvent ReadOnly ReDim REM RemoveHandler Resume ' + + 'Return Select Set Shadows Shared Short Single Static Step Stop String ' + + 'Structure Sub SyncLock Then Throw To True Try TypeOf Unicode Until ' + + 'Variant When While With WithEvents WriteOnly Xor'; + + this.regexList = [ + { regex: /'.*$/gm, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: /^\s*#.*$/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // vb keyword + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['vb', 'vbnet']; + + SyntaxHighlighter.brushes.Vb = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushXml.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushXml.js new file mode 100644 index 0000000..69d9fd0 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shBrushXml.js @@ -0,0 +1,69 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function process(match, regexInfo) + { + var constructor = SyntaxHighlighter.Match, + code = match[0], + tag = new XRegExp('(<|<)[\\s\\/\\?]*(?[:\\w-\\.]+)', 'xg').exec(code), + result = [] + ; + + if (match.attributes != null) + { + var attributes, + regex = new XRegExp('(? [\\w:\\-\\.]+)' + + '\\s*=\\s*' + + '(? ".*?"|\'.*?\'|\\w+)', + 'xg'); + + while ((attributes = regex.exec(code)) != null) + { + result.push(new constructor(attributes.name, match.index + attributes.index, 'color1')); + result.push(new constructor(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string')); + } + } + + if (tag != null) + result.push( + new constructor(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword') + ); + + return result; + } + + this.regexList = [ + { regex: new XRegExp('(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)', 'gm'), css: 'color2' }, // + { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // + { regex: new XRegExp('(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)', 'sg'), func: process } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['xml', 'xhtml', 'xslt', 'html']; + + SyntaxHighlighter.brushes.Xml = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shCore.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shCore.js new file mode 100644 index 0000000..b47b645 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shCore.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('K M;I(M)1S 2U("2a\'t 4k M 4K 2g 3l 4G 4H");(6(){6 r(f,e){I(!M.1R(f))1S 3m("3s 15 4R");K a=f.1w;f=M(f.1m,t(f)+(e||""));I(a)f.1w={1m:a.1m,19:a.19?a.19.1a(0):N};H f}6 t(f){H(f.1J?"g":"")+(f.4s?"i":"")+(f.4p?"m":"")+(f.4v?"x":"")+(f.3n?"y":"")}6 B(f,e,a,b){K c=u.L,d,h,g;v=R;5K{O(;c--;){g=u[c];I(a&g.3r&&(!g.2p||g.2p.W(b))){g.2q.12=e;I((h=g.2q.X(f))&&h.P===e){d={3k:g.2b.W(b,h,a),1C:h};1N}}}}5v(i){1S i}5q{v=11}H d}6 p(f,e,a){I(3b.Z.1i)H f.1i(e,a);O(a=a||0;a-1},3d:6(g){e+=g}};c1&&p(e,"")>-1){a=15(J.1m,n.Q.W(t(J),"g",""));n.Q.W(f.1a(e.P),a,6(){O(K c=1;c<14.L-2;c++)I(14[c]===1d)e[c]=1d})}I(J.1w&&J.1w.19)O(K b=1;be.P&&J.12--}H e};I(!D)15.Z.1A=6(f){(f=n.X.W(J,f))&&J.1J&&!f[0].L&&J.12>f.P&&J.12--;H!!f};1r.Z.1C=6(f){M.1R(f)||(f=15(f));I(f.1J){K e=n.1C.1p(J,14);f.12=0;H e}H f.X(J)};1r.Z.Q=6(f,e){K a=M.1R(f),b,c;I(a&&1j e.58()==="3f"&&e.1i("${")===-1&&y)H n.Q.1p(J,14);I(a){I(f.1w)b=f.1w.19}Y f+="";I(1j e==="6")c=n.Q.W(J,f,6(){I(b){14[0]=1f 1r(14[0]);O(K d=0;dd.L-3;){i=1r.Z.1a.W(g,-1)+i;g=1Q.3i(g/10)}H(g?d[g]||"":"$")+i}Y{g=+i;I(g<=d.L-3)H d[g];g=b?p(b,i):-1;H g>-1?d[g+1]:h}})})}I(a&&f.1J)f.12=0;H c};1r.Z.1e=6(f,e){I(!M.1R(f))H n.1e.1p(J,14);K a=J+"",b=[],c=0,d,h;I(e===1d||+e<0)e=5D;Y{e=1Q.3i(+e);I(!e)H[]}O(f=M.3c(f);d=f.X(a);){I(f.12>c){b.U(a.1a(c,d.P));d.L>1&&d.P=e)1N}f.12===d.P&&f.12++}I(c===a.L){I(!n.1A.W(f,"")||h)b.U("")}Y b.U(a.1a(c));H b.L>e?b.1a(0,e):b};M.1h(/\\(\\?#[^)]*\\)/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"});M.1h(/\\((?!\\?)/,6(){J.19.U(N);H"("});M.1h(/\\(\\?<([$\\w]+)>/,6(f){J.19.U(f[1]);J.2N=R;H"("});M.1h(/\\\\k<([\\w$]+)>/,6(f){K e=p(J.19,f[1]);H e>-1?"\\\\"+(e+1)+(3R(f.2S.3a(f.P+f[0].L))?"":"(?:)"):f[0]});M.1h(/\\[\\^?]/,6(f){H f[0]==="[]"?"\\\\b\\\\B":"[\\\\s\\\\S]"});M.1h(/^\\(\\?([5A]+)\\)/,6(f){J.3d(f[1]);H""});M.1h(/(?:\\s+|#.*)+/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"},M.1B,6(){H J.2K("x")});M.1h(/\\./,6(){H"[\\\\s\\\\S]"},M.1B,6(){H J.2K("s")})})();1j 2e!="1d"&&(2e.M=M);K 1v=6(){6 r(a,b){a.1l.1i(b)!=-1||(a.1l+=" "+b)}6 t(a){H a.1i("3e")==0?a:"3e"+a}6 B(a){H e.1Y.2A[t(a)]}6 p(a,b,c){I(a==N)H N;K d=c!=R?a.3G:[a.2G],h={"#":"1c",".":"1l"}[b.1o(0,1)]||"3h",g,i;g=h!="3h"?b.1o(1):b.5u();I((a[h]||"").1i(g)!=-1)H a;O(a=0;d&&a\'+c+""});H a}6 n(a,b){a.1e("\\n");O(K c="",d=0;d<50;d++)c+=" ";H a=v(a,6(h){I(h.1i("\\t")==-1)H h;O(K g=0;(g=h.1i("\\t"))!=-1;)h=h.1o(0,g)+c.1o(0,b-g%b)+h.1o(g+1,h.L);H h})}6 x(a){H a.Q(/^\\s+|\\s+$/g,"")}6 D(a,b){I(a.Pb.P)H 1;Y I(a.Lb.L)H 1;H 0}6 y(a,b){6 c(k){H k[0]}O(K d=N,h=[],g=b.2D?b.2D:c;(d=b.1I.X(a))!=N;){K i=g(d,b);I(1j i=="3f")i=[1f e.2L(i,d.P,b.23)];h=h.1O(i)}H h}6 E(a){K b=/(.*)((&1G;|&1y;).*)/;H a.Q(e.3A.3M,6(c){K d="",h=N;I(h=b.X(c)){c=h[1];d=h[2]}H\'\'+c+""+d})}6 z(){O(K a=1E.36("1k"),b=[],c=0;c<1z 4I="1Z://2y.3L.3K/4L/5L"><3J><4N 1Z-4M="5G-5M" 6K="2O/1z; 6J=6I-8" /><1t>6L 1v<3B 1L="25-6M:6Q,6P,6O,6N-6F;6y-2f:#6x;2f:#6w;25-22:6v;2O-3D:3C;">1v3v 3.0.76 (72 73 3x)1Z://3u.2w/1v70 17 6U 71.6T 6X-3x 6Y 6D.6t 61 60 J 1k, 5Z 5R 5V <2R/>5U 5T 5S!\'}},1Y:{2j:N,2A:{}},1U:{},3A:{6n:/\\/\\*[\\s\\S]*?\\*\\//2c,6m:/\\/\\/.*$/2c,6l:/#.*$/2c,6k:/"([^\\\\"\\n]|\\\\.)*"/g,6o:/\'([^\\\\\'\\n]|\\\\.)*\'/g,6p:1f M(\'"([^\\\\\\\\"]|\\\\\\\\.)*"\',"3z"),6s:1f M("\'([^\\\\\\\\\']|\\\\\\\\.)*\'","3z"),6q:/(&1y;|<)!--[\\s\\S]*?--(&1G;|>)/2c,3M:/\\w+:\\/\\/[\\w-.\\/?%&=:@;]*/g,6a:{18:/(&1y;|<)\\?=?/g,1b:/\\?(&1G;|>)/g},69:{18:/(&1y;|<)%=?/g,1b:/%(&1G;|>)/g},6d:{18:/(&1y;|<)\\s*1k.*?(&1G;|>)/2T,1b:/(&1y;|<)\\/\\s*1k\\s*(&1G;|>)/2T}},16:{1H:6(a){6 b(i,k){H e.16.2o(i,k,e.13.1x[k])}O(K c=\'\',d=e.16.2x,h=d.2X,g=0;g";H c},2o:6(a,b,c){H\'<2W>\'+c+""},2b:6(a){K b=a.1F,c=b.1l||"";b=B(p(b,".20",R).1c);K d=6(h){H(h=15(h+"6f(\\\\w+)").X(c))?h[1]:N}("6g");b&&d&&e.16.2x[d].2B(b);a.3N()},2x:{2X:["21","2P"],21:{1H:6(a){I(a.V("2l")!=R)H"";K b=a.V("1t");H e.16.2o(a,"21",b?b:e.13.1x.21)},2B:6(a){a=1E.6j(t(a.1c));a.1l=a.1l.Q("47","")}},2P:{2B:6(){K a="68=0";a+=", 18="+(31.30-33)/2+", 32="+(31.2Z-2Y)/2+", 30=33, 2Z=2Y";a=a.Q(/^,/,"");a=1P.6Z("","38",a);a.2C();K b=a.1E;b.6W(e.13.1x.37);b.6V();a.2C()}}}},35:6(a,b){K c;I(b)c=[b];Y{c=1E.36(e.13.34);O(K d=[],h=0;h(.*?))\\\\]$"),s=1f M("(?<27>[\\\\w-]+)\\\\s*:\\\\s*(?<1T>[\\\\w-%#]+|\\\\[.*?\\\\]|\\".*?\\"|\'.*?\')\\\\s*;?","g");(j=s.X(k))!=N;){K o=j.1T.Q(/^[\'"]|[\'"]$/g,"");I(o!=N&&m.1A(o)){o=m.X(o);o=o.2V.L>0?o.2V.1e(/\\s*,\\s*/):[]}l[j.27]=o}g={1F:g,1n:C(i,l)};g.1n.1D!=N&&d.U(g)}H d},1M:6(a,b){K c=J.35(a,b),d=N,h=e.13;I(c.L!==0)O(K g=0;g")==o-3){m=m.4h(0,o-3);s=R}l=s?m:l}I((i.1t||"")!="")k.1t=i.1t;k.1D=j;d.2Q(k);b=d.2F(l);I((i.1c||"")!="")b.1c=i.1c;i.2G.74(b,i)}}},2E:6(a){w(1P,"4k",6(){e.1M(a)})}};e.2E=e.2E;e.1M=e.1M;e.2L=6(a,b,c){J.1T=a;J.P=b;J.L=a.L;J.23=c;J.1V=N};e.2L.Z.1q=6(){H J.1T};e.4l=6(a){6 b(j,l){O(K m=0;md)1N;Y I(g.P==c.P&&g.L>c.L)a[b]=N;Y I(g.P>=c.P&&g.P\'+c+""},3Q:6(a,b){K c="",d=a.1e("\\n").L,h=2u(J.V("2i-1s")),g=J.V("2z-1s-2t");I(g==R)g=(h+d-1).1q().L;Y I(3R(g)==R)g=0;O(K i=0;i\'+j+"":"")+i)}H a},4f:6(a){H a?"<4a>"+a+"":""},4b:6(a,b){6 c(l){H(l=l?l.1V||g:g)?l+" ":""}O(K d=0,h="",g=J.V("1D",""),i=0;i|&1y;2R\\s*\\/?&1G;/2T;I(e.13.46==R)b=b.Q(h,"\\n");I(e.13.44==R)b=b.Q(h,"");b=b.1e("\\n");h=/^\\s*/;g=4Q;O(K i=0;i0;i++){K k=b[i];I(x(k).L!=0){k=h.X(k);I(k==N){a=a;1N a}g=1Q.4q(k[0].L,g)}}I(g>0)O(i=0;i\'+(J.V("16")?e.16.1H(J):"")+\'<3Z 5z="0" 5H="0" 5J="0">\'+J.4f(J.V("1t"))+"<3T><3P>"+(1u?\'<2d 1g="1u">\'+J.3Q(a)+"":"")+\'<2d 1g="17">\'+b+""},2F:6(a){I(a===N)a="";J.17=a;K b=J.3Y("T");b.3X=J.1H(a);J.V("16")&&w(p(b,".16"),"5c",e.16.2b);J.V("3V-17")&&w(p(b,".17"),"56",f);H b},2Q:6(a){J.1c=""+1Q.5d(1Q.5n()*5k).1q();e.1Y.2A[t(J.1c)]=J;J.1n=C(e.2v,a||{});I(J.V("2k")==R)J.1n.16=J.1n.1u=11},5j:6(a){a=a.Q(/^\\s+|\\s+$/g,"").Q(/\\s+/g,"|");H"\\\\b(?:"+a+")\\\\b"},5f:6(a){J.28={18:{1I:a.18,23:"1k"},1b:{1I:a.1b,23:"1k"},17:1f M("(?<18>"+a.18.1m+")(?<17>.*?)(?<1b>"+a.1b.1m+")","5o")}}};H e}();1j 2e!="1d"&&(2e.1v=1v);',62,441,'||||||function|||||||||||||||||||||||||||||||||||||return|if|this|var|length|XRegExp|null|for|index|replace|true||div|push|getParam|call|exec|else|prototype||false|lastIndex|config|arguments|RegExp|toolbar|code|left|captureNames|slice|right|id|undefined|split|new|class|addToken|indexOf|typeof|script|className|source|params|substr|apply|toString|String|line|title|gutter|SyntaxHighlighter|_xregexp|strings|lt|html|test|OUTSIDE_CLASS|match|brush|document|target|gt|getHtml|regex|global|join|style|highlight|break|concat|window|Math|isRegExp|throw|value|brushes|brushName|space|alert|vars|http|syntaxhighlighter|expandSource|size|css|case|font|Fa|name|htmlScript|dA|can|handler|gm|td|exports|color|in|href|first|discoveredBrushes|light|collapse|object|cache|getButtonHtml|trigger|pattern|getLineHtml|nbsp|numbers|parseInt|defaults|com|items|www|pad|highlighters|execute|focus|func|all|getDiv|parentNode|navigator|INSIDE_CLASS|regexList|hasFlag|Match|useScriptTags|hasNamedCapture|text|help|init|br|input|gi|Error|values|span|list|250|height|width|screen|top|500|tagName|findElements|getElementsByTagName|aboutDialog|_blank|appendChild|charAt|Array|copyAsGlobal|setFlag|highlighter_|string|attachEvent|nodeName|floor|backref|output|the|TypeError|sticky|Za|iterate|freezeTokens|scope|type|textarea|alexgorbatchev|version|margin|2010|005896|gs|regexLib|body|center|align|noBrush|require|childNodes|DTD|xhtml1|head|org|w3|url|preventDefault|container|tr|getLineNumbersHtml|isNaN|userAgent|tbody|isLineHighlighted|quick|void|innerHTML|create|table|links|auto|smart|tab|stripBrs|tabs|bloggerMode|collapsed|plain|getCodeLinesHtml|caption|getMatchesHtml|findMatches|figureOutLineNumbers|removeNestedMatches|getTitleHtml|brushNotHtmlScript|substring|createElement|Highlighter|load|HtmlScript|Brush|pre|expand|multiline|min|Can|ignoreCase|find|blur|extended|toLowerCase|aliases|addEventListener|innerText|textContent|wasn|select|createTextNode|removeChild|option|same|frame|xmlns|dtd|twice|1999|equiv|meta|htmlscript|transitional|1E3|expected|PUBLIC|DOCTYPE|on|W3C|XHTML|TR|EN|Transitional||configured|srcElement|Object|after|run|dblclick|matchChain|valueOf|constructor|default|switch|click|round|execAt|forHtmlScript|token|gimy|functions|getKeywords|1E6|escape|within|random|sgi|another|finally|supply|MSIE|ie|toUpperCase|catch|returnValue|definition|event|border|imsx|constructing|one|Infinity|from|when|Content|cellpadding|flags|cellspacing|try|xhtml|Type|spaces|2930402|hosted_button_id|lastIndexOf|donate|active|development|keep|to|xclick|_s|Xml|please|like|you|paypal|cgi|cmd|webscr|bin|highlighted|scrollbars|aspScriptTags|phpScriptTags|sort|max|scriptScriptTags|toolbar_item|_|command|command_|number|getElementById|doubleQuotedString|singleLinePerlComments|singleLineCComments|multiLineCComments|singleQuotedString|multiLineDoubleQuotedString|xmlComments|alt|multiLineSingleQuotedString|If|https|1em|000|fff|background|5em|xx|bottom|75em|Gorbatchev|large|serif|CDATA|continue|utf|charset|content|About|family|sans|Helvetica|Arial|Geneva|3em|nogutter|Copyright|syntax|close|write|2004|Alex|open|JavaScript|highlighter|July|02|replaceChild|offset|83'.split('|'),0,{})) diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shLegacy.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shLegacy.js new file mode 100644 index 0000000..6d9fd4d --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/scripts/shLegacy.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('3 u={8:{}};u.8={A:4(c,k,l,m,n,o){4 d(a,b){2 a!=1?a:b}4 f(a){2 a!=1?a.E():1}c=c.I(":");3 g=c[0],e={};t={"r":K};M=1;5=8.5;9(3 j R c)e[c[j]]="r";k=f(d(k,5.C));l=f(d(l,5.D));m=f(d(m,5.s));o=f(d(o,5.Q));n=f(d(n,5["x-y"]));2{P:g,C:d(t[e.O],k),D:d(t[e.N],l),s:d({"r":r}[e.s],m),"x-y":d(4(a,b){9(3 h=T S("^"+b+"\\\\[(?\\\\w+)\\\\]$","U"),i=1,p=0;p tags to the document body + for (i = 0; i < elements.length; i++) + { + var url = brushes[elements[i].params.brush]; + + if (!url) + continue; + + scripts[url] = false; + loadScript(url); + } + + function loadScript(url) + { + var script = document.createElement('script'), + done = false + ; + + script.src = url; + script.type = 'text/javascript'; + script.language = 'javascript'; + script.onload = script.onreadystatechange = function() + { + if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) + { + done = true; + scripts[url] = true; + checkAll(); + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + script.parentNode.removeChild(script); + } + }; + + // sync way of adding script tags to the page + document.body.appendChild(script); + }; + + function checkAll() + { + for(var url in scripts) + if (scripts[url] == false) + return; + + if (allCalled) + SyntaxHighlighter.highlight(allParams); + }; +}; + +})(); diff --git a/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/src/shCore.js b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/src/shCore.js new file mode 100644 index 0000000..4214763 --- /dev/null +++ b/upstream/clipper-6.4.2/Documentation/Scripts/SyntaxHighlighter/src/shCore.js @@ -0,0 +1,1721 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +// +// Begin anonymous function. This is used to contain local scope variables without polutting global scope. +// +var SyntaxHighlighter = function() { + +// CommonJS +if (typeof(require) != 'undefined' && typeof(XRegExp) == 'undefined') +{ + XRegExp = require('XRegExp').XRegExp; +} + +// Shortcut object which will be assigned to the SyntaxHighlighter variable. +// This is a shorthand for local reference in order to avoid long namespace +// references to SyntaxHighlighter.whatever... +var sh = { + defaults : { + /** Additional CSS class names to be added to highlighter elements. */ + 'class-name' : '', + + /** First line number. */ + 'first-line' : 1, + + /** + * Pads line numbers. Possible values are: + * + * false - don't pad line numbers. + * true - automaticaly pad numbers with minimum required number of leading zeroes. + * [int] - length up to which pad line numbers. + */ + 'pad-line-numbers' : false, + + /** Lines to highlight. */ + 'highlight' : null, + + /** Title to be displayed above the code block. */ + 'title' : null, + + /** Enables or disables smart tabs. */ + 'smart-tabs' : true, + + /** Gets or sets tab size. */ + 'tab-size' : 4, + + /** Enables or disables gutter. */ + 'gutter' : true, + + /** Enables or disables toolbar. */ + 'toolbar' : true, + + /** Enables quick code copy and paste from double click. */ + 'quick-code' : true, + + /** Forces code view to be collapsed. */ + 'collapse' : false, + + /** Enables or disables automatic links. */ + 'auto-links' : true, + + /** Gets or sets light mode. Equavalent to turning off gutter and toolbar. */ + 'light' : false, + + 'html-script' : false + }, + + config : { + space : ' ', + + /** Enables use of tags. */ + scriptScriptTags : { left: /(<|<)\s*script.*?(>|>)/gi, right: /(<|<)\/\s*script\s*(>|>)/gi } + }, + + toolbar: { + /** + * Generates HTML markup for the toolbar. + * @param {Highlighter} highlighter Highlighter instance. + * @return {String} Returns HTML markup. + */ + getHtml: function(highlighter) + { + var html = '
', + items = sh.toolbar.items, + list = items.list + ; + + function defaultGetHtml(highlighter, name) + { + return sh.toolbar.getButtonHtml(highlighter, name, sh.config.strings[name]); + }; + + for (var i = 0; i < list.length; i++) + html += (items[list[i]].getHtml || defaultGetHtml)(highlighter, list[i]); + + html += '
'; + + return html; + }, + + /** + * Generates HTML markup for a regular button in the toolbar. + * @param {Highlighter} highlighter Highlighter instance. + * @param {String} commandName Command name that would be executed. + * @param {String} label Label text to display. + * @return {String} Returns HTML markup. + */ + getButtonHtml: function(highlighter, commandName, label) + { + return '' + label + '' + ; + }, + + /** + * Event handler for a toolbar anchor. + */ + handler: function(e) + { + var target = e.target, + className = target.className || '' + ; + + function getValue(name) + { + var r = new RegExp(name + '_(\\w+)'), + match = r.exec(className) + ; + + return match ? match[1] : null; + }; + + var highlighter = getHighlighterById(findParentElement(target, '.syntaxhighlighter').id), + commandName = getValue('command') + ; + + // execute the toolbar command + if (highlighter && commandName) + sh.toolbar.items[commandName].execute(highlighter); + + // disable default A click behaviour + e.preventDefault(); + }, + + /** Collection of toolbar items. */ + items : { + // Ordered lis of items in the toolbar. Can't expect `for (var n in items)` to be consistent. + list: ['expandSource', 'help'], + + expandSource: { + getHtml: function(highlighter) + { + if (highlighter.getParam('collapse') != true) + return ''; + + var title = highlighter.getParam('title'); + return sh.toolbar.getButtonHtml(highlighter, 'expandSource', title ? title : sh.config.strings.expandSource); + }, + + execute: function(highlighter) + { + var div = getHighlighterDivById(highlighter.id); + removeClass(div, 'collapsed'); + } + }, + + /** Command to display the about dialog window. */ + help: { + execute: function(highlighter) + { + var wnd = popup('', '_blank', 500, 250, 'scrollbars=0'), + doc = wnd.document + ; + + doc.write(sh.config.strings.aboutDialog); + doc.close(); + wnd.focus(); + } + } + } + }, + + /** + * Finds all elements on the page which should be processes by SyntaxHighlighter. + * + * @param {Object} globalParams Optional parameters which override element's + * parameters. Only used if element is specified. + * + * @param {Object} element Optional element to highlight. If none is + * provided, all elements in the current document + * are returned which qualify. + * + * @return {Array} Returns list of { target: DOMElement, params: Object } objects. + */ + findElements: function(globalParams, element) + { + var elements = element ? [element] : toArray(document.getElementsByTagName(sh.config.tagName)), + conf = sh.config, + result = [] + ; + + // support for