bscheibel 5 rokov pred
commit
cdf98e02bb
100 zmenil súbory, kde vykonal 32131 pridanie a 0 odobranie
  1. 13 0
      .idea/dxf_reader.iml
  2. 4 0
      .idea/misc.xml
  3. 8 0
      .idea/modules.xml
  4. 6 0
      .idea/vcs.xml
  5. 13912 0
      GV_12.DXF
  6. 23 0
      dxf_reader.py
  7. 76 0
      venv/bin/activate
  8. 37 0
      venv/bin/activate.csh
  9. 75 0
      venv/bin/activate.fish
  10. 10 0
      venv/bin/dxfaudit
  11. 10 0
      venv/bin/dxfpp
  12. 12 0
      venv/bin/easy_install
  13. 12 0
      venv/bin/easy_install-3.7
  14. 12 0
      venv/bin/pip
  15. 12 0
      venv/bin/pip3
  16. 12 0
      venv/bin/pip3.7
  17. BIN
      venv/bin/python
  18. BIN
      venv/bin/python3
  19. BIN
      venv/bin/python3.7
  20. 2 0
      venv/lib/python3.7/site-packages/easy-install.pth
  21. 1 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/INSTALLER
  22. 47 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/LICENSE.txt
  23. 394 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/METADATA
  24. 373 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/RECORD
  25. 5 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/WHEEL
  26. 4 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/entry_points.txt
  27. 1 0
      venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/top_level.txt
  28. 33 0
      venv/lib/python3.7/site-packages/ezdxf/__init__.py
  29. 611 0
      venv/lib/python3.7/site-packages/ezdxf/acadctb.py
  30. 9 0
      venv/lib/python3.7/site-packages/ezdxf/addons/__init__.py
  31. 684 0
      venv/lib/python3.7/site-packages/ezdxf/addons/dimlines.py
  32. 277 0
      venv/lib/python3.7/site-packages/ezdxf/addons/importer.py
  33. 203 0
      venv/lib/python3.7/site-packages/ezdxf/addons/menger_sponge.py
  34. 19 0
      venv/lib/python3.7/site-packages/ezdxf/addons/mixins.py
  35. 158 0
      venv/lib/python3.7/site-packages/ezdxf/addons/mtext.py
  36. 218 0
      venv/lib/python3.7/site-packages/ezdxf/addons/sierpinski_pyramid.py
  37. 851 0
      venv/lib/python3.7/site-packages/ezdxf/addons/table.py
  38. 6 0
      venv/lib/python3.7/site-packages/ezdxf/audit/__init__.py
  39. 71 0
      venv/lib/python3.7/site-packages/ezdxf/audit/__main__.py
  40. 284 0
      venv/lib/python3.7/site-packages/ezdxf/audit/auditor.py
  41. 120 0
      venv/lib/python3.7/site-packages/ezdxf/database.py
  42. 457 0
      venv/lib/python3.7/site-packages/ezdxf/dimstyleoverride.py
  43. 549 0
      venv/lib/python3.7/site-packages/ezdxf/drawing.py
  44. 521 0
      venv/lib/python3.7/site-packages/ezdxf/dxfentity.py
  45. 18 0
      venv/lib/python3.7/site-packages/ezdxf/dxffactory.py
  46. 47 0
      venv/lib/python3.7/site-packages/ezdxf/entityspace.py
  47. 113 0
      venv/lib/python3.7/site-packages/ezdxf/eztypes.py
  48. 177 0
      venv/lib/python3.7/site-packages/ezdxf/filemanagement.py
  49. 1094 0
      venv/lib/python3.7/site-packages/ezdxf/graphicsfactory.py
  50. 44 0
      venv/lib/python3.7/site-packages/ezdxf/groupby.py
  51. 5 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/__init__.py
  52. 200 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/attrib.py
  53. 68 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/block.py
  54. 183 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/dimension.py
  55. 259 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/facemixins.py
  56. 137 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/factory.py
  57. 205 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/graphics.py
  58. 143 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/headervars.py
  59. 289 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/insert.py
  60. 574 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/layouts.py
  61. 54 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/polyfacebuilder.py
  62. 335 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/polyline.py
  63. 859 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/tableentries.py
  64. 119 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/text.py
  65. 78 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/trace.py
  66. 257 0
      venv/lib/python3.7/site-packages/ezdxf/legacy/viewport.py
  67. 4 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/__init__.py
  68. 249 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/attributes.py
  69. 445 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/const.py
  70. 30 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/encoding.py
  71. 366 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/extendedtags.py
  72. 19 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/hdrvars.py
  73. 169 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/loader.py
  74. 241 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/packedtags.py
  75. 558 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/repair.py
  76. 164 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/tagger.py
  77. 319 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/tags.py
  78. 47 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/tagwriter.py
  79. 218 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/types.py
  80. 160 0
      venv/lib/python3.7/site-packages/ezdxf/lldxf/validator.py
  81. 47 0
      venv/lib/python3.7/site-packages/ezdxf/math/__init__.py
  82. 223 0
      venv/lib/python3.7/site-packages/ezdxf/math/arc.py
  83. 88 0
      venv/lib/python3.7/site-packages/ezdxf/math/bbox.py
  84. 194 0
      venv/lib/python3.7/site-packages/ezdxf/math/bezier.py
  85. 159 0
      venv/lib/python3.7/site-packages/ezdxf/math/bezier4p.py
  86. 194 0
      venv/lib/python3.7/site-packages/ezdxf/math/box.py
  87. 861 0
      venv/lib/python3.7/site-packages/ezdxf/math/bspline.py
  88. 135 0
      venv/lib/python3.7/site-packages/ezdxf/math/bulge.py
  89. 145 0
      venv/lib/python3.7/site-packages/ezdxf/math/circle.py
  90. 104 0
      venv/lib/python3.7/site-packages/ezdxf/math/construct2d.py
  91. 37 0
      venv/lib/python3.7/site-packages/ezdxf/math/convexhull.py
  92. 99 0
      venv/lib/python3.7/site-packages/ezdxf/math/eulerspiral.py
  93. 274 0
      venv/lib/python3.7/site-packages/ezdxf/math/line.py
  94. 209 0
      venv/lib/python3.7/site-packages/ezdxf/math/matrix.py
  95. 541 0
      venv/lib/python3.7/site-packages/ezdxf/math/matrix44.py
  96. 20 0
      venv/lib/python3.7/site-packages/ezdxf/math/points.py
  97. 62 0
      venv/lib/python3.7/site-packages/ezdxf/math/shape.py
  98. 46 0
      venv/lib/python3.7/site-packages/ezdxf/math/surfaces.py
  99. 314 0
      venv/lib/python3.7/site-packages/ezdxf/math/ucs.py
  100. 0 0
      venv/lib/python3.7/site-packages/ezdxf/math/vector.py

+ 13 - 0
.idea/dxf_reader.iml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/venv" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="TestRunnerService">
+    <option name="PROJECT_TEST_RUNNER" value="Unittests" />
+  </component>
+</module>

+ 4 - 0
.idea/misc.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (dxf_reader)" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/dxf_reader.iml" filepath="$PROJECT_DIR$/.idea/dxf_reader.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 13912 - 0
GV_12.DXF


+ 23 - 0
dxf_reader.py

@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+from fileinput import input
+
+
+def printpoint(b):
+    print(b)
+    obj = dict(zip(b[::2], b[1::2]))
+    if obj['0'] == 'AcDbMText':
+        print('{}'.format(obj['100']))
+
+
+print('Code','Text')# header line
+buffer = ['0', 'fake']    # give first pass through loop something to process
+for line in input("GV_12.DXF"):
+    line = line.rstrip()
+    print(line)
+    if line == '0':         # we've started a new section, so
+        printpoint(buffer)      # handle the captured section
+        buffer = []             # and start a new one
+    buffer.append(line)
+
+printpoint(buffer)        # buffer left over from last pass through loop

+ 76 - 0
venv/bin/activate

@@ -0,0 +1,76 @@
+# This file must be used with "source bin/activate" *from bash*
+# you cannot run it directly
+
+deactivate () {
+    # reset old environment variables
+    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
+        PATH="${_OLD_VIRTUAL_PATH:-}"
+        export PATH
+        unset _OLD_VIRTUAL_PATH
+    fi
+    if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
+        PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
+        export PYTHONHOME
+        unset _OLD_VIRTUAL_PYTHONHOME
+    fi
+
+    # This should detect bash and zsh, which have a hash command that must
+    # be called to get it to forget past commands.  Without forgetting
+    # past commands the $PATH changes we made may not be respected
+    if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+        hash -r
+    fi
+
+    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
+        PS1="${_OLD_VIRTUAL_PS1:-}"
+        export PS1
+        unset _OLD_VIRTUAL_PS1
+    fi
+
+    unset VIRTUAL_ENV
+    if [ ! "$1" = "nondestructive" ] ; then
+    # Self destruct!
+        unset -f deactivate
+    fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+VIRTUAL_ENV="/home/bscheibel/PycharmProjects/dxf_reader/venv"
+export VIRTUAL_ENV
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${PYTHONHOME:-}" ] ; then
+    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
+    unset PYTHONHOME
+fi
+
+if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+    _OLD_VIRTUAL_PS1="${PS1:-}"
+    if [ "x(venv) " != x ] ; then
+	PS1="(venv) ${PS1:-}"
+    else
+    if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
+        # special case for Aspen magic directories
+        # see http://www.zetadev.com/software/aspen/
+        PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
+    else
+        PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
+    fi
+    fi
+    export PS1
+fi
+
+# This should detect bash and zsh, which have a hash command that must
+# be called to get it to forget past commands.  Without forgetting
+# past commands the $PATH changes we made may not be respected
+if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+    hash -r
+fi

+ 37 - 0
venv/bin/activate.csh

@@ -0,0 +1,37 @@
+# This file must be used with "source bin/activate.csh" *from csh*.
+# You cannot run it directly.
+# Created by Davide Di Blasi <davidedb@gmail.com>.
+# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
+
+alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+setenv VIRTUAL_ENV "/home/bscheibel/PycharmProjects/dxf_reader/venv"
+
+set _OLD_VIRTUAL_PATH="$PATH"
+setenv PATH "$VIRTUAL_ENV/bin:$PATH"
+
+
+set _OLD_VIRTUAL_PROMPT="$prompt"
+
+if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+    if ("venv" != "") then
+        set env_name = "venv"
+    else
+        if (`basename "VIRTUAL_ENV"` == "__") then
+            # special case for Aspen magic directories
+            # see http://www.zetadev.com/software/aspen/
+            set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
+        else
+            set env_name = `basename "$VIRTUAL_ENV"`
+        endif
+    endif
+    set prompt = "[$env_name] $prompt"
+    unset env_name
+endif
+
+alias pydoc python -m pydoc
+
+rehash

+ 75 - 0
venv/bin/activate.fish

@@ -0,0 +1,75 @@
+# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org)
+# you cannot run it directly
+
+function deactivate  -d "Exit virtualenv and return to normal shell environment"
+    # reset old environment variables
+    if test -n "$_OLD_VIRTUAL_PATH"
+        set -gx PATH $_OLD_VIRTUAL_PATH
+        set -e _OLD_VIRTUAL_PATH
+    end
+    if test -n "$_OLD_VIRTUAL_PYTHONHOME"
+        set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
+        set -e _OLD_VIRTUAL_PYTHONHOME
+    end
+
+    if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
+        functions -e fish_prompt
+        set -e _OLD_FISH_PROMPT_OVERRIDE
+        functions -c _old_fish_prompt fish_prompt
+        functions -e _old_fish_prompt
+    end
+
+    set -e VIRTUAL_ENV
+    if test "$argv[1]" != "nondestructive"
+        # Self destruct!
+        functions -e deactivate
+    end
+end
+
+# unset irrelevant variables
+deactivate nondestructive
+
+set -gx VIRTUAL_ENV "/home/bscheibel/PycharmProjects/dxf_reader/venv"
+
+set -gx _OLD_VIRTUAL_PATH $PATH
+set -gx PATH "$VIRTUAL_ENV/bin" $PATH
+
+# unset PYTHONHOME if set
+if set -q PYTHONHOME
+    set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
+    set -e PYTHONHOME
+end
+
+if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+    # fish uses a function instead of an env var to generate the prompt.
+
+    # save the current fish_prompt function as the function _old_fish_prompt
+    functions -c fish_prompt _old_fish_prompt
+
+    # with the original prompt function renamed, we can override with our own.
+    function fish_prompt
+        # Save the return status of the last command
+        set -l old_status $status
+
+        # Prompt override?
+        if test -n "(venv) "
+            printf "%s%s" "(venv) " (set_color normal)
+        else
+            # ...Otherwise, prepend env
+            set -l _checkbase (basename "$VIRTUAL_ENV")
+            if test $_checkbase = "__"
+                # special case for Aspen magic directories
+                # see http://www.zetadev.com/software/aspen/
+                printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal)
+            else
+                printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)
+            end
+        end
+
+        # Restore the return status of the previous command.
+        echo "exit $old_status" | .
+        _old_fish_prompt
+    end
+
+    set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+end

+ 10 - 0
venv/bin/dxfaudit

@@ -0,0 +1,10 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from ezdxf.audit.__main__ import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 10 - 0
venv/bin/dxfpp

@@ -0,0 +1,10 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from ezdxf.pp.__main__ import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 12 - 0
venv/bin/easy_install

@@ -0,0 +1,12 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# EASY-INSTALL-ENTRY-SCRIPT: 'setuptools==40.8.0','console_scripts','easy_install'
+__requires__ = 'setuptools==40.8.0'
+import re
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(
+        load_entry_point('setuptools==40.8.0', 'console_scripts', 'easy_install')()
+    )

+ 12 - 0
venv/bin/easy_install-3.7

@@ -0,0 +1,12 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# EASY-INSTALL-ENTRY-SCRIPT: 'setuptools==40.8.0','console_scripts','easy_install-3.7'
+__requires__ = 'setuptools==40.8.0'
+import re
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(
+        load_entry_point('setuptools==40.8.0', 'console_scripts', 'easy_install-3.7')()
+    )

+ 12 - 0
venv/bin/pip

@@ -0,0 +1,12 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# EASY-INSTALL-ENTRY-SCRIPT: 'pip==19.0.3','console_scripts','pip'
+__requires__ = 'pip==19.0.3'
+import re
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(
+        load_entry_point('pip==19.0.3', 'console_scripts', 'pip')()
+    )

+ 12 - 0
venv/bin/pip3

@@ -0,0 +1,12 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# EASY-INSTALL-ENTRY-SCRIPT: 'pip==19.0.3','console_scripts','pip3'
+__requires__ = 'pip==19.0.3'
+import re
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(
+        load_entry_point('pip==19.0.3', 'console_scripts', 'pip3')()
+    )

+ 12 - 0
venv/bin/pip3.7

@@ -0,0 +1,12 @@
+#!/home/bscheibel/PycharmProjects/dxf_reader/venv/bin/python
+# EASY-INSTALL-ENTRY-SCRIPT: 'pip==19.0.3','console_scripts','pip3.7'
+__requires__ = 'pip==19.0.3'
+import re
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(
+        load_entry_point('pip==19.0.3', 'console_scripts', 'pip3.7')()
+    )

BIN
venv/bin/python


BIN
venv/bin/python3


BIN
venv/bin/python3.7


+ 2 - 0
venv/lib/python3.7/site-packages/easy-install.pth

@@ -0,0 +1,2 @@
+./setuptools-40.8.0-py3.7.egg
+./pip-19.0.3-py3.7.egg

+ 1 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/INSTALLER

@@ -0,0 +1 @@
+pip

+ 47 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/LICENSE.txt

@@ -0,0 +1,47 @@
+MIT License
+
+Copyright (c) 2011-2018, Manfred Moitzi
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+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 AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+
+Deutsche Übersetzung:
+
+Copyright (c) 2011-2018, Manfred Moitzi
+
+Hiermit wird unentgeltlich, jeder Person, die eine Kopie der Software 
+und der zugehörigen Dokumentationen (die "Software") erhält, die 
+Erlaubnis erteilt, uneingeschränkt zu benutzen, inklusive und ohne 
+Ausnahme, dem Recht, sie zu verwenden, kopieren, ändern, fusionieren, 
+verlegen, verbreiten, unterlizenzieren und/oder zu verkaufen, und 
+Personen, die diese Software erhalten, diese Rechte zu geben, unter den 
+folgenden Bedingungen: 
+
+Der obige Urheberrechtsvermerk und dieser Erlaubnisvermerk sind in allen 
+Kopien oder Teilkopien der Software beizulegen. 
+
+DIE SOFTWARE WIRD OHNE JEDE AUSDRÜCKLICHE ODER IMPLIZIERTE GARANTIE 
+BEREITGESTELLT, EINSCHLIESSLICH DER GARANTIE ZUR BENUTZUNG FÜR DEN 
+VORGESEHENEN ODER EINEM BESTIMMTEN ZWECK SOWIE JEGLICHER 
+RECHTSVERLETZUNG, JEDOCH NICHT DARAUF BESCHRÄNKT. IN KEINEM FALL SIND 
+DIE AUTOREN ODER COPYRIGHTINHABER FÜR JEGLICHEN SCHADEN ODER SONSTIGE 
+ANSPRÜCHE HAFTBAR ZU MACHEN, OB INFOLGE DER ERFÜLLUNG EINES VERTRAGES, 
+EINES DELIKTES ODER ANDERS IM ZUSAMMENHANG MIT DER SOFTWARE ODER 
+SONSTIGER VERWENDUNG DER SOFTWARE ENTSTANDEN. 
+

+ 394 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/METADATA

@@ -0,0 +1,394 @@
+Metadata-Version: 2.1
+Name: ezdxf
+Version: 0.9
+Summary: A Python package to create/manipulate DXF drawings.
+Home-page: https://ezdxf.mozman.at
+Author: Manfred Moitzi
+Author-email: me@mozman.at
+License: MIT License
+Download-URL: https://pypi.org/project/ezdxf/
+Keywords: DXF,CAD
+Platform: OS Independent
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Intended Audience :: Developers
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Provides: ezdxf
+Requires-Python: >=3.5
+Description-Content-Type: text/markdown
+Requires-Dist: pyparsing (>=2.0.1)
+
+ezdxf
+=====
+
+Abstract
+--------
+
+A Python package to create and modify DXF drawings, independent from the DXF
+version. You can open/save every DXF file without losing any content (except comments),
+Unknown tags in the DXF file will be ignored but preserved for saving. With this behavior
+it is possible to open also DXF drawings that contains data from 3rd party applications.
+
+Quick-Info
+----------
+
+- ezdxf is a Python package to create new DXF files and read/modify/write existing DXF files
+- the intended audience are developers
+- requires at least CPython 3.5, for Python 2 support use ezdxf < 0.9
+- OS independent
+- additional required packages: [pyparsing](https://pypi.org/project/pyparsing/)
+- MIT-License
+- read/write/new support for DXF versions: R12, R2000, R2004, R2007, R2010, R2013 and R2018
+- additional read support for DXF versions R13/R14 (upgraded to R2000)
+- additional read support for older DXF versions than R12 (upgraded to R12)
+- preserves third-party DXF content
+- additional fast DXF R12 writer, that creates just an ENTITIES section with support for the basic DXF entities
+
+a simple example:
+
+```python
+import ezdxf
+
+drawing = ezdxf.new(dxfversion='R2010')  
+# alternative:  use the AutoCAD release name 
+# ezdxf.new(dxfversion='R2010')
+modelspace = drawing.modelspace()
+modelspace.add_line((0, 0), (10, 0), dxfattribs={'color': 7})
+drawing.layers.new('TEXTLAYER', dxfattribs={'color': 2})
+
+# use set_pos() for proper TEXT alignment:
+# the relations between halign, valign, insert and align_point are tricky.
+modelspace.add_text(
+    'Test', 
+    dxfattribs={
+        'layer': 'TEXTLAYER'
+    }).set_pos((0, 0.2), align='CENTER')
+
+drawing.saveas('test.dxf')
+```
+
+example for the *r12writer*, writes a simple DXF R12 file without in-memory structures:
+
+```python
+from random import random
+from ezdxf.r12writer import r12writer
+
+MAX_X_COORD = 1000.0
+MAX_Y_COORD = 1000.0
+CIRCLE_COUNT = 100000
+
+with r12writer("many_circles.dxf") as dxf:
+    for i in range(CIRCLE_COUNT):
+        dxf.add_circle((MAX_X_COORD*random(), MAX_Y_COORD*random()), radius=2)
+```
+
+The r12writer supports only the ENTITIES section of a DXF R12 drawing, no HEADER, TABLES or BLOCKS section is
+present, except FIXED-TABLES are written, than some additional predefined text styles and line types are available.
+
+Installation
+------------
+
+Install with pip:
+
+    pip install ezdxf
+
+Install develop version (only if you have to)::
+
+    pip install git+https://github.com/mozman/ezdxf.git@develop
+
+For Python 2 users:
+
+    pip install ezdxf<0.9
+
+
+or from source:
+
+    python setup.py install
+
+Website
+-------
+
+https://ezdxf.mozman.at/
+
+Documentation
+-------------
+
+Documentation of development version at https://ezdxf.mozman.at/docs
+
+Documentation of latest release at http://ezdxf.readthedocs.io/
+
+Contribution
+------------
+
+The source code of ezdxf can be found at GitHub.com:
+
+http://github.com/mozman/ezdxf.git
+
+Only pull requests for the **develop** branch will be accepted.
+
+Feedback
+--------
+
+Issue Tracker at:
+
+http://github.com/mozman/ezdxf/issues
+
+Questions and Feedback at Google Groups:
+
+https://groups.google.com/d/forum/python-ezdxf
+
+python-ezdxf@googlegroups.com
+
+Feedback is greatly appreciated.
+
+Manfred
+
+Contact
+-------
+
+ezdxf@mozman.at
+
+News
+====
+
+Version 0.9 - 2019-02-24
+------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-9.html
+- IMPORTANT: Python 2 support REMOVED, if Python 2 support needed: add `ezdxf<0.9` to your `requirements.txt` 
+- NEW: testing on Manjaro Linux in a VM by tox
+- CHANGE: converted NEWS.rst to NEWS.md and README.rst to README.md  
+- CHANGE: moved `Importer()` from `ezdxf.tools` to `ezdxf.addons` - internal structures of modern DXF files are too complex
+  and too undocumented to support importing data in a reliable way - using `Importer()` may corrupt your DXF files or just 
+  don't work!
+- NEW: type annotations to core package and add-ons.
+- NEW: argument `setup` in `ezdxf.new('R12', setup=True)` to setup default line types, text styles and dimension styles, 
+  this feature is disabled by default.
+- NEW: Duplicate table entries: `dwg.styles.duplicate_entry('OpenSans', new_name='OpenSansNew')`, this works for 
+  all tables, but is intended to duplicate STYLES and DIMSTYLES.
+- CHANGED: replaced proprietary fonts in style declarations by open source fonts
+- NEW: open source fonts to download https://github.com/mozman/ezdxf/tree/master/fonts
+- __OpenSansCondensed-Light__ font used for default dimension styles
+- NEW: subpackage `ezdxf.render`, because of DIMENSION rendering
+- NEW: support for AutoCAD standard arrows
+- NEW: support for creating linear DIMENSION entities
+- NEW: background color support for MTEXT
+- CHANGE: DXF template cleanup, removed non standard text styles, dimension styles, layers and blocks
+- CHANGE: text style STANDARD uses `txt` font 
+- CHANGE: renamed subpackage `ezdxf.algebra` to `ezdxf.math`
+- CHANGE: moved `addons.curves` to `render.curves`
+- CHANGE: moved `addons.mesh` to `render.mesh`
+- CHANGE: moved `addons.r12spline` to `render.r12spline`
+- CHANGE: moved `addons.forms` to `render.forms`
+- CHANGE: renamed construction helper classes into Construction...()
+  - `Ray2D()` renamed to `ConstructionRay()`
+  - `Circle()` renamed to `ConstructionCircle()`
+  - `Arc()` renamed to `ConstructionArc()`
+- NEW: construction tools `ConstructionLine()` and `ConstructionBox()`
+- REMOVED: `almost_equal` use `math.isclose`
+- REMOVED: `almost_equal_points` use `ezdxf.math.is_close_points`
+- BUGFIX: closed LWPOLYLINE did not work in AutoCAD (tag order matters), introduced with v0.8.9 packed data structure
+- BUGFIX: `UCS.to_ocs_angle_deg()` corrected
+
+Version 0.8.9 - 2018-11-28
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-9.html
+- IMPORTANT: Python 2 support will be dropped in ezdxf v0.9.0, because Python 2 support get more and more annoying.
+- CHANGE: refactoring of internal tag representation for a smaller memory footprint, but with some speed penalty
+- NEW: packed data for LWPOLYLINE points, faster `__getitem__`;  added `__setitem__`, `__delitem__`, `insert()` and 
+  `append()` methods; renamed `discard_points()` in `clear()`; removed `get_rstrip_points()` and ctx manager 
+  `rstrip_points()`; user defined point format;
+- NEW: packed data for SPLINE, knots, weights, fit- and control points are stored as `array.array()`;
+  `Spline.get_knot_values()`, `Spline.get_weights()`, `Spline.get_control_points()` and `Spline.get_fit_points()` are 
+  deprecated, direct access to this attributes by `Spline.knot_values`, `Spline.weights`, `Spline.control_points` and 
+  `Spline.fit_points`, all attributes with a list-like interface. Knot, control point and fit point counter updated 
+  automatically, therefore counters are read only now.
+- NEW: packed data for MESH, vertices, faces, edges and edge crease values stored as `array.array()`, high level interface unchanged
+- NEW: `Drawing.layouts_and_blocks()`, iterate over all layouts (mode space and paper space) and all block definitions.
+- NEW: `Drawing.chain_layouts_and_blocks()`, chain entity spaces of all layouts and blocks. Yields an iterator for all
+  entities in all layouts and blocks
+- NEW: `Drawing.query()`, entity query over all layouts and blocks
+- NEW: `Drawing.groupby()`, groups DXF entities of all layouts and blocks by an DXF attribute or a key function
+- NEW: `Layout.set_redraw_order()` and `Layout.get_redraw_order()`, to change redraw order of entities in model space and
+  paper space layouts
+- NEW: `BlockLayout.is_layout_block`, `True` if block is a model space or paper space block definition
+- NEW: `ezdxf.algebra.Arc` helper class to create arcs from 2 points and an angle or radius, or from 3 points
+- NEW: `ezdxf.algebra.Arc.add_to_layout()` with UCS support to create 3D arcs
+- NEW: rename paper space layouts by `Drawing.layouts.rename(old_name, new_name)`
+- NEW: Basic support for embedded objects (new in AutoCAD 2018), ezdxf reads and writes the embedded data as it is,
+  no interpretation no modification, just enough to not break DXF files with embedded objects at saving.
+- CHANGE: `Drawing.blocks.delete_block(name, safe=True)`, new parameter save, check if block is still referenced
+  (raises `DXFValueError`)
+- CHANGE: `Drawing.blocks.delete_all_blocks(safe=True)`, if parameter safe is `True`, do not delete blocks that are still referenced
+- BUGFIX: invalid CLASS definition for DXF version R2000 (AC1015) fixed, bug was only triggered at upgrading from R13/R14 to R2000
+- BUGFIX: fixed broken `Viewport.AcDbViewport` property
+- __BASIC__ read support for many missing DXF entities/objects
+
+    - ACAD_PROXY_GRAPHIC
+    - HELIX
+    - LEADER
+    - LIGHT
+    - MLEADER (incomplete)
+    - MLINE (incomplete)
+    - OLEFRAME
+    - OLE2FRAME
+    - SECTION
+    - TABLE (incomplete)
+    - TOLERANCE
+    - WIPEOUT
+    - ACAD_PROXY_OBJECT
+    - DATATABLE
+    - DICTIONARYVAR
+    - DIMASSOC
+    - FIELD (incomplete)
+    - FIELDLIST (not documented by Autodesk)
+    - IDBUFFER
+    - LAYER_FILTER
+    - MATERIAL
+    - MLEADERSTYLE
+    - MLINESTYLE
+    - SORTENTSTABLE
+    - SUN
+    - SUNSTUDY (incomplete) (no real world DXF files with SUNSTUDY for testing available)
+    - TABLESTYLE (incomplete)
+    - VBA_PROJECT (no real world DXF files with embedded VBA for testing available)
+    - VISUALSTYLE
+    - WIPEOUTVARIABLES
+    - for all unsupported entities/objects exist only raw DXF tag support
+
+Version 0.8.8 - 2018-04-02
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-8.html
+- NEW: read/write support for GEODATA entity
+- NEW: read/(limited)write support for SURFACE, EXTRUDEDSURFACE, REVOLVEDSURFACE, LOFTEDSURFACE and SWEPTSURFACE entity
+- NEW: support for extension dictionaries
+- NEW: `add_spline_control_frame()`, create and add B-spline control frame from fit points
+- NEW: `add_spline_approx()`, approximate B-spline by a reduced count of control points
+- NEW: `ezdxf.setup_linetypes(dwg)`, setup standard line types
+- NEW: `ezdxf.setup_styles(dwg)`, setup standard text styles
+- NEW: `LWPolyline.vertices()` yields all points as `(x, y)` tuples in OCS, `LWPolyline.dxf.elevation` is the z-axis value
+- NEW: `LWPolyline.vertices_in_wcs()` yields all points as `(x, y, z)` tuples in WCS
+- NEW: basic `__str__()`  and `__repr__()` support for DXF entities, returns just DXF type and handle
+- NEW: bulge related function in module `ezdxf.algebra.bulge`
+- NEW: Object Coordinate System support by `DXFEntity.ocs()` and `OCS()` class in module ezdxf.algebra
+- NEW: User Coordinate System support by `UCS()` class in module `ezdxf.algebra`
+- CHANGE: `DXFEntity.set_app_data()` and `Entity.set_xdata` accept also list of tuples as tags, `DXFTag()` is not required
+- BUGFIX: entity structure validator excepts group code >= 1000 before XDATA section (used in AutoCAD Civil 3D and AutoCAD Map 3D)
+
+Version 0.8.7 - 2018-03-04
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-7.html
+- NEW: entity.get_layout() returns layout in which entity resides or None if unassigned
+- NEW: copy any DXF entity by entity.copy() without associated layout, add copy to any layout you want, by
+  layout.add_entity().
+- NEW: copy entity to another layout by entity.copy_to_layout(layout)
+- NEW: move entity from actual layout to another layout by entity.move_to_layout(layout)
+- NEW: support for splines by control points: add_open_spline(), add_closed_spline(), add_rational_spline(),
+  add_closed_rational_spline()
+- NEW: bspline_control_frame() calculates B-spline control points from fit points, but not the same as AutoCAD
+- NEW: R12Spline add-on, 2d B-spline with control frame support by AutoCAD, but curve is just an approximated POLYLINE
+- NEW: added entity.get_flag_state() and entity.set_flag_state() for easy access to binary coded flags
+- NEW: set new $FINGERPRINTGUID for new drawings
+- NEW: set new $VERSIONGUID on saving a drawing
+- NEW: improved IMAGE support, by adding RASTERVARIABLES entity, use Drawing.set_raster_variables(frame, quality, units)
+- BUGFIX: closing user defined image boundary path automatically, else AutoCAD crashes
+
+Version 0.8.6 - 2018-02-17
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-6.html
+- NEW: ezdxf project website: https://ezdxf.mozman.at/
+- CHANGE: create all missing tables of the TABLES sections for DXF R12
+- BUGFIX: entities on new layouts will be saved
+- NEW: Layout.page_setup() and correct 'main' viewport for DXF R2000+; For DXF R12 page_setup() exists, but does not
+  provide useful results. Page setup for DXF R12 is still a mystery to me.
+- NEW: Table(), MText(), Ellipse(), Spline(), Bezier(), Clothoid(), LinearDimension(), RadialDimension(),
+  ArcDimension() and AngularDimension() composite objects from dxfwrite as add-ons, these add-ons support DXF R12
+- NEW: geometry builder as add-ons: MeshBuilder(), MeshVertexMerger(), MengerSponge(), SierpinskyPyramid(), these
+  add-ons require DXF R2000+ (MESH entity)
+- BUGFIX: fixed invalid implementation of context manager for r12writer
+
+Version 0.8.5 - 2018-01-28
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-5.html
+- CHANGE: block names are case insensitive 'TEST' == 'Test' (like AutoCAD)
+- CHANGE: table entry (layer, linetype, style, dimstyle, ...) names are case insensitive 'TEST' == 'Test' (like AutoCAD)
+- CHANGE: raises DXFInvalidLayerName() for invalid characters in layer names: <>/\":;?*|=`
+- CHANGE: audit process rewritten
+- CHANGE: skip all comments, group code 999
+- CHANGE: removed compression for unused sections (THUMBNAILSECTION, ACDSDATA)
+- NEW: write DXF R12 files without handles: set dwg.header['$HANDLING']=0, default value is 1
+- added subclass marker filter for R12 and prior files in legacy_mode=True (required for malformed DXF files)
+- removed special check for Leica Disto Unit files, use readfile(filename, legacy_mode=True) (malformed DXF R12 file,
+  see previous point)
+
+Version 0.8.4 - 2018-01-14
+--------------------------
+
+- Release notes: https://ezdxf.mozman.at/release-v0-8-4.html
+- NEW: Support for complex line types with text or shapes
+- NEW: DXF file structure validator at SECTION level, tags outside of sections will be removed
+- NEW: Basic read support for DIMENSION
+- CHANGE: improved exception management, in the future ezdxf should only raise exceptions inherited from DXFError for
+  DXF related errors, previous exception classes still work
+
+    - DXFValueError(DXFError, ValueError)
+    - DXFKeyError(DXFError, KeyError)
+    - DXFAttributeError(DXFError, AttributeError)
+    - DXFIndexError(DXFError, IndexError)
+    - DXFTableEntryError(DXFValueError)
+
+- speedup low level tag reader around 5%, and speedup tag compiler around 5%
+
+Version 0.8.3 - 2018-01-02
+--------------------------
+
+- CHANGE: Lwpolyline - suppress yielding z coordinates if they exists (DXFStructureError: z coordinates are not defined in the DXF standard)
+- NEW: setup creates a script called 'dxfpp' (DXF Pretty Printer) in the Python script folder
+- NEW: basic support for DXF format AC1032 introduced by AutoCAD 2018
+- NEW: ezdxf use logging and writes all logs to a logger called 'ezdxf'. Logging setup is the domain of the application!
+- NEW: warns about multiple block definitions with the same name in a DXF file. (DXFStructureError)
+- NEW: legacy_mode parameter in ezdxf.read() and ezdxf.readfile(): tries do fix coordinate order in LINE
+  entities (10, 11, 20, 21) by the cost of around 5% overall speed penalty at DXF file loading
+
+Version 0.8.2 - 2017-05-01
+--------------------------
+
+- NEW: Insert.delete_attrib(tag) - delete ATTRIB entities from the INSERT entity
+- NEW: Insert.delete_all_attribs() - delete all ATTRIB entities from the INSERT entity
+- BUGFIX: setting attribs_follow=1 at INSERT entity before adding an attribute entity works
+
+Version 0.8.1 - 2017-04-06
+--------------------------
+
+- NEW: added support for constant ATTRIB/ATTDEF to the INSERT (block reference) entity
+- NEW: added ATTDEF management methods to BlockLayout (has_attdef, get_attdef, get_attdef_text)
+- NEW: added (read/write) properties to ATTDEF/ATTRIB for setting flags (is_const, is_invisible, is_verify, is_preset)
+
+Version 0.8.0 - 2017-03-28
+--------------------------
+
+- added groupby(dxfattrib='', key=None) entity query function, it is supported by all layouts and the query result
+  container: Returns a dict, where entities are grouped by a dxfattrib or the result of a key function.
+- added ezdxf.audit() for DXF error checking for drawings created by ezdxf - but not very capable yet
+- dxfattribs in factory functions like add_line(dxfattribs=...), now are copied internally and stay unchanged, so they
+  can be reused multiple times without getting modified by ezdxf.
+- removed deprecated Drawing.create_layout() -> Drawing.new_layout()
+- removed deprecated Layouts.create() -> Layout.new()
+- removed deprecated Table.create() -> Table.new()
+- removed deprecated DXFGroupTable.add() -> DXFGroupTable.new()
+- BUFIX in EntityQuery.extend()
+
+
+

+ 373 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/RECORD

@@ -0,0 +1,373 @@
+../../../bin/dxfaudit,sha256=VipVuUWz9si0V12-lDBGfet7xhTBYP4rsPW6csEwZj4,264
+../../../bin/dxfpp,sha256=5JsAbFoDC6O63U2ywUC9Y7JmfaHqjn2mBLV7fjboKqs,261
+ezdxf-0.9.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+ezdxf-0.9.dist-info/LICENSE.txt,sha256=rEy5oKMdqe5Or6pqipbEw23gey0AECJ0FPhmt3vWG4U,2298
+ezdxf-0.9.dist-info/METADATA,sha256=U9SpChnCdJrJE4Nzw5ZYMGdieCnLu2LeOg23wcXdHRk,18080
+ezdxf-0.9.dist-info/RECORD,,
+ezdxf-0.9.dist-info/WHEEL,sha256=P3qJ5yBP-M5n4VbrgU3eTDIHvegviaBxiqCi1ejHWOs,97
+ezdxf-0.9.dist-info/entry_points.txt,sha256=FmZDMQlK-wesxG0ck98D1zNZjhlODyQr2_MeblPV2ME,87
+ezdxf-0.9.dist-info/top_level.txt,sha256=jDpbvSz92tkSe1t8mGnMMWQp6VOaxtRxpmcpNEWe6IM,6
+ezdxf/__init__.py,sha256=HNPDlGSu6WVsbl4HcPioohdyblyaLuoC-TnxjMqJBl8,1533
+ezdxf/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/__pycache__/acadctb.cpython-37.pyc,,
+ezdxf/__pycache__/database.cpython-37.pyc,,
+ezdxf/__pycache__/dimstyleoverride.cpython-37.pyc,,
+ezdxf/__pycache__/drawing.cpython-37.pyc,,
+ezdxf/__pycache__/dxfentity.cpython-37.pyc,,
+ezdxf/__pycache__/dxffactory.cpython-37.pyc,,
+ezdxf/__pycache__/entityspace.cpython-37.pyc,,
+ezdxf/__pycache__/eztypes.cpython-37.pyc,,
+ezdxf/__pycache__/filemanagement.cpython-37.pyc,,
+ezdxf/__pycache__/graphicsfactory.cpython-37.pyc,,
+ezdxf/__pycache__/groupby.cpython-37.pyc,,
+ezdxf/__pycache__/options.cpython-37.pyc,,
+ezdxf/__pycache__/query.cpython-37.pyc,,
+ezdxf/__pycache__/queryparser.cpython-37.pyc,,
+ezdxf/__pycache__/r12writer.cpython-37.pyc,,
+ezdxf/__pycache__/tracker.cpython-37.pyc,,
+ezdxf/__pycache__/units.cpython-37.pyc,,
+ezdxf/__pycache__/version.cpython-37.pyc,,
+ezdxf/acadctb.py,sha256=ngEiFdVmmAzIdB7iFJVJSEYP0ElYxKicVLpmD671xf8,19632
+ezdxf/addons/__init__.py,sha256=PZ_5ziu6GdfmdeUTM47_SkoMteXppkddtItneR3olb8,377
+ezdxf/addons/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/addons/__pycache__/dimlines.cpython-37.pyc,,
+ezdxf/addons/__pycache__/importer.cpython-37.pyc,,
+ezdxf/addons/__pycache__/menger_sponge.cpython-37.pyc,,
+ezdxf/addons/__pycache__/mixins.cpython-37.pyc,,
+ezdxf/addons/__pycache__/mtext.cpython-37.pyc,,
+ezdxf/addons/__pycache__/sierpinski_pyramid.cpython-37.pyc,,
+ezdxf/addons/__pycache__/table.cpython-37.pyc,,
+ezdxf/addons/dimlines.py,sha256=w7Njtyi_xS6AeorU2G0l3HUoOnKAWqPX15AbVMKLl2c,26660
+ezdxf/addons/importer.py,sha256=WCpW6Q9W8eJAD25tvJQL9B6lzg1rlKzPZyt4TJEVA2c,13382
+ezdxf/addons/menger_sponge.py,sha256=4WH_w-XCw7wDmqgnTR-nRfc298h6JfyQPPHd8n5OlHs,7442
+ezdxf/addons/mixins.py,sha256=qLtfbsM9C1RdcSUbx7NkoY01A3zCUhXIkFKCXKp2FQU,492
+ezdxf/addons/mtext.py,sha256=9ucI53wTwjTJS4u2kocSkSLVU3VqBfsBIvFCxJ68VpU,5707
+ezdxf/addons/sierpinski_pyramid.py,sha256=2-u8VTkNhOoHovZ83RH8ukS6v6eRjP2lrYr0_zcttRo,7331
+ezdxf/addons/table.py,sha256=xki732HICHSIzXEgtUDX7bSGi3N0Ui7OODq-aTSBCPY,30447
+ezdxf/audit/__init__.py,sha256=UaGoyJQ6tfsef-Rgl88o4XO1OmiAzOOnRljRNBxPfZ4,193
+ezdxf/audit/__main__.py,sha256=nYu-B7pOTUeSDZPvubz6OiRyf7jTYwmrMVm9U7gf3xA,1941
+ezdxf/audit/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/audit/__pycache__/__main__.cpython-37.pyc,,
+ezdxf/audit/__pycache__/auditor.cpython-37.pyc,,
+ezdxf/audit/auditor.py,sha256=OAIq-nEus8bxl5oW00wckgYvaBeIfL93RtghUV-AYWI,10934
+ezdxf/database.py,sha256=ERdnpbe2vxw5WN-XB_vwYCxeRUE0XUeUIus6FPAoMrU,4777
+ezdxf/dimstyleoverride.py,sha256=-WVCtqHroMCnl2v1kJb4OEpBy-tQZyEUByF7wZIckA4,19576
+ezdxf/drawing.py,sha256=yTjN0HViDyjhbJSJNHtzX-1y7nTOVAY6GzR7iY_yxIg,20623
+ezdxf/dxfentity.py,sha256=DnMwcF-ha3S4hiG2i3HqdYHV21ImUGKMpGGCAmVc-hs,17930
+ezdxf/dxffactory.py,sha256=lb6wzoywAKVvIyqCcSiQy3YiPOVn_5IGtjy9_KO1MJM,536
+ezdxf/entityspace.py,sha256=SWH3FXKUp5ATu3vTxx58PrG_uxiCKR8kIXiKxvpJMAw,1544
+ezdxf/eztypes.py,sha256=42jC1bitNOYjBnc8sjzHwcH-BJiPzDOuvU5RFc0mcFQ,4605
+ezdxf/filemanagement.py,sha256=RLUKf1j4jFPYmTx4w6MDw39LsSuoNxKWKgq0yu7az-A,6688
+ezdxf/graphicsfactory.py,sha256=n4mNi4bAbGbqTieDuQ2mpL80d7FKZ2ySfHMHkwMwrLo,49110
+ezdxf/groupby.py,sha256=LzgOHGdxxHESN2LNz8btoaS5bsuSX_uo9VmpkzrwHOQ,1788
+ezdxf/legacy/__init__.py,sha256=lELAz3zIJ_qNdlfrKqfbppOc0Ixi6uR5MGZUX_w9QdM,175
+ezdxf/legacy/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/attrib.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/block.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/dimension.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/facemixins.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/factory.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/graphics.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/headervars.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/insert.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/layouts.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/polyfacebuilder.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/polyline.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/tableentries.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/text.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/trace.cpython-37.pyc,,
+ezdxf/legacy/__pycache__/viewport.cpython-37.pyc,,
+ezdxf/legacy/attrib.py,sha256=YrbnMTArP3cg7GclSq4hfVz7SVxp4zuW328DSa8N34g,4245
+ezdxf/legacy/block.py,sha256=xqQpr7tjvbhrMl5WoIoqvWSOVvNk7G9pvL6RGzCKnzQ,1942
+ezdxf/legacy/dimension.py,sha256=7n2-tjPzhItHKX1nx0kxfAOtuI6Wkruj_elGjFqTp4Q,7016
+ezdxf/legacy/facemixins.py,sha256=J7w3hl9xsC34yDu-KlvXTz9kGJqMQe9aCqf_Hmykhd0,9377
+ezdxf/legacy/factory.py,sha256=iFUhOZ9vcBnvjtV3_hw40fXn4G5AnOerGVLx5md2-ug,4771
+ezdxf/legacy/graphics.py,sha256=W50ajaJe0tIGeFsNQe9ZqoRtZ19IhnFvSjwenBqb8Wg,3889
+ezdxf/legacy/headervars.py,sha256=tBYczqS1BujFujJPD2slkB89_GDGkj2kzXvLppquZ_8,6460
+ezdxf/legacy/insert.py,sha256=Xq13simh-Uw9SWMW0026RENCljbXH0VEEShu7r-TpOw,9245
+ezdxf/legacy/layouts.py,sha256=obvxjmIaN5GPTRg3jjPVstYpWpw0fMI_0DHcxJNsXhU,19824
+ezdxf/legacy/polyfacebuilder.py,sha256=lgVDIJJ5GEKxvZyNac0fEMYlFhXDg3M9uSv0QGsDfqo,1796
+ezdxf/legacy/polyline.py,sha256=zKAgqhZktRZkS8HaAUO3igxlB8UGkS8hKrh1EnScpx0,11038
+ezdxf/legacy/tableentries.py,sha256=80RBBr59oswS_Pujp8OC-O8uUMXlSmEU0iK0fPsIMLI,23611
+ezdxf/legacy/text.py,sha256=h2Klj8uqKZB6dW3gmoeocTnlJCZTpUnB3znZSK6UewE,3101
+ezdxf/legacy/trace.py,sha256=9rlUXdu5d4_2T8FJX9CLVu-VHREzFNfX-PesuBhyyFo,1598
+ezdxf/legacy/viewport.py,sha256=vaL16BGp91sF4-ZD4KEiE0FTIWXgnOBp9MxNW3QWz_g,7450
+ezdxf/lldxf/__init__.py,sha256=gqkX3VP0mJJoSXNr_SK9GcYF_AtXfHS02aGow2qJc-E,119
+ezdxf/lldxf/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/attributes.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/const.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/encoding.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/extendedtags.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/hdrvars.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/loader.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/packedtags.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/repair.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/tagger.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/tags.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/tagwriter.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/types.cpython-37.pyc,,
+ezdxf/lldxf/__pycache__/validator.cpython-37.pyc,,
+ezdxf/lldxf/attributes.py,sha256=5jYeHiw8XCAPdraYEOCy4Sw71eYLxemTA_DqcDoxXqQ,9928
+ezdxf/lldxf/const.py,sha256=dq-QHCAcA7LQsNUHip6DTBpBx3wh5xJVk-C43z90Gl8,11000
+ezdxf/lldxf/encoding.py,sha256=lBcbtMBnmzZvUfEv550aAFNHz83-H2LarnQeGCZViDY,1130
+ezdxf/lldxf/extendedtags.py,sha256=cbkDZ_Gs8ZaPu_a932gNVVftOhh0lRfAiM3Cn1fcADs,14571
+ezdxf/lldxf/hdrvars.py,sha256=CA43pGL8inNkKF4X7sRNlnjtoAXoYGcEJ_PmUmmnstA,562
+ezdxf/lldxf/loader.py,sha256=_BgepTD6PB6fIoxW8IJmvU5NFFaY7i-5HoE4Xn0tFLA,6793
+ezdxf/lldxf/packedtags.py,sha256=GGRHOB1AjV36475hwQEyEA7sVi8qBlzfk2X9zCKCsMk,7406
+ezdxf/lldxf/repair.py,sha256=5yVaGTRrDBoEjSd4DANZtoBXL2NNOUiTFlP0tgf23Ns,13511
+ezdxf/lldxf/tagger.py,sha256=bozbNEMSV_DT8pOFaQFwYhd4jUDr1WXPMEEAd1GIRpk,6714
+ezdxf/lldxf/tags.py,sha256=qDuI5zYsO1Qg7u5oeEIqQUOnM8kUoMASq6uqA3dQ7Bc,9602
+ezdxf/lldxf/tagwriter.py,sha256=uBb86E7_-jjVLPiZbBrXVwUKNAaEjvFoTsoMDJxJ_-k,1468
+ezdxf/lldxf/types.py,sha256=zsTzeXT4-QShM2LzyH9eDScNoQvkumZNfdahrd-wS48,6111
+ezdxf/lldxf/validator.py,sha256=8vCALFt-pPzAvZzKbcMzylT89UZigXP83Otd7MQPQ3A,6412
+ezdxf/math/__init__.py,sha256=-0tNYhV-XIgKAFmc9L1TIL5iJH6Vxo16pwiZBOZ56d0,1765
+ezdxf/math/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/math/__pycache__/arc.cpython-37.pyc,,
+ezdxf/math/__pycache__/bbox.cpython-37.pyc,,
+ezdxf/math/__pycache__/bezier.cpython-37.pyc,,
+ezdxf/math/__pycache__/bezier4p.cpython-37.pyc,,
+ezdxf/math/__pycache__/box.cpython-37.pyc,,
+ezdxf/math/__pycache__/bspline.cpython-37.pyc,,
+ezdxf/math/__pycache__/bulge.cpython-37.pyc,,
+ezdxf/math/__pycache__/circle.cpython-37.pyc,,
+ezdxf/math/__pycache__/construct2d.cpython-37.pyc,,
+ezdxf/math/__pycache__/convexhull.cpython-37.pyc,,
+ezdxf/math/__pycache__/eulerspiral.cpython-37.pyc,,
+ezdxf/math/__pycache__/line.cpython-37.pyc,,
+ezdxf/math/__pycache__/matrix.cpython-37.pyc,,
+ezdxf/math/__pycache__/matrix44.cpython-37.pyc,,
+ezdxf/math/__pycache__/points.cpython-37.pyc,,
+ezdxf/math/__pycache__/shape.cpython-37.pyc,,
+ezdxf/math/__pycache__/surfaces.cpython-37.pyc,,
+ezdxf/math/__pycache__/ucs.cpython-37.pyc,,
+ezdxf/math/__pycache__/vector.cpython-37.pyc,,
+ezdxf/math/arc.py,sha256=2mgo22rYNXZ5O39iKDdT-AFA9hrbzds508tGR_SfrmQ,8991
+ezdxf/math/bbox.py,sha256=gY6G6dQK6LcZIKi3Ppw1JX6qTAA5U-goG5AO76_zO2w,2641
+ezdxf/math/bezier.py,sha256=5GCEpMMaLcdLklwr_eKuerCccZpuj_ros4OjPHJpae4,6750
+ezdxf/math/bezier4p.py,sha256=aciVCsh2ewGtFgmcxFH5x3j1rGxDgN68LHol51Jl9zw,5257
+ezdxf/math/box.py,sha256=QoJjEdK0a1pgB3VRxkTH6XRn1lsWqYjOwNZTKXlY-go,6360
+ezdxf/math/bspline.py,sha256=6UoIzMflRQ-UNmRfLYDsPIRJd4Zj3Z12x0FXDDHDJRA,32924
+ezdxf/math/bulge.py,sha256=CSVi2va2KyMWXT2S9wmg7WB35riS1jnHyjmy8MWLqU4,4349
+ezdxf/math/circle.py,sha256=XJwBlnp-cf4UabMqbOdCc-_AnolMqk_tvjnyJ_q5gYw,5411
+ezdxf/math/construct2d.py,sha256=PYBv1noVMr0Ugf92rDMALOJd2BoiwbN0mr2t5lPk14g,2820
+ezdxf/math/convexhull.py,sha256=NT7hQsohIlLFrjVj71tUXgD8eji0hdCVyzVo8aTQBk8,1304
+ezdxf/math/eulerspiral.py,sha256=tqiMoqp4SQpL31jQNFZNZPU3BoIapUA-gX5HuLEHDJs,3328
+ezdxf/math/line.py,sha256=sRv02O_MvVQg6_bg_bRfwDoOweCIHxucBAwEh3eCwQc,8572
+ezdxf/math/matrix.py,sha256=FFPvO_ozCaAT7ecGMTTiztc78L7xde-lzfKESqYTyDs,7126
+ezdxf/math/matrix44.py,sha256=TghgyqyaBYIHqTcN276q1I-kSm_f2Q_YDI1XRSrmfFY,18832
+ezdxf/math/points.py,sha256=tnOyzdGP_0nXClE6g94N1bHUe2RosTNlBsLP8w7zorU,557
+ezdxf/math/shape.py,sha256=i8jEoAKt3afKxNX4o7PRIqSa6dTkw6NAhLb9Y1ffPNk,2142
+ezdxf/math/surfaces.py,sha256=jkaPKGz-q3smaO7hJws_YGOiWqSb1YdtqToDkUYkKuU,1654
+ezdxf/math/ucs.py,sha256=dVsPdusc9nvCO1ntXk3fnUY_jcrKymjCMPYCwfr5cqk,10149
+ezdxf/math/vector.py,sha256=COXNl6nDmqFqC4M9zyvnuju8Ua0tSCC4eWlQ6_BM9oc,22398
+ezdxf/modern/__init__.py,sha256=6zbiFPM2Z4N-9tCsTlYfSgW1t33Z_25MeirDeMgTe1o,170
+ezdxf/modern/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/modern/__pycache__/acad_proxy_entity.cpython-37.pyc,,
+ezdxf/modern/__pycache__/acad_table.cpython-37.pyc,,
+ezdxf/modern/__pycache__/attrib.cpython-37.pyc,,
+ezdxf/modern/__pycache__/block.cpython-37.pyc,,
+ezdxf/modern/__pycache__/datatable.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dimassoc.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dimension.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dxfdict.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dxfgroups.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dxflayout.cpython-37.pyc,,
+ezdxf/modern/__pycache__/dxfobjects.cpython-37.pyc,,
+ezdxf/modern/__pycache__/ellipse.cpython-37.pyc,,
+ezdxf/modern/__pycache__/factory.cpython-37.pyc,,
+ezdxf/modern/__pycache__/field.cpython-37.pyc,,
+ezdxf/modern/__pycache__/fieldlist.cpython-37.pyc,,
+ezdxf/modern/__pycache__/geodata.cpython-37.pyc,,
+ezdxf/modern/__pycache__/graphics.cpython-37.pyc,,
+ezdxf/modern/__pycache__/hatch.cpython-37.pyc,,
+ezdxf/modern/__pycache__/headervars.cpython-37.pyc,,
+ezdxf/modern/__pycache__/helix.cpython-37.pyc,,
+ezdxf/modern/__pycache__/idbuffer.cpython-37.pyc,,
+ezdxf/modern/__pycache__/image.cpython-37.pyc,,
+ezdxf/modern/__pycache__/insert.cpython-37.pyc,,
+ezdxf/modern/__pycache__/layer_filter.cpython-37.pyc,,
+ezdxf/modern/__pycache__/layouts.cpython-37.pyc,,
+ezdxf/modern/__pycache__/leader.cpython-37.pyc,,
+ezdxf/modern/__pycache__/light.cpython-37.pyc,,
+ezdxf/modern/__pycache__/lwpolyline.cpython-37.pyc,,
+ezdxf/modern/__pycache__/material.cpython-37.pyc,,
+ezdxf/modern/__pycache__/matrix_accessors.cpython-37.pyc,,
+ezdxf/modern/__pycache__/mesh.cpython-37.pyc,,
+ezdxf/modern/__pycache__/mleader.cpython-37.pyc,,
+ezdxf/modern/__pycache__/mline.cpython-37.pyc,,
+ezdxf/modern/__pycache__/mtext.cpython-37.pyc,,
+ezdxf/modern/__pycache__/object_manager.cpython-37.pyc,,
+ezdxf/modern/__pycache__/oleframe.cpython-37.pyc,,
+ezdxf/modern/__pycache__/polyline.cpython-37.pyc,,
+ezdxf/modern/__pycache__/ray.cpython-37.pyc,,
+ezdxf/modern/__pycache__/section.cpython-37.pyc,,
+ezdxf/modern/__pycache__/solid3d.cpython-37.pyc,,
+ezdxf/modern/__pycache__/sortentstable.cpython-37.pyc,,
+ezdxf/modern/__pycache__/spline.cpython-37.pyc,,
+ezdxf/modern/__pycache__/sun.cpython-37.pyc,,
+ezdxf/modern/__pycache__/sunstudy.cpython-37.pyc,,
+ezdxf/modern/__pycache__/surface.cpython-37.pyc,,
+ezdxf/modern/__pycache__/tableentries.cpython-37.pyc,,
+ezdxf/modern/__pycache__/tablestyle.cpython-37.pyc,,
+ezdxf/modern/__pycache__/text.cpython-37.pyc,,
+ezdxf/modern/__pycache__/tolerance.cpython-37.pyc,,
+ezdxf/modern/__pycache__/trace.cpython-37.pyc,,
+ezdxf/modern/__pycache__/underlay.cpython-37.pyc,,
+ezdxf/modern/__pycache__/vba_project.cpython-37.pyc,,
+ezdxf/modern/__pycache__/viewport.cpython-37.pyc,,
+ezdxf/modern/__pycache__/visualstyle.cpython-37.pyc,,
+ezdxf/modern/__pycache__/wipeout.cpython-37.pyc,,
+ezdxf/modern/__pycache__/xrecord.cpython-37.pyc,,
+ezdxf/modern/acad_proxy_entity.py,sha256=ABe1xFjo2QJru8JBGIzL5ISIBSpeD83I3xnDd5BJSDo,1972
+ezdxf/modern/acad_table.py,sha256=G4V-_t5xI9NAui5tNAIR-SRP0nVmuRbJqFJ78x2sRXU,10611
+ezdxf/modern/attrib.py,sha256=gY-StoP-7OTvBZsN8C7_HYGj_bRX5DUHfLL3-T2U0lo,3354
+ezdxf/modern/block.py,sha256=pInYX0oDVqmv9hH2pz1kqJLHN0v5CzJ6lG2jomiPfA4,1425
+ezdxf/modern/datatable.py,sha256=G1eg5ELPhEIfbiyXPjqOqppRUIa2n0SZAJLM7rVL4Ts,2034
+ezdxf/modern/dimassoc.py,sha256=18skRpgEbSysHcKfKmTqJIaYA3_brxK5eTjOhv42Y9o,3152
+ezdxf/modern/dimension.py,sha256=R8H8NIXq46MTntVQy71moyJAsvIbLBmG0cKmg9qNiM0,11189
+ezdxf/modern/dxfdict.py,sha256=jIsieT6EDGmL2DQtoWafJqNNfxY7K-3MfL8K4CQ4R_o,9859
+ezdxf/modern/dxfgroups.py,sha256=J1oGwfxcKx82xQmqJN_PCZ6DS5Xju694ALZQLShFLOg,7007
+ezdxf/modern/dxflayout.py,sha256=k2X4tQjaP5mq31kCoM9RmaeK2HT4qKTU9UjFSruUzno,6226
+ezdxf/modern/dxfobjects.py,sha256=Tj1EEPUb0H_H1uhJfdMN9W4Bd_DEEg6SBrevLB4ktgI,1295
+ezdxf/modern/ellipse.py,sha256=kHATYU1jogeUtRYdPQc7C6_O2ouX1ZnTy_ppiJnVnDg,1067
+ezdxf/modern/factory.py,sha256=gbOTtJuuN3ELrj8V1foM3JzbYQF1n-o_2ycaFCN119Y,7776
+ezdxf/modern/field.py,sha256=-mnhwcSfP0n0dQzDRGGU5QwpAB9eG1jYnB_GWz4S9vg,1949
+ezdxf/modern/fieldlist.py,sha256=Gr1ji-KHbULcOHLG80NOmrd39-ChsR7yLsvLmInSU_o,1483
+ezdxf/modern/geodata.py,sha256=4yxq5vTWC5BWJdtyjXcWVGWM6_RYjJ9SJQbSeIW4Ki4,9061
+ezdxf/modern/graphics.py,sha256=XKYBkgWRG3lsVkoaJ9YXaCNOn1Pku4M30mmDSZsv2jU,7037
+ezdxf/modern/hatch.py,sha256=NLcya36omi2q_YFZht7OzF7xcN4g8vs_FA_Rmov4vGE,36297
+ezdxf/modern/headervars.py,sha256=72oM9LtWfgt58RW3UcISPWQIq2zWFO6Ln_fE7PwXIvk,12173
+ezdxf/modern/helix.py,sha256=bcDDqVIWToyiZrsy79B0eRb7MR_dlcOlkwis9temXjg,1503
+ezdxf/modern/idbuffer.py,sha256=NULXHqcU-Y7HTsuCN6DQ39u2qG2cSPLzxJT0kH6Tg08,3511
+ezdxf/modern/image.py,sha256=0Vhd7k1SMeug3iIw7y_rvxu25d9-qllYaFMgzkNYrm4,7613
+ezdxf/modern/insert.py,sha256=8X1y46LTpG_LXFV6cuVXrhZtt7z1iSezkuWzODr4XCA,1292
+ezdxf/modern/layer_filter.py,sha256=jR2rQy8JnQWiU9ItoPJtQ1O90gyREixpU-wJkCbOaQA,1706
+ezdxf/modern/layouts.py,sha256=qOvSKl9V5rbnpSt7sIXpLt9cU62qLcjKT3gEebFyrDA,35765
+ezdxf/modern/leader.py,sha256=uyJmPUmZeBVaBacd0fzhXAfDxjwJ5ALOMO_ED1wUGck,3057
+ezdxf/modern/light.py,sha256=_n54d1nje269ex4OztrQZDuSw_UZXaWcX2iukI2ZEpg,1974
+ezdxf/modern/lwpolyline.py,sha256=vWHeQHkOdyQ2cYVXG34jVm-KIATNGAUidWvLdNRp1dw,11425
+ezdxf/modern/material.py,sha256=Q0DhZQ6ZfYiLc3umj69sKmu9FQa1B3X0S2_fIRtml2s,11932
+ezdxf/modern/matrix_accessors.py,sha256=fVMFpraxBjlflHl_ouM_471fl_wHH-tq3SveaSRFt-I,1014
+ezdxf/modern/mesh.py,sha256=cS68Cof9Iu7Rb679HNmsRa4yUz29TtA35urqImWNFRo,12882
+ezdxf/modern/mleader.py,sha256=-Mls6dnRqp7fiB-me_oUGupfuOUo1JiZ1jYw_mhgDE8,4591
+ezdxf/modern/mline.py,sha256=U-yiETjwh53cC9dFC3rNzMwzqXuWG4sABgkeIBWSkmE,7027
+ezdxf/modern/mtext.py,sha256=m_FH_OHrAHijpztV7RgmhdVZ0Klk5bs596c3zC5N_1w,9534
+ezdxf/modern/object_manager.py,sha256=5uvSsMW7nGKQZDVFsX5eOVhrOOQ2thm7UMFGte40cDg,2767
+ezdxf/modern/oleframe.py,sha256=IBPU8CuuiIGDl6RWgjj862v5uZNAaD04jQq7lgmYe9E,1451
+ezdxf/modern/polyline.py,sha256=o2FyXjwYbBbqjmxpfVyR0ORab_rtTPe7sOp6huVgU_w,5468
+ezdxf/modern/ray.py,sha256=iIcDqG6i9xOirs4I1TNhvTJ8k7X7hGLfh7fOHDO3rLk,828
+ezdxf/modern/section.py,sha256=8XJWrzLS2j02JbnyLVHXZ-Ad5J1U6QlNR3MVmUSPdZ8,1820
+ezdxf/modern/solid3d.py,sha256=Iha8_WXXXfvjQdtQavqIPR2BFhpwhm4WWBDA0bkJWz0,3559
+ezdxf/modern/sortentstable.py,sha256=ygHT2mUTczus5Qi71rlOG4rumDaUWxMG_aH3ALf5Inw,5368
+ezdxf/modern/spline.py,sha256=cwFz48eE7ha6WEISKFajfvUufgyXuNYnQDycjl5MWlk,11356
+ezdxf/modern/sun.py,sha256=TJyxl-4S8MZ-HQ0hbReIuClcnW_o3iV_uU_vzeW2tIM,1233
+ezdxf/modern/sunstudy.py,sha256=9BagdvKBkiPsYrLCP0dLpGSFjWfnI3BtxWsSLOnScqU,2122
+ezdxf/modern/surface.py,sha256=BvcT9ywFXCHaUY_ZjRDx1-o6X6NoyY91xMdimd2NMsM,14840
+ezdxf/modern/tableentries.py,sha256=nzW8oNR6VHRiKdWhT0Vuh58ZqdUNDvwMfpunfozrzqY,20197
+ezdxf/modern/tablestyle.py,sha256=igVgqwjTHdi8JBxPx6-SPStX139EekoX5cKTydF_g6Q,2667
+ezdxf/modern/text.py,sha256=KRhoM5GvBpkMXa1aQdRORTw6kchMGe3jm9RQJVM3gig,1630
+ezdxf/modern/tolerance.py,sha256=4pjkuh9PLllzd87tci50vHVU05mZFDc77yg04uL8jrM,1077
+ezdxf/modern/trace.py,sha256=RXTgpnnfahfKOYlTTmWTUn8gDm_ytuqd_Qh2IN-XDcc,1894
+ezdxf/modern/underlay.py,sha256=T5J6eVZDKaqlEx2jJzcmngskrlRHelKe2sreXXY6f-s,7342
+ezdxf/modern/vba_project.py,sha256=UUwHVE8qRtxV80o3abpPiR-Twn_v1vWKWyP49ZF3-88,1684
+ezdxf/modern/viewport.py,sha256=YD6nBbpnJvogQrhINVP1aUoswYbWbaGmdWKPzyWYwrs,8117
+ezdxf/modern/visualstyle.py,sha256=XPwUDFsnPS7anF3nzkh5hS-TaJ77L2atkHwD4KlvxcM,2529
+ezdxf/modern/wipeout.py,sha256=p3mgUNWWhDpOlotm68UCuBr6fVsH7LJdpqztZsWT_rc,1951
+ezdxf/modern/xrecord.py,sha256=-r_REO50lCBLzhWk2BBB1mOzv2WU3wGE6Wt09rAjMgc,2065
+ezdxf/options.py,sha256=pkG04D1Qvlab8LcJFtzAxVZ5GCcrxkGm6RkTFBrbaHU,526
+ezdxf/pp/__init__.py,sha256=7GiPs2UqkFQdHHC_GrC6Pd3bME8oDHC4rHIQaJ9CLKw,116
+ezdxf/pp/__main__.py,sha256=J7GRhErbxKucD-3yQ0G7v6Iv2HOq4v5_78MWgPw5zPc,3817
+ezdxf/pp/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/pp/__pycache__/__main__.cpython-37.pyc,,
+ezdxf/pp/__pycache__/dxfpp.cpython-37.pyc,,
+ezdxf/pp/__pycache__/rawpp.cpython-37.pyc,,
+ezdxf/pp/__pycache__/reflinks.cpython-37.pyc,,
+ezdxf/pp/dxfpp.css,sha256=RtFegvq7gI2otgnoAtyyDYm897Bu11L_z-b7sUIFCas,2987
+ezdxf/pp/dxfpp.html,sha256=Dt3EALzO4ExftcXZdKkKaOeIvi94p4QisVUnmEapZEk,1144
+ezdxf/pp/dxfpp.js,sha256=QOTpqHLnCSAs2bk7GHtsEuRm23HhMrwvZuFr-PHEdqw,11304
+ezdxf/pp/dxfpp.py,sha256=kgi9YOXjCBF6UdluveUE4UVjSzg0nFpBytIbF_W3Slg,16548
+ezdxf/pp/rawpp.css,sha256=5y_ZAdtUOgCwd4aQcP0Sd63RD0vpNILgAQd4T7ZRl4o,812
+ezdxf/pp/rawpp.html,sha256=VYsU6ZqFPJra16XXeenzyiD8yuneRjGJ-9-L3VBBS7k,316
+ezdxf/pp/rawpp.py,sha256=q178yyd8fl9coVXTHjM1UqvG4v6e41JofRdNO8waE5s,2242
+ezdxf/pp/reflinks.py,sha256=La5lbAfmGD4DXZDbYcnt070zmcS-ofPfoX0_yAkk35I,6348
+ezdxf/query.py,sha256=5ZLWWcvmJCjTls-1sQRJuZwa34SVVDVoj7Id6KTdTTY,10654
+ezdxf/queryparser.py,sha256=oDhpvu3JEb19hqv88dZ4q8Ekr4BxwVkIv7XLeSsObEY,2589
+ezdxf/r12writer.py,sha256=tg64P3r9CxuI3bNECWCJFK1D_RFDO2mrhDMzAVQJrKM,12267
+ezdxf/render/__init__.py,sha256=QBYkvmNTYS6MpwJ25sfRVW8_z3eaybsp4DqFUQc2ig4,254
+ezdxf/render/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/render/__pycache__/arrows.cpython-37.pyc,,
+ezdxf/render/__pycache__/curves.cpython-37.pyc,,
+ezdxf/render/__pycache__/dimension.cpython-37.pyc,,
+ezdxf/render/__pycache__/forms.cpython-37.pyc,,
+ezdxf/render/__pycache__/mesh.cpython-37.pyc,,
+ezdxf/render/__pycache__/r12spline.cpython-37.pyc,,
+ezdxf/render/arrows.py,sha256=xbexyftHc1M47i-6hbsgZDdxVJd75KkNd0S6suFgMQw,17119
+ezdxf/render/curves.py,sha256=Ho5UK6KHKT-weMcG3ocaH63ndh_2DaiDOdCRz_nakwI,12413
+ezdxf/render/dimension.py,sha256=Fp7kM9jRESrRwjDKF4B_NdlqUkNTFBNJMCozjtg7LH0,58119
+ezdxf/render/forms.py,sha256=EyIWdzawrEFT4sOHzf0m6qbr_DK3kQ9bhQbT8EzFB1Q,15779
+ezdxf/render/mesh.py,sha256=AKjoqFmW8TpmVPuXULOOJJVc9rHMs44I9_EsfQP092I,8147
+ezdxf/render/r12spline.py,sha256=zc5dW0RDdFIBEcAw5BxzLhXcTYQHBVx7jGTFqU5G-G0,5125
+ezdxf/sections/__init__.py,sha256=mW60LK7hO4r6cwb87oDmC98qtGkYHBH070yc_Prf5FQ,122
+ezdxf/sections/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/sections/__pycache__/abstract.cpython-37.pyc,,
+ezdxf/sections/__pycache__/acdsdata.cpython-37.pyc,,
+ezdxf/sections/__pycache__/blocks.cpython-37.pyc,,
+ezdxf/sections/__pycache__/classes.cpython-37.pyc,,
+ezdxf/sections/__pycache__/entities.cpython-37.pyc,,
+ezdxf/sections/__pycache__/header.cpython-37.pyc,,
+ezdxf/sections/__pycache__/objects.cpython-37.pyc,,
+ezdxf/sections/__pycache__/sections.cpython-37.pyc,,
+ezdxf/sections/__pycache__/table.cpython-37.pyc,,
+ezdxf/sections/__pycache__/tables.cpython-37.pyc,,
+ezdxf/sections/__pycache__/unsupported.cpython-37.pyc,,
+ezdxf/sections/abstract.py,sha256=W2OZAnVxgYmVG1znkKlnD4eixqo9VK-K6gqo9Qg8IrE,3299
+ezdxf/sections/acdsdata.py,sha256=p7OOEGfYnLxqhBZqz9nfv9th4F0EA3yqFPqs-lH83dk,5552
+ezdxf/sections/blocks.py,sha256=zfh5jsfDIZh1VemGY-EnzenlPaXyT_DhzhqW8604n1k,9028
+ezdxf/sections/classes.py,sha256=d18lKu-jJnww4HnIG8EtiRGifzLLiAVVAlpkGEEVJlo,2459
+ezdxf/sections/entities.py,sha256=_QQLZG4bfzgpFAcpGKEA6Fg0sF8FVmlsNC1bFMKLMKk,2912
+ezdxf/sections/header.py,sha256=StFPKplvJQADfGK4pkplJJ6_DUMvmmbtjwEdetB0HjQ,7383
+ezdxf/sections/objects.py,sha256=eORBnDHMUysO5yAilTjHtLOVZmBArATlOGFvDclSkyY,8219
+ezdxf/sections/sections.py,sha256=qyQv0k0_DuUVRPL21Bvh4lAyVgy6X_HzWWMBGOESwis,3983
+ezdxf/sections/table.py,sha256=rCU5sY073oiHwUIAI-q66Dk2I_9667I6h2CqxXssqxM,9304
+ezdxf/sections/tables.py,sha256=OrtvVTAQCCiK1l1D1XiXIRabqDZYD_MZlo1HtZ6mtCM,5350
+ezdxf/sections/unsupported.py,sha256=jLnuSQ9k-zoN6V1cDGJN-mDZwgY6QxlYf7pC02Cy2zk,1069
+ezdxf/templates/AC1009.dxf,sha256=5PbmDN9KCH9-44Yv0QXjxMPeoOdc2RTRLzFozyNjXSI,5759
+ezdxf/templates/AC1015.dxf,sha256=VKggpcicnLPE-TlNPxtQSc-w4-Qq0Hc2llF-5heigSY,16619
+ezdxf/templates/AC1018.dxf,sha256=lP94YGQyP1ncnAbL4gN8AdgWpta3jIGldrOJCgqPQqA,16893
+ezdxf/templates/AC1021.dxf,sha256=fdFMl9TIkhlPYhRaUYipan2crOSATjRQgeEzdmAYbt4,17990
+ezdxf/templates/AC1024.dxf,sha256=JKMDY7i5J8Qgn6bzy5tbMOk3_0Y5oCEbc2bKwbteP4Q,18024
+ezdxf/templates/AC1027.dxf,sha256=F3uRijJq8Zpzv1ioWrA97mvk9o3eI89s4rmJiih8REo,18526
+ezdxf/templates/AC1032.dxf,sha256=5ZSlxOBIzs2PiwEnh5nZ1syEROqzwglQ_TLNCFXdb1I,18526
+ezdxf/templates/__init__.py,sha256=phzaPuyFyLnT4nmzIKGKoE81TrDNRKX86Ju8iwYRYtQ,1202
+ezdxf/templates/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/tools/__init__.py,sha256=6LsWVOrzBV5jG01Kbe18uOnX7koJacphkomYybBhrTs,1610
+ezdxf/tools/__pycache__/__init__.cpython-37.pyc,,
+ezdxf/tools/__pycache__/binarydata.cpython-37.pyc,,
+ezdxf/tools/__pycache__/codepage.cpython-37.pyc,,
+ezdxf/tools/__pycache__/complex_ltype.cpython-37.pyc,,
+ezdxf/tools/__pycache__/crypt.cpython-37.pyc,,
+ezdxf/tools/__pycache__/handle.cpython-37.pyc,,
+ezdxf/tools/__pycache__/indexing.cpython-37.pyc,,
+ezdxf/tools/__pycache__/juliandate.cpython-37.pyc,,
+ezdxf/tools/__pycache__/pattern.cpython-37.pyc,,
+ezdxf/tools/__pycache__/perlin.cpython-37.pyc,,
+ezdxf/tools/__pycache__/rgb.cpython-37.pyc,,
+ezdxf/tools/__pycache__/standards.cpython-37.pyc,,
+ezdxf/tools/__pycache__/test.cpython-37.pyc,,
+ezdxf/tools/__pycache__/zipmanager.cpython-37.pyc,,
+ezdxf/tools/binarydata.py,sha256=om19G0kH4x9-3sgGYbYrLLN3ZKMPi-ANDDnjLGCYq9Q,397
+ezdxf/tools/codepage.py,sha256=xAEBuCWYSYci0N8DO-s6DEi7BG0O-0c0MmEqLOrCWyI,1150
+ezdxf/tools/complex_ltype.py,sha256=MwBYPevyOrljmGecNO01e2OLCbEbedWlTkpBQpOU6Mc,7033
+ezdxf/tools/crypt.py,sha256=vnPAIICvNKJxuETXcckYZW_JODxBNobcbSob9GqBQBg,1595
+ezdxf/tools/handle.py,sha256=AN4GCqqxQGxtmjvRS1YTRd5eej6k4_JKjGlNW8D5fbM,699
+ezdxf/tools/indexing.py,sha256=ssTzvokVSyTLhXqiX-Ku1mjiCu2iia1XNw96PUcgpzg,780
+ezdxf/tools/juliandate.py,sha256=IJmCQCb3V1rBRUjE4GNOYCl6hQbKoL4iz-n57NX0gA0,2156
+ezdxf/tools/pattern.py,sha256=Aw89itqQMeVOnWH2ezwVyrrDJu8B17wKPh3CAT--e9A,23329
+ezdxf/tools/perlin.py,sha256=Y0P8hJOn2aR-l6ceSAdRLmu7ywhs8bCVw1qRLonqrnA,15354
+ezdxf/tools/rgb.py,sha256=OtEAwvNaQdAIOHIIaCNpL5rOuDUSZxeqkYsLop4c5Bc,4537
+ezdxf/tools/standards.py,sha256=sAjDJaHM0WQ8NaUNNX2iJjDme3VNdoptzAcTctUzAHU,12442
+ezdxf/tools/test.py,sha256=MwqH5zDznwGtqpsIkRxJLYWAqDYtLXtquXzOI65L0Cs,1429
+ezdxf/tools/zipmanager.py,sha256=XAbNt2-7gjOtgg27JDnn6JLrxqKgJ_U8Y7Xo8gRKjUo,2779
+ezdxf/tracker.py,sha256=eJ334Ie4YOfIKZht-ttBU76CO0s8iugoZKQmark-j2k,168
+ezdxf/units.py,sha256=pAQUAmczntt-1lQ1tkpQ7HKSAVBtNYrhNSfBAuQI_9o,1428
+ezdxf/version.py,sha256=6SgX0jfXOQlOdB2DtzuWmY2EwN5mxcuUZN6pbog7JjY,1167

+ 5 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/WHEEL

@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.32.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+

+ 4 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/entry_points.txt

@@ -0,0 +1,4 @@
+[console_scripts]
+dxfaudit = ezdxf.audit.__main__:main
+dxfpp = ezdxf.pp.__main__:main
+

+ 1 - 0
venv/lib/python3.7/site-packages/ezdxf-0.9.dist-info/top_level.txt

@@ -0,0 +1 @@
+ezdxf

+ 33 - 0
venv/lib/python3.7/site-packages/ezdxf/__init__.py

@@ -0,0 +1,33 @@
+# Created: 10.03.2011
+# Copyright (C) 2011-2018, Manfred Moitzi
+# License: MIT License
+# import version data
+import sys
+from .version import version, __version__
+
+VERSION = __version__
+__author__ = "mozman <me@mozman.at>"
+
+PYPY = hasattr(sys, 'pypy_version_info')
+# name space imports - do not remove
+from ezdxf.options import options  # example: ezdxf.options.template_dir = 'c:\templates'
+from ezdxf.tools import transparency2float, float2transparency  # convert transparency integer values to floats 0..1
+from ezdxf.tools.rgb import int2rgb, rgb2int
+from ezdxf.tools.pattern import PATTERN
+from ezdxf.lldxf import const  # restore module structure ezdxf.const
+from ezdxf.lldxf.validator import is_dxf_file, is_dxf_stream
+from ezdxf.filemanagement import new, read, readfile, readzip
+from ezdxf.tools.standards import setup_linetypes, setup_styles, setup_dimstyles, setup_dimstyle
+from ezdxf.render.arrows import ARROWS
+
+# Exceptions
+from ezdxf.lldxf.const import DXFError  # base error exception
+from ezdxf.lldxf.const import DXFStructureError, DXFVersionError, DXFTableEntryError, DXFAppDataError, DXFXDataError
+from ezdxf.lldxf.const import DXFAttributeError, DXFValueError, DXFKeyError, DXFIndexError, DXFTypeError, DXFInvalidLayerName
+from ezdxf.lldxf.const import DXFBlockInUseError
+# name space imports - do not remove
+
+import codecs
+from ezdxf.lldxf.encoding import dxf_backslash_replace
+codecs.register_error('dxfreplace', dxf_backslash_replace)  # setup DXF unicode encoder -> '\U+nnnn'
+

+ 611 - 0
venv/lib/python3.7/site-packages/ezdxf/acadctb.py

@@ -0,0 +1,611 @@
+#!/usr/bin/env python
+# coding:utf-8
+# Purpose: read, create and write acad ctb files
+# Created: 23.03.2010 for dxfwrite, added to ezdxf package on 2016-03-06
+# Copyright (C) 2010, Manfred Moitzi
+# License: MIT License
+# IMPORTANT: use only standard 7-Bit ascii code
+from typing import Union, Tuple, Optional, BinaryIO, TextIO, Iterable, List, Any
+from io import StringIO
+from array import array
+from struct import pack
+import zlib
+
+ENDSTYLE_BUTT = 0
+ENDSTYLE_SQUARE = 1
+ENDSTYLE_ROUND = 2
+ENDSTYLE_DIAMOND = 3
+ENDSTYLE_OBJECT = 4
+
+JOINSTYLE_MITER = 0
+JOINSTYLE_BEVEL = 1
+JOINSTYLE_ROUND = 2
+JOINSTYLE_DIAMOND = 3
+JOINSTYLE_OBJECT = 5
+
+FILL_STYLE_SOLID = 64
+FILL_STYLE_CHECKERBOARD = 65
+FILL_STYLE_CROSSHATCH = 66
+FILL_STYLE_DIAMONDS = 67
+FILL_STYLE_HORIZONTAL_BARS = 68
+FILL_STYLE_SLANT_LEFT = 69
+FILL_STYLE_SLANT_RIGHT = 70
+FILL_STYLE_SQUARE_DOTS = 71
+FILL_STYLE_VERICAL_BARS = 72
+FILL_STYLE_OBJECT = 73
+
+DITHERING_ON = 1  # bit coded color_policy
+GRAYSCALE_ON = 2  # bit coded color_policy
+
+AUTOMATIC = 0
+OBJECT_LINEWEIGHT = 0
+OBJECT_LINETYPE = 31
+OBJECT_COLOR = -1
+OBJECT_COLOR2 = -1006632961
+
+STYLE_COUNT = 255
+
+DEFAULT_LINE_WEIGHTS = [
+    0.00,  # 0
+    0.05,  # 1
+    0.09,  # 2
+    0.10,  # 3
+    0.13,  # 4
+    0.15,  # 5
+    0.18,  # 6
+    0.20,  # 7
+    0.25,  # 8
+    0.30,  # 9
+    0.35,  # 10
+    0.40,  # 11
+    0.45,  # 12
+    0.50,  # 13
+    0.53,  # 14
+    0.60,  # 15
+    0.65,  # 16
+    0.70,  # 17
+    0.80,  # 18
+    0.90,  # 19
+    1.00,  # 20
+    1.06,  # 21
+    1.20,  # 22
+    1.40,  # 23
+    1.58,  # 24
+    2.00,  # 25
+    2.11,  # 26
+]
+
+
+def color_name(index: int) -> str:
+    return 'Color_%d' % (index + 1)
+
+
+def get_bool(value: Union[str, bool]) -> bool:
+    if isinstance(value, str):
+        upperstr = value.upper()
+        if upperstr == 'TRUE':
+            value = True
+        elif upperstr == 'FALSE':
+            value = False
+        else:
+            raise ValueError("Unknown bool value '%s'." % str(value))
+    return value
+
+
+class UserStyle:
+    def __init__(self, index: int, data: dict = None, parent: 'UserStyles' = None):
+        data = data or {}
+        self.parent = parent
+        self.index = int(index)
+        self.description = str(data.get('description', ""))
+        # do not set _color, _mode_color or _color_policy directly
+        # use set_color() method, and the properties dithering and grayscale
+        self._color = int(data.get('color', OBJECT_COLOR))
+        if self._color != OBJECT_COLOR:
+            self._mode_color = int(data.get('mode_color', self._color))
+        self._color_policy = int(data.get('color_policy', DITHERING_ON))
+        self.physical_pen_number = int(data.get('physical_pen_number', AUTOMATIC))
+        self.virtual_pen_number = int(data.get('virtual_pen_number', AUTOMATIC))
+        self.screen = int(data.get('screen', 100))
+        self.linepattern_size = float(data.get('linepattern_size', 0.5))
+        self.linetype = int(data.get('linetype', OBJECT_LINETYPE))  # 0 .. 30
+        self.adaptive_linetype = get_bool(data.get('adaptive_linetype', True))
+        self.lineweight = int(data.get('lineweight', OBJECT_LINEWEIGHT))
+        self.end_style = int(data.get('end_style', ENDSTYLE_OBJECT))
+        self.join_style = int(data.get('join_style', JOINSTYLE_OBJECT))
+        self.fill_style = int(data.get('fill_style', FILL_STYLE_OBJECT))
+
+    def set_color(self, red: int, green: int, blue: int) -> None:
+        """
+        Set color as rgb-tuple.
+
+        """
+        self._mode_color = mode_color2int(red, green, blue)
+        # when defining a user-color, <mode_color> represents the real truecolor
+        # as rgb-tuple with the magic number 0xC2 as highest byte, the <color>
+        # value calculated for a user-color is not a rgb-tuple and has the magic
+        # number 0xC3 (sometimes), I set for <color> the same value a for
+        # <mode_color>, because Autocad corrects the <color> value by itself.
+        self._color = self._mode_color
+
+    def set_object_color(self) -> None:
+        """
+        Set color to object color.
+
+        """
+        self._color = OBJECT_COLOR
+        self._mode_color = OBJECT_COLOR
+
+    def set_lineweight(self, lineweight: float) -> None:
+        """Set lineweight. Use 0.0 to set lineweight by object.
+
+        lineweight in mm! not the lineweight index
+
+        """
+        self.lineweight = self.parent.get_lineweight_index(lineweight)
+
+    def get_lineweight(self) -> float:
+        """
+        Returns the lineweight in millimeters.
+
+        :returns: lineweight in mm or 0.0 for use entity lineweight
+
+        """
+        return self.parent.lineweights[self.lineweight]
+
+    def has_object_color(self) -> bool:
+        """
+        True if style has object color.
+
+        """
+        return self._color == OBJECT_COLOR or \
+               self._color == OBJECT_COLOR2
+
+    def get_color(self) -> Optional[Tuple[int, int, int, int]]:
+        """
+        Get style color as rgb-tuple or None if style has object color.
+
+        """
+        if self.has_object_color():
+            return None  # object color
+        else:
+            return int2color(self._mode_color)[:3]
+
+    def get_dxf_color_index(self) -> int:
+        return self.index + 1
+
+    def get_dithering(self) -> bool:
+        return bool(self._color_policy & DITHERING_ON)
+
+    def set_dithering(self, status: bool) -> None:
+        if status:
+            self._color_policy |= DITHERING_ON
+        else:
+            self._color_policy &= ~DITHERING_ON
+
+    dithering = property(get_dithering, set_dithering)
+
+    def get_grayscale(self) -> bool:
+        return bool(self._color_policy & GRAYSCALE_ON)
+
+    def set_grayscale(self, status: bool) -> None:
+        if status:
+            self._color_policy |= GRAYSCALE_ON
+        else:
+            self._color_policy &= ~GRAYSCALE_ON
+
+    grayscale = property(get_grayscale, set_grayscale)
+
+    def write(self, stream: TextIO) -> None:
+        """
+        Write style data to file-like object <stream>.
+
+        """
+        index = self.index
+        stream.write(' %d{\n' % index)
+        stream.write('  name="%s\n' % color_name(index))
+        stream.write('  localized_name="%s\n' % color_name(index))
+        stream.write('  description="%s\n' % self.description)
+        stream.write('  color=%d\n' % self._color)
+        if self._color != OBJECT_COLOR:
+            stream.write('  mode_color=%d\n' % self._mode_color)
+        stream.write('  color_policy=%d\n' % self._color_policy)
+        stream.write('  physical_pen_number=%d\n' % self.physical_pen_number)
+        stream.write('  virtual_pen_number=%d\n' % self.virtual_pen_number)
+        stream.write('  screen=%d\n' % self.screen)
+        stream.write('  linepattern_size=%s\n' % str(self.linepattern_size))
+        stream.write('  linetype=%d\n' % self.linetype)
+        stream.write('  adaptive_linetype=%s\n' % str(bool(self.adaptive_linetype)).upper())
+        stream.write('  lineweight=%s\n' % str(self.lineweight))
+        stream.write('  fill_style=%d\n' % self.fill_style)
+        stream.write('  end_style=%d\n' % self.end_style)
+        stream.write('  join_style=%d\n' % self.join_style)
+        stream.write(' }\n')
+
+
+class UserStyles:
+    """
+    UserStyle container
+
+    """
+
+    def __init__(self, description: str = "", scale_factor: float = 1.0, apply_factor: bool = False):
+        self.description = description
+        self.scale_factor = scale_factor
+        self.apply_factor = apply_factor
+
+        # set custom_line... to 1 for showing lineweights in inch in the Autocad
+        # ctb editor window, but lineweights are always defined in mm
+        self.custom_lineweight_display_units = 0
+        self.styles = [None] * (STYLE_COUNT + 1)  # type: List[UserStyle]
+        self.lineweights = array('f', DEFAULT_LINE_WEIGHTS)
+        self.set_default_styles()
+
+    def set_default_styles(self) -> None:
+        for index in range(STYLE_COUNT):
+            self._set_style(UserStyle(index))
+
+    @staticmethod
+    def check_color_index(dxf_color_index: int) -> int:
+        if 0 < dxf_color_index < 256:
+            return dxf_color_index
+        raise IndexError('color index has to be in the range [1 .. 255].')
+
+    def iter_styles(self) -> Iterable[UserStyle]:
+        return (style for style in self.styles[1:])
+
+    def _set_style(self, style: UserStyle) -> None:
+        style.parent = self
+        self.styles[style.get_dxf_color_index()] = style
+
+    def set_style(self, dxf_color_index: int, data: dict = None) -> UserStyle:
+        """
+        Set <dxf_color_index> to new attributes defined in init_dict.
+
+        """
+        dxf_color_index = self.check_color_index(dxf_color_index)
+        # ctb table index is dxf_color_index - 1
+        # ctb table starts with index 0, where dxf_color_index=0 means BYBLOCK
+        style = UserStyle(dxf_color_index - 1, data)
+        self._set_style(style)
+        return style
+
+    def get_style(self, dxf_color_index: int) -> UserStyle:
+        """
+        Get style for <dxf_color_index>.
+
+        """
+        dxf_color_index = self.check_color_index(dxf_color_index)
+        return self.styles[dxf_color_index]
+
+    def get_color(self, dxf_color_index: int) -> Optional[Tuple[int, int, int, int]]:
+        """
+        Get rgb-color-tuple for <dxf_color_index> or None if not specified.
+
+        """
+        style = self.get_style(dxf_color_index)
+        return style.get_color()
+
+    def get_lineweight(self, dxf_color_index: int):
+        """
+        Returns the assigned lineweight for <dxf_color_index> in mm.
+
+        """
+        style = self.get_style(dxf_color_index)
+        lineweight = style.get_lineweight()
+        if lineweight == 0.0:
+            return None
+        else:
+            return lineweight
+
+    def get_lineweight_index(self, lineweight: float) -> int:
+        """
+        Get index of lineweight in the lineweight table or append lineweight to lineweight table.
+
+        """
+        try:
+            return self.lineweights.index(lineweight)
+        except ValueError:
+            self.lineweights.append(lineweight)
+            return len(self.lineweights) - 1
+
+    def set_table_lineweight(self, index: int, weight: float) -> int:
+        """
+        Index is the lineweight table index, not the dxf color index.
+
+        :param int index: lineweight table index = UserStyle.lineweight
+        :param float weight: in millimeters
+
+        """
+        try:
+            self.lineweights[index] = weight
+            return index
+        except IndexError:
+            self.lineweights.append(weight)
+            return len(self.lineweights) - 1
+
+    def get_table_lineweight(self, index: int) -> float:
+        """
+        Returns lineweight in millimeters.
+
+        :param int index: lineweight table index = UserStyle.lineweight
+        :returns: lineweight in mm or 0.0 for use entity lineweight
+
+        """
+        return self.lineweights[index]
+
+    def save(self, filename: str) -> None:
+        """
+        Save ctb-file to <filename>.
+
+        """
+        with open(filename, 'wb') as stream:
+            self.write(stream)
+
+    def write(self, stream: BinaryIO):
+        """
+        Create and compress the ctb-file to <stream>.
+
+        """
+        memfile = StringIO()
+        self.write_content(memfile)
+        memfile.write(chr(0))  # end of file
+        body = memfile.getvalue()
+        memfile.close()
+        self._compress(stream, body)
+
+    def write_content(self, stream: TextIO) -> None:
+        """
+        Write the ctb-file to <fileobj>.
+
+        """
+        self._write_header(stream)
+        self._write_aci_table(stream)
+        self._write_ctb_plot_styles(stream)
+        self._write_lineweights(stream)
+
+    def _write_header(self, stream: TextIO) -> None:
+        """
+        Write header values of ctb-file to <stream>.
+
+        """
+        stream.write('description="%s\n' % self.description)
+        stream.write('aci_table_available=TRUE\n')
+        stream.write('scale_factor=%.1f\n' % self.scale_factor)
+        stream.write('apply_factor=%s\n' % str(self.apply_factor).upper())
+        stream.write('custom_lineweight_display_units=%s\n' % str(
+            self.custom_lineweight_display_units))
+
+    def _write_aci_table(self, stream: TextIO) -> None:
+        """
+        Write autocad color index table to ctb-file <stream>.
+
+        """
+        stream.write('aci_table{\n')
+        for style in self.iter_styles():
+            index = style.index
+            stream.write(' %d="%s\n' % (index, color_name(index)))
+        stream.write('}\n')
+
+    def _write_ctb_plot_styles(self, stream: TextIO) -> None:
+        """
+        Write user styles to ctb-file <stream>.
+
+        """
+        stream.write('plot_style{\n')
+        for style in self.iter_styles():
+            style.write(stream)
+        stream.write('}\n')
+
+    def _write_lineweights(self, stream: TextIO) -> None:
+        """
+        Write custom lineweights table to ctb-file <stream>.
+
+        """
+        stream.write('custom_lineweight_table{\n')
+        for index, weight in enumerate(self.lineweights):
+            stream.write(' %d=%.2f\n' % (index, weight))
+        stream.write('}\n')
+
+    def parse(self, text: str) -> None:
+        """
+        Parse and get values of plot styles from <text>.
+
+        """
+
+        def set_lineweights(lineweights):
+            if lineweights is None:
+                return
+            self.lineweights = array('f', [0.0] * len(lineweights))
+            for key, value in lineweights.items():
+                self.lineweights[int(key)] = float(value)
+
+        def set_styles(styles):
+            for index, style in styles.items():
+                style = UserStyle(index, style)
+                self._set_style(style)
+
+        parser = CtbParser(text)
+        self.description = parser.get('description', "")
+        self.scale_factor = float(parser.get('scale_factor', 1.0))
+        self.apply_factor = get_bool(parser.get('apply_factor', True))
+        self.custom_lineweight_display_units = int(
+            parser.get('custom_lineweight_display_units', 0))
+        set_lineweights(parser.get('custom_lineweight_table', None))
+        set_styles(parser.get('plot_style', {}))
+
+    def _compress(self, stream: BinaryIO, body: str):
+        """
+        Compress ctb-file-body and write it to <stream>.
+
+        """
+
+        def writestr(s):
+            stream.write(s.encode())
+
+        body = body.encode()
+        comp_body = zlib.compress(body)
+        adler_chksum = zlib.adler32(comp_body)
+        writestr('PIAFILEVERSION_2.0,CTBVER1,compress\r\npmzlibcodec')
+        stream.write(pack('LLL', adler_chksum, len(body), len(comp_body)))
+        stream.write(comp_body)
+
+
+def read(stream: BinaryIO) -> UserStyles:
+    """
+    Read a ctb-file from the file-like object <stream>.
+
+    """
+    content = _decompress(stream)
+    content = content.decode()
+    styles = UserStyles()
+    styles.parse(content)
+    return styles
+
+
+def load(filename: str) -> UserStyles:
+    """
+    Load the ctb-file <filename>.
+
+    """
+    with open(filename, 'rb') as stream:
+        ctbfile = read(stream)
+    return ctbfile
+
+
+def _decompress(stream: BinaryIO) -> bytes:
+    """
+    Read and decompress the file content of the file-like object <stream>.
+
+    """
+    content = stream.read()
+    data = zlib.decompress(content[60:])  # type: bytes
+    return data[:-1]  # truncate trailing \nul
+
+
+class CtbParser:
+    """
+    A very simple ctb-file parser. Ctb-files are created by programs, so the file structure should be correct
+    in the most cases.
+
+    """
+
+    def __init__(self, text: str):
+        """
+        :param str text: ctb content as string
+
+        """
+        self.data = {}
+        for element, value in CtbParser.iteritems(text):
+            self.data[element] = value
+
+    @staticmethod
+    def iteritems(text: str):
+        """
+        iterate over all first level (start at col 0) elements
+
+        """
+
+        def get_name() -> str:
+            """
+            Get element name of line <line_index>.
+
+            """
+            line = lines[line_index]
+            if line.endswith('{'):  # start of a list like 'plot_style{'
+                name = line[:-1]
+            else:  # simple name=value line
+                name = line.split('=', 1)[0]
+            return name.strip()
+
+        def get_mapping() -> dict:
+            """
+            Get mapping of elements enclosed by { }.
+
+            e. g. lineweigths, plot_styles, aci_table
+
+            """
+            nonlocal line_index
+
+            def end_of_list():
+                return lines[line_index].endswith('}')
+
+            data = dict()
+            while not end_of_list():
+                name = get_name()
+                value = get_value()  # get value or sub-list
+                data[name] = value
+            line_index += 1
+            return data  # skip '}' - end of list
+
+        def get_value() -> Union[str, dict]:
+            """
+            Get value of line <line_index> or the list that starts in line <line_index>.
+
+            """
+            nonlocal line_index
+            line = lines[line_index]
+            if line.endswith('{'):  # start of a list
+                line_index += 1
+                value = get_mapping()
+            else:  # it's a simple name=value line
+                value = line.split('=', 1)[1]  # type: str
+                value = value.lstrip('"')  # strings look like this: name="value
+                line_index += 1
+            return value
+
+        def skip_empty_lines():
+            nonlocal line_index
+            while line_index < len(lines) and len(lines[line_index]) == 0:
+                line_index += 1
+
+        lines = text.split('\n')
+        line_index = 0
+        while line_index < len(lines):
+            name = get_name()
+            value = get_value()
+            yield (name, value)
+            skip_empty_lines()
+
+    def get(self, name: str, default: Any) -> Any:
+        return self.data.get(name, default)
+
+
+# color_type: (thx to Rammi)
+# Take color from layer, ignore other bytes.
+COLOR_BY_LAYER = 0xc0
+# Take color from insertion, ignore other bytes
+COLOR_BY_BLOCK = 0xc1
+# RGB value, other bytes are R,G,B.
+COLOR_RGB = 0xc2
+# ACI, AutoCAD color index, other bytes are 0,0,index
+COLOR_ACI = 0xc3
+
+
+def int2color(color: int) -> Tuple[int, int, int, int]:
+    """
+    Convert color integer value from ctb-file to rgb-tuple plus a magic number.
+
+    """
+    # Take color from layer, ignore other bytes.
+    color_type = (color & 0xff000000) >> 24
+    red = (color & 0xff0000) >> 16
+    green = (color & 0xff00) >> 8
+    blue = color & 0xff
+    return red, green, blue, color_type
+
+
+def mode_color2int(red: int, green: int, blue: int, color_type=COLOR_RGB) -> int:
+    """
+    Convert rgb-tuple to an int value.
+
+    """
+    return -color2int(red, green, blue, color_type)
+
+
+def color2int(red: int, green: int, blue: int, color_type: int) -> int:
+    """
+    Convert rgb-tuple to an int value.
+
+    """
+    return -((color_type << 24) + (red << 16) + (green << 8) + blue) & 0xffffffff

+ 9 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/__init__.py

@@ -0,0 +1,9 @@
+# Created: 13.02.2018
+# Copyright (C) 2011-2018, Manfred Moitzi
+# License: MIT License
+from .table import Table, CustomCell
+from .mtext import MText
+from .menger_sponge import MengerSponge
+from .sierpinski_pyramid import SierpinskyPyramid
+from .dimlines import LinearDimension, AngularDimension, ArcDimension, RadialDimension, dimstyles
+from .importer import Importer

+ 684 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/dimlines.py

@@ -0,0 +1,684 @@
+# Purpose: dimension lines as composite entities build with basic dxf entities, but not the DIMENSION entity.
+# Created: 10.03.2010, 2018 adapted for ezdxf
+# Copyright (c) 2010-2018, Manfred Moitzi
+# License: MIT License
+"""
+Dimension lines as composite entities build with basic dxf entities, but not the DIMENSION entity.
+
+OBJECTS
+
+- LinearDimension
+- AngularDimension
+- ArcDimension
+- RadialDimension
+
+PUBLIC MEMBERS
+
+dimstyles
+    dimstyle container
+
+    - new(name, kwargs) to create a new dimstyle
+    - get(name) to get a dimstyle, 'Default' if name does not exist
+    - setup(drawing) create Blocks and Layers in drawing
+"""
+from typing import Any, Dict, TYPE_CHECKING, Iterable, List, Tuple
+from math import radians, degrees, pi
+from abc import abstractmethod
+
+from ezdxf.math.vector import Vector, distance, lerp
+from ezdxf.math.line import ConstructionRay
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Drawing, GenericLayoutType, Vertex
+
+DIMENSIONS_MIN_DISTANCE = 0.05
+DIMENSIONS_FLOATINGPOINT = '.'
+
+ANGLE_DEG = 180. / pi
+ANGLE_GRAD = 200. / pi
+ANGLE_RAD = 1.
+
+
+class DimStyle(dict):
+    """
+    DimStyle parameter struct, a dumb object just to store values
+
+    """
+    default_values = [
+        # tick block name, use setup to generate default blocks <dimblk> <dimblk1> <dimblk2>
+        ('tick', 'DIMTICK_ARCH'),
+        # scale factor for ticks-block <dimtsz> <dimasz>
+        ('tickfactor', 1.),
+        # tick2x means tick is drawn only for one side, insert tick a second
+        # time rotated about 180 degree, but only one time at the dimension line
+        # ends, this is useful for arrow-like ticks. hint: set dimlineext to 0. <none>
+        ('tick2x', False),
+        # dimension value scale factor, value = drawing-units * scale <dimlfac>
+        ('scale', 100.),
+        # round dimension value to roundval fractional digits <dimdec>
+        ('roundval', 0),
+        # round dimension value to half units, round 0.4, 0.6 to 0.5 <dimrnd>
+        ('roundhalf', False),
+        # dimension value text color <dimclrt>
+        ('textcolor', 7),
+        # dimension value text height <dimtxt>
+        ('height', .5),
+        # dimension text prefix and suffix like 'x=' ... ' cm' <dimpost>
+        ('prefix', ''),
+        ('suffix', ''),
+        # dimension value text style <dimtxsty>
+        ('style', 'OpenSansCondensed-Light'),
+        # default layer for whole dimension object
+        ('layer', 'DIMENSIONS'),
+        # dimension line color index (0 from layer) <dimclrd>
+        ('dimlinecolor', 7),
+        # dimension line extensions (in dimline direction, left and right) <dimdle>
+        ('dimlineext', .3),
+        # draw dimension value text `textabove` drawing-units above the
+        # dimension line <dimgap>
+        ('textabove', 0.2),
+        # switch extension line False=off, True=on <dimse1> <dimse2>
+        ('dimextline', True),
+        # dimension extension line color index (0 from layer) <dimclre>
+        ('dimextlinecolor', 5),
+        # gap between measure target point and end of extension line <dimexo>
+        ('dimextlinegap', 0.3)
+    ]
+
+    def __init__(self, name: str, **kwargs):
+        super().__init__(DimStyle.default_values)
+        # dimstyle name
+        self['name'] = name
+        self.update(kwargs)
+
+    def __getattr__(self, attr: str) -> Any:
+        return self[attr]
+
+    def __setattr__(self, attr: str, value: Any) -> None:
+        self[attr] = value
+
+
+class DimStyles:
+    """
+    DimStyle container
+
+    """
+
+    def __init__(self):
+        self._styles = {}  # type: Dict[str, DimStyle]
+        self.default = DimStyle('Default')
+
+        self.new(
+            "angle.deg",
+            scale=ANGLE_DEG,
+            suffix=str('°'),
+            roundval=0,
+            tick="DIMTICK_RADIUS",
+            tick2x=True,
+            dimlineext=0.,
+            dimextline=False
+        )
+        self.new(
+            "angle.grad",
+            scale=ANGLE_GRAD,
+            suffix='gon',
+            roundval=0,
+            tick="DIMTICK_RADIUS",
+            tick2x=True,
+            dimlineext=0.,
+            dimextline=False
+        )
+        self.new(
+            "angle.rad",
+            scale=ANGLE_RAD,
+            suffix='rad',
+            roundval=3,
+            tick="DIMTICK_RADIUS",
+            tick2x=True,
+            dimlineext=0.,
+            dimextline=False
+        )
+
+    def get(self, name: str) -> DimStyle:
+        """
+        Get DimStyle() object by name.
+        """
+        return self._styles.get(name, self.default)
+
+    def new(self, name: str, **kwargs) -> DimStyle:
+        """
+        Create a new dimstyle
+        """
+        style = DimStyle(name, **kwargs)
+        self._styles[name] = style
+        return style
+
+    @staticmethod
+    def setup(drawing: 'Drawing'):
+        """
+        Insert necessary definitions into drawing:
+
+            ticks: DIMTICK_ARCH, DIMTICK_DOT, DIMTICK_ARROW
+        """
+        # default pen assignment:
+        # 1 : 1.40mm - red
+        # 2 : 0.35mm - yellow
+        # 3 : 0.70mm - green
+        # 4 : 0.50mm - cyan
+        # 5 : 0.13mm - blue
+        # 6 : 1.00mm - magenta
+        # 7 : 0.25mm - white/black
+        # 8, 9 : 2.00mm
+        # >=10 : 1.40mm
+
+        dimcolor = {'color': dimstyles.default.dimextlinecolor, 'layer': 'BYBLOCK'}
+        color4 = {'color': 4, 'layer': 'BYBLOCK'}
+        color7 = {'color': 7, 'layer': 'BYBLOCK'}
+
+        block = drawing.blocks.new('DIMTICK_ARCH')
+        block.add_line(start=(0., +.5), end=(0., -.5), dxfattribs=dimcolor)
+        block.add_line(start=(-.2, -.2), end=(.2, +.2), dxfattribs=color4)
+
+        block = drawing.blocks.new('DIMTICK_DOT')
+        block.add_line(start=(0., .5), end=(0., -.5), dxfattribs=dimcolor)
+        block.add_circle(center=(0, 0), radius=.1, dxfattribs=color4)
+
+        block = drawing.blocks.new('DIMTICK_ARROW')
+
+        block.add_line(start=(0., .5), end=(0., -.50), dxfattribs=dimcolor)
+        block.add_solid([(0, 0), (.3, .05), (.3, -.05)], dxfattribs=color7)
+
+        block = drawing.blocks.new('DIMTICK_RADIUS')
+        block.add_solid([(0, 0), (.3, .05), (0.25, 0.), (.3, -.05)], dxfattribs=color7)
+
+
+dimstyles = DimStyles()  # use this factory to create new dimstyles
+
+
+class _DimensionBase:
+    """
+    Abstract base class for dimension lines.
+
+    """
+
+    def __init__(self, dimstyle: str, layer: str, roundval: int):
+        self.dimstyle = dimstyles.get(dimstyle)
+        self.layer = layer
+        self.roundval = roundval
+
+    def prop(self, property_name: str) -> Any:
+        """
+        Get dimension line properties by `property_name` with the possibility to override several properties.
+        """
+        if property_name == 'layer':
+            return self.layer if self.layer is not None else self.dimstyle.layer
+        elif property_name == 'roundval':
+            return self.roundval if self.roundval is not None else self.dimstyle.roundval
+        else:  # pass through self.dimstyle object DimStyle()
+            return self.dimstyle[property_name]
+
+    def format_dimtext(self, dimvalue: float) -> str:
+        """
+        Format the dimension text.
+        """
+        # TODO: consider roundhalf property
+        dimtextfmt = "%." + str(self.prop('roundval')) + "f"
+        dimtext = dimtextfmt % dimvalue
+        if DIMENSIONS_FLOATINGPOINT in dimtext:
+            # remove successional zeros
+            dimtext.rstrip('0')
+            # remove floating point as last char
+            dimtext.rstrip(DIMENSIONS_FLOATINGPOINT)
+        return self.prop('prefix') + dimtext + self.prop('suffix')
+
+    @abstractmethod
+    def render(self, layout: 'GenericLayoutType'):
+        pass
+
+
+class LinearDimension(_DimensionBase):
+    """
+    Simple straight dimension line with two or more measure points, build with basic DXF entities. This is NOT a dxf
+    dimension entity. And This is a 2D element, so all z-values will be ignored!
+
+    """
+
+    def __init__(self, pos: 'Vertex', measure_points: Iterable['Vertex'], angle: float = 0., dimstyle: str = 'Default',
+                 layer: str = None, roundval: int = None):
+        """
+        LinearDimension Constructor.
+
+        Args:
+            pos: location as (x, y) tuple of dimension line, line goes through this point
+            measure_points: list of points as (x, y) tuples to dimension (two or more)
+            angle: angle (in degree) of dimension line
+            dimstyle: dimstyle name, 'Default' - style is the default value
+            layer: dimension line layer, override the default value of dimstyle
+            roundval: count of decimal places
+
+        """
+        super().__init__(dimstyle, layer, roundval)
+        self.angle = angle
+        self.measure_points = list(measure_points)
+        self.text_override = [""] * self.section_count
+        self.dimlinepos = Vector(pos)
+        self.layout = None
+
+    def set_text(self, section: int, text: str) -> None:
+        """
+        Set and override the text of the dimension text for the given dimension line section.
+        """
+        self.text_override[section] = text
+
+    def _setup(self) -> None:
+        """
+        Calc setup values and determines the point order of the dimension line points.
+        """
+        self.measure_points = [Vector(point) for point in self.measure_points]  # type: List[Vector]
+        dimlineray = ConstructionRay(self.dimlinepos, angle=radians(self.angle))  # Type: ConstructionRay
+        self.dimline_points = [self._get_point_on_dimline(point, dimlineray) for point in
+                               self.measure_points]  # type: List[Vector]
+        self.point_order = self._indices_of_sorted_points(self.dimline_points)  # type: List[int]
+        self._build_vectors()
+
+    def _get_dimline_point(self, index: int) -> 'Vertex':
+        """
+        Get point on the dimension line, index runs left to right.
+        """
+        return self.dimline_points[self.point_order[index]]
+
+    def _get_section_points(self, section: int) -> Tuple[Vector, Vector]:
+        """
+        Get start and end point on the dimension line of dimension section.
+        """
+        return self._get_dimline_point(section), self._get_dimline_point(section + 1)
+
+    def _get_dimline_bounds(self) -> Tuple[Vector, Vector]:
+        """
+        Get the first and the last point of dimension line.
+        """
+        return self._get_dimline_point(0), self._get_dimline_point(-1)
+
+    @property
+    def section_count(self) -> int:
+        """ count of dimline sections """
+        return len(self.measure_points) - 1
+
+    @property
+    def point_count(self) -> int:
+        """ count of dimline points """
+        return len(self.measure_points)
+
+    def render(self, layout: 'GenericLayoutType') -> None:
+        """ build dimension line object with basic dxf entities """
+        self._setup()
+        self._draw_dimline(layout)
+        if self.prop('dimextline'):
+            self._draw_extension_lines(layout)
+        self._draw_text(layout)
+        self._draw_ticks(layout)
+
+    @staticmethod
+    def _indices_of_sorted_points(points: Iterable['Vertex']) -> List[int]:
+        """ get indices of points, for points sorted by x, y values """
+        indexed_points = [(point, idx) for idx, point in enumerate(points)]
+        indexed_points.sort()
+        return [idx for point, idx in indexed_points]
+
+    def _build_vectors(self) -> None:
+        """ build unit vectors, parallel and normal to dimension line """
+        point1, point2 = self._get_dimline_bounds()
+        self.parallel_vector = (Vector(point2) - Vector(point1)).normalize()
+        self.normal_vector = self.parallel_vector.orthogonal()
+
+    @staticmethod
+    def _get_point_on_dimline(point: 'Vertex', dimray: ConstructionRay) -> Vector:
+        """ get the measure target point projection on the dimension line """
+        return dimray.intersect(dimray.orthogonal(point))
+
+    def _draw_dimline(self, layout: 'GenericLayoutType') -> None:
+        """ build dimension line entity """
+        start_point, end_point = self._get_dimline_bounds()
+
+        dimlineext = self.prop('dimlineext')
+        if dimlineext > 0:
+            start_point = start_point - (self.parallel_vector * dimlineext)
+            end_point = end_point + (self.parallel_vector * dimlineext)
+
+        attribs = {
+            'color': self.prop('dimlinecolor'),
+            'layer': self.prop('layer'),
+        }
+        layout.add_line(
+            start=start_point,
+            end=end_point,
+            dxfattribs=attribs,
+        )
+
+    def _draw_extension_lines(self, layout: 'GenericLayoutType') -> None:
+        """ build the extension lines entities """
+        dimextlinegap = self.prop('dimextlinegap')
+        attribs = {
+            'color': self.prop('dimlinecolor'),
+            'layer': self.prop('layer'),
+        }
+
+        for dimline_point, target_point in zip(self.dimline_points, self.measure_points):
+            if distance(dimline_point, target_point) > max(dimextlinegap, DIMENSIONS_MIN_DISTANCE):
+                direction_vector = (target_point - dimline_point).normalize()
+                target_point = target_point - (direction_vector * dimextlinegap)
+                layout.add_line(
+                    start=dimline_point,
+                    end=target_point,
+                    dxfattribs=attribs,
+                )
+
+    def _draw_text(self, layout: 'GenericLayoutType') -> None:
+        """ build the dimension value text entity """
+        attribs = {
+            'height': self.prop('height'),
+            'color': self.prop('textcolor'),
+            'layer': self.prop('layer'),
+            'rotation': self.angle,
+            'style': self.prop('style'),
+        }
+        for section in range(self.section_count):
+            dimvalue_text = self._get_dimvalue_text(section)
+            insert_point = self._get_text_insert_point(section)
+            layout.add_text(
+                text=dimvalue_text,
+                dxfattribs=attribs,
+            ).set_pos(insert_point, align='MIDDLE_CENTER')
+
+    def _get_dimvalue_text(self, section: int) -> str:
+        """ get the dimension value as text, distance from point1 to point2 """
+        override = self.text_override[section]
+        if len(override):
+            return override
+        point1, point2 = self._get_section_points(section)
+
+        dimvalue = distance(point1, point2) * self.prop('scale')
+        return self.format_dimtext(dimvalue)
+
+    def _get_text_insert_point(self, section: int) -> Vector:
+        """ get the dimension value text insert point """
+        point1, point2 = self._get_section_points(section)
+        dist = self.prop('height') / 2. + self.prop('textabove')
+        return lerp(point1, point2) + (self.normal_vector * dist)
+
+    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
+        """ insert the dimension line ticks, (markers on the dimension line) """
+        attribs = {
+            'xscale': self.prop('tickfactor'),
+            'yscale': self.prop('tickfactor'),
+            'layer': self.prop('layer'),
+        }
+
+        def add_tick(index: int, rotate: bool = False) -> None:
+            """ build the insert-entity for the tick block """
+            attribs['rotation'] = self.angle + (180. if rotate else 0.)
+            layout.add_blockref(
+                insert=self._get_dimline_point(index),
+                name=self.prop('tick'),
+                dxfattribs=attribs,
+            )
+
+        if self.prop('tick2x'):
+            for index in range(0, self.point_count - 1):
+                add_tick(index, False)
+            for index in range(1, self.point_count):
+                add_tick(index, True)
+        else:
+            for index in range(self.point_count):
+                add_tick(index, False)
+
+
+class AngularDimension(_DimensionBase):
+    """
+    Draw an angle dimensioning line at dimline pos from start to end, dimension text is the angle build of the three
+    points start-center-end.
+
+    """
+    DEG = ANGLE_DEG
+    GRAD = ANGLE_GRAD
+    RAD = ANGLE_RAD
+
+    def __init__(self, pos: 'Vertex', center: 'Vertex', start: 'Vertex', end: 'Vertex',
+                 dimstyle: str = 'angle.deg', layer: str = None, roundval: int = None):
+        """
+        AngularDimension constructor.
+
+        Args:
+            pos: location as (x, y) tuple of dimension line, line goes through this point
+            center: center point as (x, y) tuple of angle
+            start: line from center to start is the first side of the angle
+            end: line from center to end is the second side of the angle
+            dimstyle: dimstyle name, 'Default' - style is the default value
+            layer: dimension line layer, override the default value of dimstyle
+            roundval: count of decimal places
+
+        """
+        super().__init__(dimstyle, layer, roundval)
+        self.dimlinepos = Vector(pos)
+        self.center = Vector(center)
+        self.start = Vector(start)
+        self.end = Vector(end)
+
+    def _setup(self) -> None:
+        """ setup calculation values """
+        self.pos_radius = distance(self.center, self.dimlinepos)  # type: float
+        self.radius = distance(self.center, self.start)  # type: float
+        self.start_vector = (self.start - self.center).normalize()  # type: Vector
+        self.end_vector = (self.end - self.center).normalize()  # type: Vector
+        self.start_angle = self.start_vector.angle  # type: float
+        self.end_angle = self.end_vector.angle  # type: float
+
+    def render(self, layout: 'GenericLayoutType') -> None:
+        """ build dimension line object with basic dxf entities """
+
+        self._setup()
+        self._draw_dimension_line(layout)
+        if self.prop('dimextline'):
+            self._draw_extension_lines(layout)
+        self._draw_dimension_text(layout)
+        self._draw_ticks(layout)
+
+    def _draw_dimension_line(self, layout: 'GenericLayoutType') -> None:
+        """ draw the dimension line from start- to endangle. """
+        layout.add_arc(
+            radius=self.pos_radius,
+            center=self.center,
+            start_angle=degrees(self.start_angle),
+            end_angle=degrees(self.end_angle),
+            dxfattribs={
+                'layer': self.prop('layer'),
+                'color': self.prop('dimlinecolor'),
+            }
+        )
+
+    def _draw_extension_lines(self, layout: 'GenericLayoutType') -> None:
+        """ build the extension lines entities """
+        for vector in [self.start_vector, self.end_vector]:
+            layout.add_line(
+                start=self._get_extline_start(vector),
+                end=self._get_extline_end(vector),
+                dxfattribs={
+                    'layer': self.prop('layer'),
+                    'color': self.prop('dimextlinecolor'),
+                }
+            )
+
+    def _get_extline_start(self, vector: Vector) -> Vector:
+        return self.center + (vector * self.prop('dimextlinegap'))
+
+    def _get_extline_end(self, vector: Vector) -> Vector:
+        return self.center + (vector * self.pos_radius)
+
+    def _draw_dimension_text(self, layout: 'GenericLayoutType') -> None:
+        attribs = {
+            'height': self.prop('height'),
+            'rotation': degrees((self.start_angle + self.end_angle) / 2 - pi / 2.),
+            'layer': self.prop('layer'),
+            'style': self.prop('style'),
+            'color': self.prop('textcolor'),
+        }
+        layout.add_text(
+            text=self._get_dimtext(),
+            dxfattribs=attribs,
+        ).set_pos(self._get_text_insert_point(), align='MIDDLE_CENTER')
+
+    def _get_text_insert_point(self) -> Vector:
+        midvector = ((self.start_vector + self.end_vector) / 2.).normalize()
+        length = self.pos_radius + self.prop('textabove') + self.prop('height') / 2.
+        return self.center + (midvector * length)
+
+    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
+        attribs = {
+            'xscale': self.prop('tickfactor'),
+            'yscale': self.prop('tickfactor'),
+            'layer': self.prop('layer'),
+        }
+        for vector, mirror in [(self.start_vector, False), (self.end_vector, self.prop('tick2x'))]:
+            insert_point = self.center + (vector * self.pos_radius)
+            rotation = vector.angle + pi / 2.
+            attribs['rotation'] = degrees(rotation + (pi if mirror else 0.))
+            layout.add_blockref(
+                insert=insert_point,
+                name=self.prop('tick'),
+                dxfattribs=attribs,
+            )
+
+    def _get_dimtext(self) -> str:
+        # set scale = ANGLE_DEG for degrees (circle = 360 deg)
+        # set scale = ANGLE_GRAD for grad(circle = 400 grad)
+        # set scale = ANGLE_RAD for rad(circle = 2*pi)
+        angle = (self.end_angle - self.start_angle) * self.prop('scale')
+        return self.format_dimtext(angle)
+
+
+class ArcDimension(AngularDimension):
+    """
+    Arc is defined by start- and endpoint on arc and the center point, or by three points lying on the arc if acr3points
+    is True. Measured length goes from start- to endpoint. The dimension line goes through the dimlinepos.
+
+    """
+
+    def __init__(self, pos: 'Vertex', center: 'Vertex', start: 'Vertex', end: 'Vertex', arc3points: bool = False,
+                 dimstyle: str = 'Default', layer: str = None, roundval: int = None):
+        """
+        Args:
+            pos: location as (x, y) tuple of dimension line, line goes through this point
+            center: center point of arc
+            start: start point of arc
+            end: end point of arc
+            arc3points: if **True** arc is defined by three points on the arc (center, start, end)
+            dimstyle: dimstyle name, 'Default' - style is the default value
+            layer: dimension line layer, override the default value of dimstyle
+            roundval: count of decimal places
+
+        """
+        super().__init__(pos, center, start, end, dimstyle, layer, roundval)
+        self.arc3points = arc3points
+
+    def _setup(self) -> None:
+        super()._setup()
+        if self.arc3points:
+            self.center = center_of_3points_arc(self.center, self.start, self.end)
+
+    def _get_extline_start(self, vector: Vector) -> Vector:
+        return self.center + (vector * (self.radius + self.prop('dimextlinegap')))
+
+    def _get_extline_end(self, vector: Vector) -> Vector:
+        return self.center + (vector * self.pos_radius)
+
+    def _get_dimtext(self) -> str:
+        arc_length = (self.end_angle - self.start_angle) * self.radius * self.prop('scale')
+        return self.format_dimtext(arc_length)
+
+
+class RadialDimension(_DimensionBase):
+    """
+    Draw a radius dimension line from `target` in direction of `center` with length drawing units. RadialDimension has
+    a special tick!!
+    """
+
+    def __init__(self, center: 'Vertex', target: 'Vertex', length: float = 1., dimstyle: str = 'Default',
+                 layer: str = None, roundval: int = None):
+        """
+        Args:
+            center: center point of radius
+            target: target point of radius
+            length: length of radius arrow (drawing length)
+            dimstyle: dimstyle name, 'Default' - style is the default value
+            layer: dimension line layer, override the default value of dimstyle
+            roundval: count of decimal places
+
+        """
+        super().__init__(dimstyle, layer, roundval)
+        self.center = Vector(center)
+        self.target = Vector(target)
+        self.length = float(length)
+
+    def _setup(self) -> None:
+        self.target_vector = (self.target - self.center).normalize()  # type: Vector
+        self.radius = distance(self.center, self.target)  # type: float
+
+    def render(self, layout: 'GenericLayoutType') -> None:
+        """ build dimension line object with basic dxf entities """
+        self._setup()
+        self._draw_dimension_line(layout)
+        self._draw_dimension_text(layout)
+        self._draw_ticks(layout)
+
+    def _draw_dimension_line(self, layout: 'GenericLayoutType') -> None:
+        start_point = self.center + (self.target_vector * (self.radius - self.length))
+        layout.add_line(
+            start=start_point, end=self.target,
+            dxfattribs={
+                'color': self.prop('dimlinecolor'),
+                'layer': self.prop('layer'),
+            },
+        )
+
+    def _draw_dimension_text(self, layout: 'GenericLayoutType') -> None:
+        layout.add_text(
+            text=self._get_dimtext(),
+            dxfattribs={
+                'height': self.prop('height'),
+                'rotation': self.target_vector.angle_deg,
+                'layer': self.prop('layer'),
+                'style': self.prop('style'),
+                'color': self.prop('textcolor'),
+            }
+        ).set_pos(self._get_insert_point(), align='MIDDLE_RIGHT')
+
+    def _get_insert_point(self) -> Vector:
+        return self.target - (self.target_vector * (self.length + self.prop('textabove')))
+
+    def _get_dimtext(self) -> str:
+        return self.format_dimtext(self.radius * self.prop('scale'))
+
+    def _draw_ticks(self, layout: 'GenericLayoutType') -> None:
+        layout.add_blockref(
+            insert=self.target,
+            name='DIMTICK_RADIUS',
+            dxfattribs={
+                'rotation': self.target_vector.angle_deg + 180,
+                'xscale': self.prop('tickfactor'),
+                'yscale': self.prop('tickfactor'),
+                'layer': self.prop('layer'),
+            }
+        )
+
+
+def center_of_3points_arc(point1: 'Vertex', point2: 'Vertex', point3: 'Vertex') -> Vector:
+    """
+    Calc center point of 3 point arc. ConstructionCircle is defined by 3 points on the circle: point1, point2 and point3.
+    """
+    ray1 = ConstructionRay(point1, point2)
+    ray2 = ConstructionRay(point1, point3)
+    midpoint1 = lerp(point1, point2)
+    midpoint2 = lerp(point1, point3)
+    center_ray1 = ray1.orthogonal(midpoint1)
+    center_ray2 = ray2.orthogonal(midpoint2)
+    return center_ray1.intersect(center_ray2)

+ 277 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/importer.py

@@ -0,0 +1,277 @@
+# Purpose: Import data from another DXF drawing
+# Created: 27.04.13
+# Copyright (c) 2013-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Dict, Iterable, List, cast
+from ezdxf.query import name_query
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Drawing, DXFEntity, Insert, BlockRecord, Layout
+
+
+class Importer:
+    def __init__(self, source: 'Drawing', target: 'Drawing', strict_mode: bool = True):
+        self.source = source  # type: Drawing
+        self.target = target  # type: Drawing
+        self._renamed_blocks = {}  # type: Dict[str, str]
+        self._handle_translation_table = {}  # type: Dict[str, str]
+        self._requires_data_from_objects_section = []  # type: List[DXFEntity]
+        if strict_mode and not self.is_compatible():
+            raise TypeError("DXF drawings are not compatible. Source version {}; Target version {}".format(
+                source.dxfversion, target.dxfversion))
+
+    def is_compatible(self) -> bool:
+        if self.source.dxfversion == self.target.dxfversion:
+            return True
+        # The basic DXF structure has been changed with version AC1012 (AutoCAD R13)
+        # It is not possible to copy from R12 to a newer version and
+        # it is not possible to copy from R13 or a newer version to R12.
+        if self.source.dxfversion == 'AC1009' or self.target.dxfversion == 'AC1009':
+            return False
+        # It is always possible to copy from older to newer versions (except R12).
+        if self.target.dxfversion > self.source.dxfversion:
+            return True
+        # It is possible to copy an entity from a newer to an older versions, if the entity is defined for both versions
+        # (like LINE, CIRCLE, ...), but this can not be granted by default. Enable this feature by
+        # Importer(s, t, strict_mode=False).
+        return False
+
+    def import_all(self, table_conflict: str = "discard", block_conflict: str = "discard") -> None:
+        self.import_tables(conflict=table_conflict)
+        self.import_blocks(conflict=block_conflict)
+        self.import_modelspace_entities()
+        self.import_required_data_from_objects_section()
+
+    def import_modelspace_entities(self, query: str = "*") -> None:
+        import_entities = self.source.modelspace().query(query)
+        target_modelspace = self.target.modelspace()
+        new_handle = self.target._handles.next
+        for entity in import_entities:  # entity is GraphicEntity() or inherited
+            new_entry_tags = entity.tags.clone()  # create a new tag list
+            handle = new_handle()
+            new_entry_tags.replace_handle(handle)
+            self.target.entitydb[handle] = new_entry_tags  # add tag list to entity database
+            new_entity = self.target.dxffactory.wrap_entity(new_entry_tags)
+            target_modelspace.add_entity(new_entity)  # add entity to modelspace
+            self.entity_post_processing(new_entity)
+
+    def entity_post_processing(self, dxf_entity: 'DXFEntity') -> None:
+        dxftype = dxf_entity.dxftype()
+        if dxftype == "INSERT":
+            self.resolve_block_ref(cast('Insert', dxf_entity))
+        elif dxftype == 'IMAGE':
+            self._requires_data_from_objects_section.append(cast('Image', dxf_entity))
+
+    def resolve_block_ref(self, block_ref: 'Insert') -> None:
+        old_block_name = block_ref.dxf.name
+        new_block_name = self._renamed_blocks.get(old_block_name, old_block_name)
+        block_ref.dxf.name = new_block_name
+
+    def import_blocks(self, query: str = "*", conflict: str = "discard") -> None:
+        """ Import block definitions.
+
+        Args:
+            query: names of blocks to import, "*" for all
+            conflict: discard|replace|rename
+
+        """
+        has_block_records = self.target.dxfversion > 'AC1009'
+
+        def import_block_record(block_layout):
+            if not has_block_records:
+                return
+            block_record_handle = block_layout.block_record_handle
+            if block_record_handle != '0':
+                block_record_handle = self.import_tags(block_record_handle)
+                self.target.sections.tables.block_records._append_entry_handle(block_record_handle)
+                block_layout.set_block_record_handle(block_record_handle)
+                block_record = self.target.dxffactory.wrap_handle(block_record_handle)
+                _cleanup_block_record(block_record)
+
+        def rename(block):
+            counter = 1
+            old_name = block.name
+            while True:
+                new_name = old_name + "_%d" % counter
+                if new_name in existing_block_names:
+                    counter += 1
+                else:
+                    block.name = new_name
+                    break
+            rename_block_record(block)
+            existing_block_names.add(new_name)
+            self._renamed_blocks[old_name] = new_name
+
+        def rename_block_record(block_layout):
+            if not has_block_records:
+                return
+            block_record_handle = block_layout.block_record_handle
+            if block_record_handle != '0':
+                block_record = self.target.dxffactory.wrap_handle(block_record_handle)
+                block_record.dxf.name = block_layout.name
+
+        def import_block_layout(source_block_layout):
+            head_handle = self.import_tags(source_block_layout._block_handle)
+            tail_handle = self.import_tags(source_block_layout._endblk_handle)
+            target_block_layout = self.target.dxffactory.new_block_layout(head_handle, tail_handle)
+            import_block_record(target_block_layout)
+            import_block_entities(source_block_layout, target_block_layout)
+            return target_block_layout
+
+        def import_block_entities(source_block_layout, target_block_layout):
+            for entity in source_block_layout:
+                target_handle = self.import_tags(entity.dxf.handle)
+                new_entity = self.target.dxffactory.wrap_handle(target_handle)
+                target_block_layout.add_entity(new_entity)
+                dxftype = new_entity.dxftype()
+                if dxftype == 'INSERT':  # maybe a reference to a renamed block
+                    resolve_block_references.append(new_entity)
+                elif dxftype == 'IMAGE':
+                    self._requires_data_from_objects_section.append(new_entity)
+
+        resolve_block_references = []
+        existing_block_names = set(block.name for block in self.target.blocks)
+        # Do not import blocks associated to layouts and model space
+        layout_block_names = frozenset(_get_layout_block_names(self.source))
+        block_names_without_layouts = frozenset(block.name for block in self.source.blocks) - layout_block_names
+        import_block_names = frozenset(name_query(block_names_without_layouts, query))
+
+        for block in self.source.blocks:  # blocks is a list, access by blocks[name] is slow
+            block_name = block.name
+            if block_name not in import_block_names:
+                continue
+            if block_name not in existing_block_names:
+                target_block_layout = import_block_layout(block)
+                self.target.blocks.add(target_block_layout)
+            else:  # we have a name conflict
+                if conflict == 'discard':
+                    continue
+                elif conflict == 'rename':
+                    target_block_layout = import_block_layout(block)
+                    rename(target_block_layout)
+                    self.target.blocks.add(target_block_layout)
+                elif conflict == 'replace':
+                    target_block_layout = import_block_layout(block)
+                    self.target.blocks.add(target_block_layout)
+                else:
+                    raise ValueError("'{}' is an invalid value for parameter conflict.".format(conflict))
+
+        # update renamed block names
+        for block_ref in resolve_block_references:
+            self.resolve_block_ref(block_ref)
+
+    def import_tables(self, query: str = "*", conflict: str = "discard") -> None:
+        """ Import table entries.
+
+        Args:
+            query: names of tables to import, "*" for all
+            conflict: discard|replace
+        """
+        table_names = [table.name for table in self.source.sections.tables if table.name != 'block_records']
+        for table_name in name_query(table_names, query):
+            self.import_table(table_name, query="*", conflict=conflict)
+
+    def import_table(self, name: str, query: str = "*", conflict: str = "discard") -> None:
+        """ Import specific entries from a table.
+        Args:
+            name: valid table names are 'layers', 'linetypes', 'appids', 'dimstyles',
+                  'styles', 'ucs', 'views', 'viewports' except 'block_records'
+                  as defined in ezdxf.table.TABLENAMES
+            query: table name query, "*" for all
+            conflict: discard|replace
+
+        """
+        if conflict not in ('replace', 'discard'):
+            raise ValueError("Unknown value '{}' for parameter 'conflict'.".format(conflict))
+        if name == 'block_records':
+            raise ValueError("Can not import whole block_records table, block_records will be imported as required.")
+        try:
+            source_table = self.source.sections.tables[name]
+        except KeyError:
+            raise ValueError("Source drawing has no table '{}'.".format(name))
+        try:
+            target_table = self.target.sections.tables[name]
+        except KeyError:
+            raise ValueError("Table '{}' does not exists in the target drawing. "
+                             "Table creation in the target drawing not implemented yet!".format(name))
+        source_entry_names = (entry.dxf.name for entry in source_table)
+        for entry_name in name_query(source_entry_names, query):
+            table_entry = source_table.get(entry_name)
+            if table_entry.dxf.name in target_table:
+                if conflict == 'discard':
+                    continue
+                else:  # replace existing entry
+                    target_table.remove(table_entry.dxf.name)
+            new_handle = self.import_tags(table_entry.dxf.handle)
+            target_table._append_entry_handle(new_handle)
+
+    def import_tags(self, source_handle: str) -> str:
+        """
+        Clone tags from source drawing, give it a new valid handle for the target drawing
+        and insert tags into the entity database of the target drawing. Returns the target handle.
+        Avoids duplicate imports of the same database entity.
+        """
+        source_db = self.source.entitydb
+        target_db = self.target.entitydb
+        next_target_handle = self.target._handles.next
+
+        def clone_tags(source_handle):
+            new_tags = source_db[source_handle].clone()
+            target_handle = next_target_handle()
+            new_tags.replace_handle(target_handle)
+            target_db[target_handle] = new_tags
+            self._handle_translation_table[source_handle] = target_handle
+            return target_handle, new_tags
+
+        main_target_handle = self._handle_translation_table.get(source_handle, None)
+        prev_tags = None
+
+        if main_target_handle is None:
+            while True:
+                target_handle, new_tags = clone_tags(source_handle)
+                if main_target_handle is None:  # just the first tag list is relevant
+                    main_target_handle = target_handle
+
+                if prev_tags is not None:
+                    prev_tags.link = target_handle
+
+                if new_tags.link is None:
+                    break
+                else:  # clone linked tags
+                    source_handle = new_tags.link
+                    prev_tags = new_tags
+
+        return main_target_handle
+
+    def import_required_data_from_objects_section(self):  # TODO
+        for entity in self._requires_data_from_objects_section:
+            # for IMAGE import IMAGE_DEF
+            pass
+
+
+def _cleanup_block_record(block_record: 'BlockRecord') -> None:
+    def remove_tags(tags, code):
+        del_tags = [tag for tag in tags if tag.code == code]
+        for tag in del_tags:
+            tags.remove(tag)
+
+    if hasattr(block_record.tags, 'get_app_data'):
+        try:  # BLKREFS are invalid handles to INSERT entities in the source drawing
+            block_refs = block_record.tags.get_app_data("{BLKREFS")
+        except ValueError:  # has no block references
+            pass
+        else:
+            remove_tags(block_refs, 331)
+        # strip preview image and save space
+        subclass = block_record.tags.get_subclass('AcDbBlockTableRecord')
+        remove_tags(subclass, 310)
+
+
+def _get_layout_block_names(dwg: 'Drawing') -> Iterable[str]:
+    def get_block_record_name(layout: 'Layout') -> str:
+        block_record = dwg.dxffactory.wrap_handle(layout._block_record_handle)
+        return block_record.dxf.name
+
+    if dwg.dxfversion <= 'AC1009':
+        return '$MODEL_SPACE', '$PAPER_SPACE'
+    return (get_block_record_name(layout) for layout in dwg.layouts)

+ 203 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/menger_sponge.py

@@ -0,0 +1,203 @@
+# Purpose: menger sponge addon for ezdxf
+# Created: 06.12.2016
+# Copyright (c) 2016 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, List, Tuple
+from ezdxf.render.mesh import MeshBuilder, MeshVertexMerger
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, GenericLayoutType, Matrix44
+
+all_cubes_size_3_template = [
+    (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (1, 1, 0), (2, 1, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0),
+    (0, 0, 1), (1, 0, 1), (2, 0, 1), (0, 1, 1), (1, 1, 1), (2, 1, 1), (0, 2, 1), (1, 2, 1), (2, 2, 1),
+    (0, 0, 2), (1, 0, 2), (2, 0, 2), (0, 1, 2), (1, 1, 2), (2, 1, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2),
+]
+
+original_menger_cubes = [
+    (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (2, 1, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0),
+    (0, 0, 1), (2, 0, 1), (0, 2, 1), (2, 2, 1),
+    (0, 0, 2), (1, 0, 2), (2, 0, 2), (0, 1, 2), (2, 1, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2),
+]
+
+menger_v1 = [
+    (0, 0, 0), (2, 0, 0), (1, 1, 0), (0, 2, 0), (2, 2, 0),
+    (1, 0, 1), (0, 1, 1), (2, 1, 1), (1, 2, 1),
+    (0, 0, 2), (2, 0, 2), (1, 1, 2), (0, 2, 2), (2, 2, 2),
+]
+
+menger_v2 = [
+    (1, 0, 0), (0, 1, 0), (2, 1, 0), (1, 2, 0),
+    (0, 0, 1), (2, 0, 1), (1, 1, 1), (0, 2, 1), (2, 2, 1),
+    (1, 0, 2), (0, 1, 2), (2, 1, 2), (1, 2, 2),
+]
+
+jerusalem_cube = [
+    (0, 0, 0), (1, 0, 0), (2, 0, 0), (3, 0, 0), (4, 0, 0), (0, 1, 0), (1, 1, 0), (3, 1, 0), (4, 1, 0), (0, 2, 0),
+    (4, 2, 0), (0, 3, 0), (1, 3, 0), (3, 3, 0), (4, 3, 0), (0, 4, 0), (1, 4, 0), (2, 4, 0), (3, 4, 0), (4, 4, 0),
+    (0, 0, 1), (1, 0, 1), (3, 0, 1), (4, 0, 1), (0, 1, 1), (1, 1, 1), (3, 1, 1), (4, 1, 1), (0, 3, 1), (1, 3, 1),
+    (3, 3, 1), (4, 3, 1), (0, 4, 1), (1, 4, 1), (3, 4, 1), (4, 4, 1), (0, 0, 2), (4, 0, 2), (0, 4, 2), (4, 4, 2),
+    (0, 0, 3), (1, 0, 3), (3, 0, 3), (4, 0, 3), (0, 1, 3), (1, 1, 3), (3, 1, 3), (4, 1, 3), (0, 3, 3), (1, 3, 3),
+    (3, 3, 3), (4, 3, 3), (0, 4, 3), (1, 4, 3), (3, 4, 3), (4, 4, 3), (0, 0, 4), (1, 0, 4), (2, 0, 4), (3, 0, 4),
+    (4, 0, 4), (0, 1, 4), (1, 1, 4), (3, 1, 4), (4, 1, 4), (0, 2, 4), (4, 2, 4), (0, 3, 4), (1, 3, 4), (3, 3, 4),
+    (4, 3, 4), (0, 4, 4), (1, 4, 4), (2, 4, 4), (3, 4, 4), (4, 4, 4),
+]
+
+building_schemas = [
+    original_menger_cubes,
+    menger_v1,
+    menger_v2,
+    jerusalem_cube,
+]
+
+# subdivide level in order of building_schemas
+cube_sizes = [3., 3., 3., 5.]
+
+# 8 corner vertices
+_cube_vertices = [
+    (0, 0, 0),
+    (1, 0, 0),
+    (1, 1, 0),
+    (0, 1, 0),
+    (0, 0, 1),
+    (1, 0, 1),
+    (1, 1, 1),
+    (0, 1, 1),
+]
+
+# 6 cube faces
+cube_faces = [
+    [0, 3, 2, 1],
+    [4, 5, 6, 7],
+    [0, 1, 5, 4],
+    [1, 2, 6, 5],
+    [3, 7, 6, 2],
+    [0, 4, 7, 3],
+]
+
+
+class MengerSponge:
+    def __init__(self, location: 'Vertex' = (0., 0., 0.), length: float = 1., level: int = 1, kind: int = 0):
+        self.cube_definitions = _menger_sponge(location=location, length=length, level=level, kind=kind)
+
+    def vertices(self) -> Iterable['Vertex']:
+        """
+        Yields the cube vertices as list of (x, y, z) tuples.
+
+        """
+        for location, length in self.cube_definitions:
+            x, y, z = location
+            yield [(x + xf * length, y + yf * length, z + zf * length) for xf, yf, zf in _cube_vertices]
+
+    __iter__ = vertices
+
+    @staticmethod
+    def faces() -> List[List[int]]:
+        """
+        Returns list of cube faces. All cube vertices have the same order, so one faces list fits them all.
+
+        """
+        return cube_faces
+
+    def render(self, layout: 'GenericLayoutType', merge: bool = False, dxfattribs: dict = None,
+               matrix: 'Matrix44' = None) -> None:
+        """
+        Renders the menger sponge into layout, set merge == *True* for rendering the whole menger sponge into one MESH
+        entity, set merge to *False* for rendering the individual cubes of the menger sponge as MESH entities.
+
+        Args:
+            layout: target layout (ezdxf)
+            merge: *True* for one MESH entity, *False* for individual MESH entities per cube
+            dxfattribs: DXF attributes for the MESH entities
+            matrix: apply transformation matrix at rendering
+
+        """
+        if merge:
+            mesh = self.mesh()
+            mesh.render(layout, dxfattribs=dxfattribs, matrix=matrix)
+        else:
+            for cube in self.cubes():
+                cube.render(layout, dxfattribs, matrix=matrix)
+
+    def cubes(self) -> Iterable[MeshBuilder]:
+        """
+        Generates all cubes of the menger sponge as individual MeshBuilder() objects.
+
+        Yields: MeshBuilder()
+
+        """
+        faces = self.faces()
+        for vertices in self:
+            mesh = MeshBuilder()
+            mesh.add_mesh(vertices=vertices, faces=faces)
+            yield mesh
+
+    def mesh(self) -> MeshVertexMerger:
+        """
+        Returns geometry as one single MESH entity.
+
+        Returns: MeshVertexMerger()
+
+        """
+        faces = self.faces()
+        mesh = MeshVertexMerger()
+        for vertices in self:
+            mesh.add_mesh(vertices=vertices, faces=faces)
+        return mesh
+
+
+def _subdivide(location: 'Vertex' = (0., 0., 0.), length: float = 1., kind: int = 0) -> List[Tuple['Vertex', float]]:
+    """
+    Divides a cube in sub-cubes and keeps only cubes determined by the building schema.
+
+    All sides are parallel to x-, y- and z-axis, location is a (x, y, z) tuple and represents the coordinates of the
+    lower left corner (nearest to the axis origin) of the cube, length is the side-length of the cube
+
+    Args:
+        location: (x, y, z) tuple, coordinates of the lower left corner of the cube
+        length: side length of the cube
+        kind: int for 0: original menger sponge; 1: Variant XOX; 2: Variant OXO; 3: Jerusalem Cube;
+
+    Returns: list of sub-cubes (location, length)
+
+    """
+
+    init_x, init_y, init_z = location
+    step_size = float(length) / cube_sizes[kind]
+    remaining_cubes = building_schemas[kind]
+
+    def sub_location(indices) -> Tuple[float, float, float]:
+        x, y, z = indices
+        return (init_x + x * step_size,
+                init_y + y * step_size,
+                init_z + z * step_size)
+
+    return [(sub_location(indices), step_size) for indices in remaining_cubes]
+
+
+def _menger_sponge(location: 'Vertex' = (0., 0., 0.), length: float = 1., level: int = 1, kind: int = 0) -> List[Tuple['Vertex', float]]:
+    """
+    Builds a menger sponge for given level.
+
+    Args:
+        location: (x, y, z) tuple, coordinates of the lower left corner of the cube
+        length: side length of the cube
+        level: level of menger sponge, has to be 1 or bigger
+        kind: int for 0: original menger sponge; 1: Variant XOX; 2: Variant OXO; 3: Jerusalem Cube;
+
+    Returns: list of sub-cubes (location, length)
+
+    """
+    kind = int(kind)
+    if kind not in (0, 1, 2, 3):
+        raise ValueError('kind has to be 0, 1, 2 or 3.')
+    level = int(level)
+    if level < 1:
+        raise ValueError("level has to be 1 or bigger.")
+    cubes = _subdivide(location, length, kind=kind)
+    for _ in range(level - 1):
+        next_level_cubes = []
+        for location, length in cubes:
+            next_level_cubes.extend(_subdivide(location, length, kind=kind))
+        cubes = next_level_cubes
+    return cubes

+ 19 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/mixins.py

@@ -0,0 +1,19 @@
+# Copyright (c) 2011, Manfred Moitzi
+# License: MIT License
+
+
+class SubscriptAttributes:
+    def __getitem__(self, item):
+        if hasattr(self, item):
+            return getattr(self, item)
+        else:
+            raise KeyError(item)
+        
+    def __setitem__(self, key, value):
+        if hasattr(self, key):
+            setattr(self, key, value)
+        else:
+            raise KeyError(key)
+
+    def __contains__(self, item):
+        return hasattr(self, item)

+ 158 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/mtext.py

@@ -0,0 +1,158 @@
+# Purpose: The MText entity is a composite entity, consisting of basic TEXT entities.
+# Created: 09.03.2010, adapted 2018 for ezdxf
+# Copyright (c) 2010-2018, Manfred Moitzi
+# License: MIT License
+"""
+MText -- MultiLine-Text-Entity, created by simple TEXT-Entities.
+
+MTEXT was introduced in R13, so this is a replacement with multiple simple
+TEXT entities. Supports valign (TOP, MIDDLE, BOTTOM), halign (LEFT, CENTER,
+RIGHT), rotation for an arbitrary (!) angle and mirror.
+
+"""
+from typing import TYPE_CHECKING
+import math
+from .mixins import SubscriptAttributes
+import ezdxf
+from ezdxf.lldxf import const
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, GenericLayoutType
+
+
+class MText(SubscriptAttributes):
+    """
+    MultiLine-Text buildup with simple Text-Entities.
+
+
+    Caution: align point is always the insert point, I don't need a second
+    alignpoint because horizontal alignment FIT, ALIGN, BASELINE_MIDDLE is not
+    supported.
+
+    linespacing -- linespacing in percent of height, 1.5 = 150% = 1+1/2 lines
+
+    supported align values:
+        'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT'
+        'MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'
+        'TOP_LEFT',    'TOP_CENTER',    'TOP_RIGHT'
+
+    """
+    MIRROR_X = const.MIRROR_X
+    MIRROR_Y = const.MIRROR_Y
+    TOP = const.TOP
+    MIDDLE = const.MIDDLE
+    BOTTOM = const.BOTTOM
+    LEFT = const.LEFT
+    CENTER = const.CENTER
+    RIGHT = const.RIGHT
+    VALID_ALIGN = frozenset([
+        'BOTTOM_LEFT',
+        'BOTTOM_CENTER',
+        'BOTTOM_RIGHT',
+        'MIDDLE_LEFT',
+        'MIDDLE_CENTER',
+        'MIDDLE_RIGHT',
+        'TOP_LEFT',
+        'TOP_CENTER',
+        'TOP_RIGHT',
+    ])
+
+    def __init__(self, text: str, insert: 'Vertex', linespacing: float = 1.5, **kwargs):
+        self.textlines = text.split('\n')
+        self.insert = insert
+        self.linespacing = linespacing
+        if 'align' in kwargs:
+            self.align = kwargs.get('align', 'TOP_LEFT').upper()
+        else:  # support for compatibility: valign, halign
+            halign = kwargs.get('halign', 0)
+            valign = kwargs.get('valign', 3)
+            self.align = const.TEXT_ALIGNMENT_BY_FLAGS.get((halign, valign), 'TOP_LEFT')
+
+        if self.align not in MText.VALID_ALIGN:
+            raise ezdxf.DXFValueError('Invalid align parameter: {}'.format(self.align))
+
+        self.height = kwargs.get('height', 1.0)
+        self.style = kwargs.get('style', 'STANDARD')
+        self.oblique = kwargs.get('oblique', 0.0)  # in degree
+        self.rotation = kwargs.get('rotation', 0.0)  # in degree
+        self.xscale = kwargs.get('xscale', 1.0)
+        self.mirror = kwargs.get('mirror', 0)  # renamed to text_generation_flag in ezdxf
+        self.layer = kwargs.get('layer', '0')
+        self.color = kwargs.get('color', const.BYLAYER)
+
+    @property
+    def lineheight(self) -> float:
+        """ Absolute linespacing in drawing units. 
+        """
+        return self.height * self.linespacing
+
+    def render(self, layout: 'GenericLayoutType') -> None:
+        """ Create the DXF-TEXT entities. 
+        """
+        textlines = self.textlines
+        if len(textlines) > 1:
+            if self.mirror & const.MIRROR_Y:
+                textlines.reverse()
+            for linenum, text in enumerate(textlines):
+                alignpoint = self._get_align_point(linenum)
+                layout.add_text(
+                    text,
+                    dxfattribs=self._dxfattribs(alignpoint),
+                )
+        elif len(textlines) == 1:
+            layout.add_text(
+                textlines[0],
+                dxfattribs=self._dxfattribs(self.insert),
+            )
+
+    def _get_align_point(self, linenum: int) -> 'Vertex':
+        """ Calculate the align point depending on the line number. 
+        """
+        x = self.insert[0]
+        y = self.insert[1]
+        try:
+            z = self.insert[2]
+        except IndexError:
+            z = 0.
+        # rotation not respected
+
+        if self.align.startswith('TOP'):
+            y -= linenum * self.lineheight
+        elif self.align.startswith('MIDDLE'):
+            y0 = linenum * self.lineheight
+            fullheight = (len(self.textlines) - 1) * self.lineheight
+            y += (fullheight / 2) - y0
+        else:  # BOTTOM
+            y += (len(self.textlines) - 1 - linenum) * self.lineheight
+        return self._rotate((x, y, z))  # consider rotation
+
+    def _rotate(self, alignpoint: 'Vertex') -> 'Vertex':
+        """
+        Rotate alignpoint around insert point about rotation degrees.
+        """
+        dx = alignpoint[0] - self.insert[0]
+        dy = alignpoint[1] - self.insert[1]
+        beta = math.radians(self.rotation)
+        x = self.insert[0] + dx * math.cos(beta) - dy * math.sin(beta)
+        y = self.insert[1] + dy * math.cos(beta) + dx * math.sin(beta)
+        return round(x, 6), round(y, 6), alignpoint[2]
+
+    def _dxfattribs(self, alignpoint: 'Vertex') -> dict:
+        """
+        Build keyword arguments for TEXT entity creation.
+        """
+        halign, valign = const.TEXT_ALIGN_FLAGS.get(self.align)
+        return {
+            'insert': alignpoint,
+            'align_point': alignpoint,
+            'layer': self.layer,
+            'color': self.color,
+            'style': self.style,
+            'height': self.height,
+            'width': self.xscale,
+            'text_generation_flag': self.mirror,
+            'rotation': self.rotation,
+            'oblique': self.oblique,
+            'halign': halign,
+            'valign': valign,
+        }

+ 218 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/sierpinski_pyramid.py

@@ -0,0 +1,218 @@
+# Purpose: sierpinski pyramid
+# Created: 07.12.2016
+# Copyright (c) 2016 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, List, Sequence, Tuple
+import math
+from ezdxf.render.mesh import MeshBuilder, MeshVertexMerger
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, GenericLayoutType, Matrix44
+
+HEIGHT4 = 1. / math.sqrt(2.)  # pyramid4 height (* length)
+HEIGHT3 = math.sqrt(6.) / 3.  # pyramid3 height (* length)
+
+DY1_FACTOR = math.tan(math.pi / 6.) / 2.  # inner circle radius
+DY2_FACTOR = 0.5 / math.cos(math.pi / 6.)  # outer circle radius
+
+
+class SierpinskyPyramid:
+    def __init__(self, location: 'Vertex' = (0., 0., 0.), length: float = 1., level: int = 1, sides: int = 4):
+        self.sides = sides
+        self.pyramid_definitions = sierpinsky_pyramid(location=location, length=length, level=level, sides=sides)
+
+    def vertices(self) -> Iterable['Vertex']:
+        """
+        Yields the pyramid vertices as list of (x, y, z) tuples.
+
+        """
+        for location, length in self.pyramid_definitions:
+            yield self._calc_vertices(location, length)
+
+    __iter__ = vertices
+
+    def _calc_vertices(self, location: 'Vertex', length: float) -> List['Vertex']:
+        """
+        Calculates the pyramid vertices.
+
+        Args:
+            location: location of the pyramid as center point of the base
+            length: pyramid side length
+
+        Returns: list of (x, y, z) tuples
+
+        """
+        len2 = length / 2.
+        x, y, z = location
+        if self.sides == 4:
+            return [
+                (x - len2, y - len2, z),
+                (x + len2, y - len2, z),
+                (x + len2, y + len2, z),
+                (x - len2, y + len2, z),
+                (x, y, z + length * HEIGHT4)
+            ]
+        elif self.sides == 3:
+            dy1 = length * DY1_FACTOR
+            dy2 = length * DY2_FACTOR
+            return [
+                (x - len2, y - dy1, z),
+                (x + len2, y - dy1, z),
+                (x, y + dy2, z),
+                (x, y, z + length * HEIGHT3)
+            ]
+        else:
+            raise ValueError("sides has to be 3 or 4.")
+
+    def faces(self) -> List[Sequence[int]]:
+        """
+        Returns list of pyramid faces. All pyramid vertices have the same order, so one faces list fits them all.
+
+        """
+        if self.sides == 4:
+            return [
+                (0, 1, 2, 3),
+                (0, 1, 4),
+                (1, 2, 4),
+                (2, 3, 4),
+                (3, 0, 4)
+            ]
+        elif self.sides == 3:
+            return [
+                (0, 1, 2),
+                (0, 1, 3),
+                (1, 2, 3),
+                (2, 0, 3)
+            ]
+        else:
+            raise ValueError("sides has to be 3 or 4.")
+
+    def render(self, layout: 'GenericLayoutType', merge: bool = False, dxfattribs: dict = None,
+               matrix: 'Matrix44' = None) -> None:
+        """
+        Renders the sierpinsky pyramid into layout, set merge == *True* for rendering the whole sierpinsky pyramid into
+        one MESH entity, set merge to *False* for rendering the individual pyramids of the sierpinsky pyramid as MESH
+        entities.
+
+        Args:
+            layout: target layout (ezdxf)
+            merge: *True* for one MESH entity, *False* for individual MESH entities per pyramid
+            dxfattribs: DXF attributes for the MESH entities
+            matrix: apply transformation matrix at rendering
+
+        """
+        if merge:
+            mesh = self.mesh()
+            mesh.render(layout, dxfattribs=dxfattribs, matrix=matrix)
+        else:
+            for pyramid in self.pyramids():
+                pyramid.render(layout, dxfattribs, matrix=matrix)
+
+    def pyramids(self) -> Iterable[MeshBuilder]:
+        """
+        Generates all pyramids of the sierpinsky pyramid as individual MeshBuilder() objects.
+
+        Yields: MeshBuilder()
+
+        """
+        faces = self.faces()
+        for vertices in self:
+            mesh = MeshBuilder()
+            mesh.add_mesh(vertices=vertices, faces=faces)
+            yield mesh
+
+    def mesh(self) -> MeshVertexMerger:
+        """
+        Returns geometry as one single MESH entity.
+
+        Returns: MeshVertexMerger()
+
+        """
+        faces = self.faces()
+        mesh = MeshVertexMerger()
+        for vertices in self:
+            mesh.add_mesh(vertices=vertices, faces=faces)
+        return mesh
+
+
+def sierpinsky_pyramid(location: 'Vertex' = (0., 0., 0.),
+                       length: float = 1.,
+                       level: int = 1,
+                       sides: int = 4) -> List[Tuple['Vertex', float]]:
+    """ Build a Sierpinski pyramid.
+
+    Args:
+        location: base center point of the pyramid
+        length: base length of the pyramid
+        level: recursive building levels, has to 1 or bigger
+        sides: 3 or 4 sided pyramids supported
+
+    Returns: list of pyramid vertices
+
+    """
+    level = int(level)
+    if level < 1:
+        raise ValueError("level has to be 1 or bigger.")
+    pyramids = _sierpinsky_pyramid(location, length, sides)
+    for _ in range(level - 1):
+        next_level_pyramids = []
+        for location, length in pyramids:
+            next_level_pyramids.extend(_sierpinsky_pyramid(location, length, sides))
+        pyramids = next_level_pyramids
+    return pyramids
+
+
+def _sierpinsky_pyramid(location: 'Vertex' = (0., 0., 0.),
+                        length: float = 1.,
+                        sides: int = 4) -> List[Tuple['Vertex', float]]:
+    if sides == 3:
+        return sierpinsky_pyramid_3(location, length)
+    elif sides == 4:
+        return sierpinsky_pyramid_4(location, length)
+    else:
+        raise ValueError("sides has to be 3 or 4.")
+
+
+def sierpinsky_pyramid_4(location: 'Vertex' = (0., 0., 0.), length: float = 1.) -> List[Tuple['Vertex', float]]:
+    """ Build a 4-sided Sierpinski pyramid. Pyramid height = length of the base square!
+
+    Args:
+        location: base center point of the pyramid
+        length: base length of the pyramid
+
+    Returns: list of (location, length) tuples, representing the sierpinski pyramid
+
+    """
+    len2 = length / 2
+    len4 = length / 4
+    x, y, z = location
+    return [
+        ((x - len4, y - len4, z), len2),
+        ((x + len4, y - len4, z), len2),
+        ((x - len4, y + len4, z), len2),
+        ((x + len4, y + len4, z), len2),
+        ((x, y, z + len2 * HEIGHT4), len2)
+    ]
+
+
+def sierpinsky_pyramid_3(location: 'Vertex' = (0., 0., 0.), length: float = 1.) -> List[Tuple['Vertex', float]]:
+    """ Build a 3-sided Sierpinski pyramid (tetraeder).
+
+    Args:
+        location: base center point of the pyramid
+        length: base length of the pyramid
+
+    Returns: list of (location, length) tuples, representing the sierpinski pyramid
+
+    """
+    dy1 = length * DY1_FACTOR * 0.5
+    dy2 = length * DY2_FACTOR * 0.5
+    len2 = length / 2
+    len4 = length / 4
+    x, y, z = location
+    return [
+        ((x - len4, y - dy1, z), len2),
+        ((x + len4, y - dy1, z), len2),
+        ((x, y + dy2, z), len2),
+        ((x, y, z + len2 * HEIGHT3), len2)
+    ]

+ 851 - 0
venv/lib/python3.7/site-packages/ezdxf/addons/table.py

@@ -0,0 +1,851 @@
+# Purpose: table, consisting of basic R12 entities
+# Created: 18.03.2010, 2018 adapted for ezdxf
+# Copyright (c) 2010-2018, Manfred Moitzi
+# License: MIT License
+"""
+Table object like a HTML-Table, as composite pf basic DXF R12 entities.
+
+Cells can contain MText or BLOCK references, or you can create your own
+cell type by extending the CustomCell() class.
+Cells can span over columns and rows.
+Text cells can contain text with an arbitrary rotation angle, or letters can be
+stacked from top to bottom.
+
+BlockCells contains block references (INSERT) created from a block
+definition (BLOCK), if the block definition contains attribute definitions
+(ATTDEF), attribs will be added to the block reference (ATTRIB).
+
+"""
+
+from copy import deepcopy
+from abc import abstractmethod
+
+from ezdxf.lldxf import const
+from .mtext import MText
+
+DEFAULT_TABLE_BGLAYER = 'TABLEBACKGROUND'
+DEFAULT_TABLE_FGLAYER = 'TABLECONTENT'
+DEFAULT_TABLE_GRIDLAYER = 'TABLEGRID'
+DEFAULT_TABLE_HEIGHT = 1.0
+DEFAULT_TABLE_WIDTH = 2.5
+DEFAULT_TEXTSTYLE = 'STANDARD'
+DEFAULT_CELL_TEXT_HEIGHT = 0.7
+DEFAULT_CELL_LINESPACING = 1.5
+DEFAULT_CELL_XSCALE = 1.0
+DEFAULT_CELL_YSCALE = 1.0
+DEFAULT_CELL_TEXTCOLOR = const.BYLAYER
+DEFAULT_CELL_BG_COLOR = None
+DEFAULT_CELL_HMARGIN = 0.1
+DEFAULT_CELL_VMARGIN = 0.1
+DEFAULT_BORDER_COLOR = 5
+DEFAULT_BORDER_LINETYPE = "BYLAYER"
+DEFAULT_BORDER_STATUS = True
+DEFAULT_BORDER_PRIORITY = 50
+
+VISIBLE = 1
+HIDDEN = 0
+
+
+class Table(object):
+    """A HTML-table like object.
+
+    The table object contains the table data cells.
+
+    """
+    name = 'TABLE'
+
+    def __init__(self, insert, nrows, ncols, default_grid=True):
+        """
+        Args:
+            insert: insert point as tuple (x, y[, z])
+            nrows: row count
+            ncols: column count
+            default_grid: draw a solid line grid if True, else draw only explicit defined borders, default grid has a
+                          priority of 50.
+
+        """
+        self.insert = insert
+        self.nrows = nrows
+        self.ncols = ncols
+        self.row_heights = [DEFAULT_TABLE_HEIGHT] * nrows
+        self.col_widths = [DEFAULT_TABLE_WIDTH] * ncols
+        self.bglayer = DEFAULT_TABLE_BGLAYER
+        self.fglayer = DEFAULT_TABLE_FGLAYER
+        self.gridlayer = DEFAULT_TABLE_GRIDLAYER
+        self.styles = {'default': Style.get_default_cell_style()}
+        if not default_grid:
+            default_style = self.get_cell_style('default')
+            default_style.set_border_status(False, False, False, False)
+
+        self._cells = {}  # data cells
+        self.frames = []  # border frame objects
+        # data contains the resulting dxf entities
+        self.data = None
+        self.empty_cell = Cell(self)  # represents all empty cells
+
+    def set_col_width(self, column, value):
+        """
+        Set column width to value (in drawing units).
+
+        Args:
+            column: zero based column index
+            value: new column width in drawing units
+        """
+        self.col_widths[column] = float(value)
+
+    def set_row_height(self, row, value):
+        """
+        Set row height to value (in drawing units).
+
+        Args:
+            row: zero based row index
+            value: new row height in drawing units
+        """
+        self.row_heights[row] = float(value)
+
+    def text_cell(self, row, col, text, span=(1, 1), style='default'):
+        """
+        Create a new text cell at position (row, col), with 'text' as
+        content, text can be a multi-line text, use ``'\\n'`` as line
+        separator.
+
+        The cell spans over **span** cells and has the cell style with the
+        name **style**.
+
+        """
+        cell = TextCell(self, text, style=style, span=span)
+        return self.set_cell(row, col, cell)
+
+    def block_cell(self, row, col, blockdef, span=(1, 1), attribs=None, style='default'):
+        """
+        Create a new block cell at position (row, col).
+
+        Content is a block reference inserted by an INSERT entity,
+        attributes will be added if the block definition contains ATTDEF. Assignments
+        are defined by attribs-key to attdef-tag association.
+
+        Example: attribs = {'num': 1} if an ATTDEF with tag=='num' in
+        the block definition exists, an attrib with text=str(1) will be
+        created and added to the insert entity.
+
+        The cell spans over 'span' cells and has the cell style with the
+        name 'style'.
+        """
+        if attribs is None:
+            attribs = {}
+        cell = BlockCell(self, blockdef, style=style, attribs=attribs, span=span)
+        return self.set_cell(row, col, cell)
+
+    def set_cell(self, row, col, cell):
+        """
+        Insert a cell at position (row, col).
+        """
+        row, col = self.validate_index(row, col)
+        self._cells[row, col] = cell
+        return cell
+
+    def get_cell(self, row, col):
+        """
+        Get cell at position (row, col).
+        """
+        row, col = self.validate_index(row, col)
+        try:
+            return self._cells[row, col]
+        except KeyError:
+            return self.empty_cell  # empty cell with default style
+
+    def validate_index(self, row, col):
+        row = int(row)
+        col = int(col)
+        if row < 0 or row >= self.nrows or \
+           col < 0 or col >= self.ncols:
+            raise IndexError('cell index out of range')
+        return row, col
+
+    def frame(self, row, col, width=1, height=1, style='default'):
+        """
+        Create a Frame object which frames the cell area starting at(row, col) covering 'width' columns and 'height' rows.
+        """
+        frame = Frame(self, pos=(row, col), span=(height, width),
+                      style=style)
+        self.frames.append(frame)
+        return frame
+
+    def new_cell_style(self, name, **kwargs):
+        """
+        Create a new Style object 'name'.
+
+        Args:
+            kwargs: see Style.get_default_cell_style()
+        """
+        style = deepcopy(self.get_cell_style('default'))
+        style.update(kwargs)
+        if 'align' in kwargs:
+            align = kwargs.get('align')
+            halign, valign = const.TEXT_ALIGN_FLAGS.get(align)
+            style['halign'] = halign
+            style['valign'] = valign
+        else:
+            halign = kwargs.get('halign')
+            valign = kwargs.get('valign')
+            style['align'] = const.TEXT_ALIGNMENT_BY_FLAGS.get(halign, valign)
+
+        self.styles[name] = style
+        return style
+
+    def new_border_style(self, color=const.BYLAYER, status=True, priority=100, linetype="BYLAYER"):
+        """
+        Create a new border style.
+
+        Args:
+            status: True for visible, else False
+            color: dxf color index
+            linetype: linetype name, BYLAYER if None
+            priority: drawing priority - higher values covers lower values
+        """
+        border_style = Style.get_default_border_style()
+        border_style['color'] = color
+        border_style['linetype'] = linetype
+        border_style['status'] = status
+        border_style['priority'] = priority
+        return border_style
+
+    def get_cell_style(self, name):
+        """
+        Get cell style by name.
+        """
+        return self.styles[name]
+
+    def iter_visible_cells(self, visibility_map):
+        """
+        Iterate over all visible cells.
+
+        Returns: a generator which yields all visible cells as tuples: (row , col, cell)
+        """
+        return ((row, col, self.get_cell(row, col)) for row, col in visibility_map)
+
+    def render(self, layout, insert=None):
+        """
+        Render table to layout object.
+        """
+        _insert = self.insert
+        if insert is not None:
+            self.insert = insert
+        visibility_map = VisibilityMap(self)
+        grid = Grid(self)
+        grid.render_lines(layout, visibility_map)
+        for row, col, cell in self.iter_visible_cells(visibility_map):
+            grid.render_cell_background(layout, row, col, cell)
+            grid.render_cell_content(layout, row, col, cell)
+
+        self.insert = _insert
+
+
+class VisibilityMap(object):
+    """
+    Stores the visibility of the table cells.
+    """
+    def __init__(self, table):
+        """
+        Create the visibility map for table.
+        """
+        self.table = table
+        self._hidden_cells = {}
+        self._create_visibility_map()
+
+    def _create_visibility_map(self):
+        """
+        Set visibility for all existing cells.
+        """
+        for row, col in iter(self):
+            cell = self.table.get_cell(row, col)
+            self._set_span_visibility(row, col, cell.span)
+
+    def _set_span_visibility(self, row, col, span):
+        """
+        Set the visibility of the given cell.
+
+        The cell itself is visible, all other cells in the span-range
+        (tuple: width, height) are invisible, they are covered by the
+        main cell (row, col).
+        """
+
+        if span != (1, 1):
+            nrows, ncols = span
+            for rowx in range(nrows):
+                for colx in range(ncols):
+                    # switch all cells in span range to invisible
+                    self.hide(row+rowx, col+colx)
+        # switch content cell visible
+        self.show(row, col)
+
+    def show(self, row, col):
+        """
+        Show cell (row, col).
+        """
+        try:
+            del self._hidden_cells[(row, col)]
+        except KeyError:
+            pass
+
+    def hide(self, row, col):
+        """
+        Hide cell (row, col).
+        """
+        self._hidden_cells[(row, col)] = HIDDEN
+
+    def iter_all_cells(self):
+        """
+        Iterate over all cell indices, yields (row, col) tuples.
+        """
+        for row in range(self.table.nrows):
+            for col in range(self.table.ncols):
+                yield row, col
+
+    def is_visible_cell(self, row, col):
+        """
+        True if cell (row, col)  is visible, else False.
+        """
+        return (row, col) not in self._hidden_cells
+
+    def __iter__(self):
+        """
+        Iterate over all visible cells.
+        """
+        return ((row, col) for (row, col) in self.iter_all_cells() if self.is_visible_cell(row, col))
+
+
+class Style(dict):
+    """
+    Cell style object.
+    """
+    @staticmethod
+    def get_default_cell_style():
+        return Style({
+            # textstyle is ignored by block cells
+            'textstyle': 'STANDARD',
+            # text height in drawing units, ignored by block cells
+            'textheight': DEFAULT_CELL_TEXT_HEIGHT,
+            # line spacing in percent = <textheight>*<linespacing>, ignored by block cells
+            'linespacing': DEFAULT_CELL_LINESPACING,
+            # text stretch or block reference x-axis scaling factor
+            'xscale': DEFAULT_CELL_XSCALE,
+            # block reference y-axis scaling factor, ignored by text cells
+            'yscale': DEFAULT_CELL_YSCALE,
+            # dxf color index, ignored by block cells
+            'textcolor': DEFAULT_CELL_TEXTCOLOR,
+            # text or block rotation in degrees
+            'rotation': 0.,
+            # Letters are stacked top-to-bottom, but not rotated
+            'stacked': False,
+            # simple combined align parameter, like 'TOP_CENTER', see also MText.VALID_ALIGN
+            'align': 'TOP_CENTER',  # higher priority than 'haling' and 'valign'
+            # horizontal alignment (const.LEFT, const.CENTER, const.RIGHT)
+            'halign': const.CENTER,
+            # vertical alignment (const.TOP, const.MIDDLE, const.BOTTOM)
+            'valign': const.TOP,
+            # left and right margin in drawing units
+            'hmargin': DEFAULT_CELL_HMARGIN,
+            # top and bottom margin
+            'vmargin': DEFAULT_CELL_VMARGIN,
+            # background color, dxf color index, ignored by block cells
+            'bgcolor': DEFAULT_CELL_BG_COLOR,
+            # left border style
+            'left': Style.get_default_border_style(),
+            # top border style
+            'top': Style.get_default_border_style(),
+            # right border style
+            'right': Style.get_default_border_style(),
+            # bottom border style
+            'bottom': Style.get_default_border_style(),
+        })
+
+    @staticmethod
+    def get_default_border_style():
+        return {
+            # border status, True for visible, False for hidden
+            'status': DEFAULT_BORDER_STATUS,
+            # dxf color index
+            'color': DEFAULT_BORDER_COLOR,
+            # linetype name, BYLAYER if None
+            'linetype': DEFAULT_BORDER_LINETYPE,
+            # drawing priority, higher values cover lower values
+            'priority': DEFAULT_BORDER_PRIORITY,
+        }
+
+    def set_border_status(self, left=True, right=True, top=True, bottom=True):
+        """
+        Set status of all cell borders at once.
+        """
+        for border, status in (('left', left),
+                               ('right', right),
+                               ('top', top),
+                               ('bottom', bottom)):
+            self[border]['status'] = status
+
+    def set_border_style(self, style, left=True, right=True, top=True, bottom=True):
+        """
+        Set border styles of all cell borders at once.
+        """
+        for border, status in (('left', left),
+                               ('right', right),
+                               ('top', top),
+                               ('bottom', bottom)):
+            if status:
+                self[border] = style
+
+
+class Grid(object):
+    """
+    Grid contains the graphical representation of the table.
+    """
+    def __init__(self, table):
+        self.table = table
+        # contains the x-axis coords of the grid lines between the data columns.
+        self.col_pos = self._calc_col_pos()
+        # contains the y-axis coords of the grid lines between the data rows.
+        self.row_pos = self._calc_row_pos()
+        # contains the horizontal border elements, list of border styles
+        # get index with _border_index(row, col), which means the border element
+        # above row, col, and row-indices are [0 .. nrows+1], nrows+1 for the
+        # grid line below the last row; list contains only the border style with
+        # the highest priority.
+        self._hborders = None # created in _init_borders
+        # same as _hborders but for the vertical borders,
+        # col-indices are [0 .. ncols+1], ncols+1 for the last grid line right
+        # of the last column
+        self._vborders = None # created in _init_borders
+        # border style to delete borders inside of merged cells
+        self.noborder = dict(status=False, priority=999, linetype="BYLAYER", color=0)
+
+    def _init_borders(self, hborder, vborder):
+        """
+        Init the _hborders with  <hborder> and _vborders with <vborder>.
+        """
+        # <border_count> has more elements than necessary, but it unifies the
+        # index calculation for _vborders and _hborders.
+        # exact values are:
+        # hborder_count = ncols * (nrows+1), hindex = ncols * <row> + <col>
+        # vborder_count = nrows * (ncols+1), vindex = (ncols+1) * <row> + <col>
+        border_count = (self.table.nrows+1) * (self.table.ncols+1)
+        self._hborders = [hborder] * border_count
+        self._vborders = [vborder] * border_count
+
+    def _border_index(self, row, col):
+        """
+        Calculate linear index for border arrays _hborders and _vborders.
+        """
+        return row * (self.table.ncols+1) + col
+
+    def set_hborder(self, row, col, border_style):
+        """
+        Set <border_style> for the horizontal border element above <row>, <col>.
+        """
+        return self._set_border_style(self._hborders, row, col, border_style)
+
+    def set_vborder(self, row, col, border_style):
+        """
+        Set <border_style> for the vertical border element left of <row>, <col>.
+        """
+        return self._set_border_style(self._vborders, row, col, border_style)
+
+    def _set_border_style(self, borders, row, col, border_style):
+        """
+        Set <border_style> for <row>, <col> in <borders>.
+        """
+        border_index = self._border_index(row, col)
+        actual_borderstyle = borders[border_index]
+        if border_style['priority'] >= actual_borderstyle['priority']:
+            borders[border_index] = border_style
+
+    def get_hborder(self, row, col):
+        """
+        Get the horizontal border element above <row>, <col>.
+        Last grid line (below <nrows>) is the element above of <nrows+1>.
+        """
+        return self._get_border(self._hborders, row, col)
+
+    def get_vborder(self, row, col):
+        """
+        Get the vertical border element left of <row>, <col>.
+        Last grid line (right of <ncols>) is the element left of <ncols+1>.
+        """
+        return self._get_border(self._vborders, row, col)
+
+    def _get_border(self, borders, row, col):
+        """
+        Get border element at <row>, <col> from <borders>.
+        """
+        return borders[self._border_index(row, col)]
+
+    def _sum_fields(self, start_value, fields, append, sign=1.):
+        """
+        Adds step-by-step the fields-values, starting with <start_value>,
+        and appends the resulting values to an other object with the
+        append-method.
+        """
+        position = start_value
+        append(position)
+        for element in fields:
+            position += element * sign
+            append(position)
+
+    def _calc_col_pos(self):
+        """ Calculate the x-axis coords of the grid lines between the columns.
+        """
+        col_pos = []
+        start_x = self.table.insert[0]
+        self._sum_fields(start_x,
+                         self.table.col_widths,
+                         col_pos.append)
+        return col_pos
+
+    def _calc_row_pos(self):
+        """ Calculate the y-axis coords of the grid lines between the rows.
+        """
+        row_pos = []
+        start_y = self.table.insert[1]
+        self._sum_fields(start_y,
+                         self.table.row_heights,
+                         row_pos.append, -1.)
+        return row_pos
+
+    def cell_coords(self, row, col, span):
+        """ Get the coordinates of the cell <row>,<col> as absolute drawing units.
+
+        :return: a tuple (left, right, top, bottom)
+        """
+        top = self.row_pos[row]
+        bottom = self.row_pos[row+span[0]]
+        left = self.col_pos[col]
+        right = self.col_pos[col+span[1]]
+        return left, right, top, bottom
+
+    def render_cell_background(self, layout, row, col, cell):
+        """
+        Render the cell background for <row>, <col> as SOLID entity.
+        """
+        style = cell.style
+        if style['bgcolor'] is None:
+            return
+        # get cell coords in absolute drawing units
+        left, right, top, bottom = self.cell_coords(row, col, cell.span)
+        ltop = (left, top)
+        lbot = (left, bottom)
+        rtop = (right, top)
+        rbot = (right, bottom)
+        layout.add_solid(
+            points=(ltop, lbot, rtop, rbot),
+            dxfattribs={
+                'color': style['bgcolor'],
+                'layer': self.table.bglayer,
+            })
+
+    def render_cell_content(self, layout, row, col, cell):
+        """
+        Render the cell content for <row>,<col> into layout object.
+        """
+        # get cell coords in absolute drawing units
+        coords = self.cell_coords(row, col, cell.span)
+        cell.render(layout, coords, self.table.fglayer)
+
+    def render_lines(self, layout, visibility_map):
+        """
+        Render all grid lines into layout object.
+        """
+        # Init borders with default_style top- and left border.
+        default_style = self.table.get_cell_style('default')
+        hborder = default_style['top']
+        vborder = default_style['left']
+        self._init_borders(hborder, vborder)
+        self._set_frames(self.table.frames)
+        self._set_borders(self.table.iter_visible_cells(visibility_map))
+        self._render_borders(layout, self.table)
+
+    def _set_borders(self, visible_cells):
+        """
+        Set borders of the visible cells.
+        """
+        for row, col, cell in visible_cells:
+            bottom_row = row + cell.span[0]
+            right_col = col + cell.span[1]
+            self._set_rect_borders(row, bottom_row, col, right_col, cell.style)
+            self._set_inner_borders(row, bottom_row, col, right_col,
+                                    self.noborder)
+
+    def _set_inner_borders(self, top_row, bottom_row, left_col, right_col, border_style):
+        """
+        Set <border_style> to the inner borders of the rectangle <top_row...
+        """
+        if bottom_row - top_row > 1:
+            for col in range(left_col, right_col):
+                for row in range(top_row+1, bottom_row):
+                    self.set_hborder(row, col, border_style)
+        if right_col - left_col > 1:
+            for row in range(top_row, bottom_row):
+                for col in range(left_col+1, right_col):
+                    self.set_vborder(row, col, border_style)
+
+    def _set_rect_borders(self, top_row, bottom_row, left_col, right_col, style):
+        """
+        Set border <style> to the rectangle <top_row><bottom_row...
+
+        The values describing the grid lines between the cells, see doc-strings for set_hborder and set_vborder and see
+        comments for self._hborders and self._vborders.
+        """
+        for col in range(left_col, right_col):
+            self.set_hborder(top_row, col, style['top'])
+            self.set_hborder(bottom_row, col, style['bottom'])
+        for row in range(top_row, bottom_row):
+            self.set_vborder(row, left_col, style['left'])
+            self.set_vborder(row, right_col, style['right'])
+
+    def _set_frames(self, frames):
+        """
+        Set borders for all defined frames.
+        """
+        for frame in frames:
+            top_row = frame.pos[0]
+            left_col = frame.pos[1]
+            bottom_row = top_row + frame.span[0]
+            right_col = left_col + frame.span[1]
+            self._set_rect_borders(top_row, bottom_row, left_col, right_col, frame.style)
+
+    def _render_borders(self, layout, table):
+        """
+        Render the grid lines as LINE entities into layout object.
+        """
+        def render_line(start, end, style):
+            """
+            Render the LINE entity into layout object.
+            """
+            if style['status']:
+                layout.add_line(
+                    start=start,
+                    end=end,
+                    dxfattribs={
+                        'layer': layer,
+                        'color': style['color'],
+                        'linetype': style['linetype']
+                    }
+                )
+
+        def render_hborders():
+            """
+            Draw the horizontal grid lines.
+            """
+            for row in range(table.nrows+1):
+                yrow = self.row_pos[row]
+                for col in range(table.ncols):
+                    xleft = self.col_pos[col]
+                    xright = self.col_pos[col+1]
+                    style = self.get_hborder(row, col)
+                    render_line((xleft, yrow), (xright, yrow), style)
+
+        def render_vborders():
+            """
+            Draw the vertical grid lines.
+            """
+            for col in range(table.ncols+1):
+                xcol = self.col_pos[col]
+                for row in range(table.nrows):
+                    ytop = self.row_pos[row]
+                    ybottom = self.row_pos[row+1]
+                    style = self.get_vborder(row, col)
+                    render_line((xcol, ytop), (xcol, ybottom), style)
+
+        layer = table.gridlayer
+        render_hborders()
+        render_vborders()
+
+
+class Frame(object):
+    """
+    Represent a rectangle cell area enclosed by border lines.
+    """
+    def __init__(self, table, pos=(0, 0), span=(1, 1), style='default'):
+        """
+        Constructor
+
+        Args:
+            table: the assigned data table
+            pos: tuple (row, col), border goes left and top of pos
+            span: count of cells that Frame covers, border goes right and below of this cells
+            style: style name as string
+        """
+        self.table = table
+        self.pos = pos
+        self.span = span
+        self.stylename = style
+
+    @property
+    def style(self):
+        return self.table.get_cell_style(self.stylename)
+
+
+class Cell(object):
+    """
+    Cell represents the table cell data.
+    """
+    def __init__(self, table, style='default', span=(1, 1)):
+        """
+        Constructor
+
+        Args:
+            table: assigned data table
+            style: style name as string
+            span: tuple(spanrows, spancols), count of cells that cell covers
+
+        Cell does not know its own position in the data table, because a cell can be used multiple times in the same or
+        in different tables. Therefore the cell itself can not determine if the cell-range reaches beyond the table
+        borders.
+        """
+        self.table = table
+        self.stylename = style
+        # span values has to be >= 1
+        self.span = span
+
+    @property
+    def span(self):
+        return self._span
+
+    @span.setter
+    def span(self, value):
+        """
+        Ensures that span values are >= 1 in each direction.
+        """
+        self._span = (max(1, value[0]), max(1, value[1]))
+
+    @property
+    def style(self):
+        """
+        Returns: Style() object of the associated table.
+        """
+        return self.table.get_cell_style(self.stylename)
+
+    @abstractmethod
+    def render(self, layout, coords, layer):
+        pass
+
+    def get_workspace_coords(self, coords):
+        """
+        Reduces the cell-coords about the hmargin and the vmargin values.
+        """
+        hmargin = self.style['hmargin']
+        vmargin = self.style['vmargin']
+        return (coords[0] + hmargin,  # left
+                coords[1] - hmargin,  # right
+                coords[2] - vmargin,  # top
+                coords[3] + vmargin)  # bottom
+
+
+CustomCell = Cell
+
+
+class TextCell(Cell):
+    """
+    Represents a multi line text. Text lines are separated by '\n'.
+    """
+    def __init__(self, table,  text, style='default', span=(1, 1)):
+        """
+        Constructor
+
+        Args:
+            table: assigned data table
+            text: multi line text, lines separated by '\n'
+            style: style-name as string
+            span: tuple(spanrows, spancols), count of cells that cell covers
+
+        see Cell.__init__()
+        """
+        super(TextCell, self).__init__(table, style, span)
+        self.text = text
+
+    def render(self, layout, coords, layer):
+        """
+        Create the cell content as MText() object.
+
+        Args:
+            layout: target layout
+            coords: tuple of border-coordinates : left, right, top, bottom
+            layer: layer, which should be used for dxf entities
+        """
+        if not len(self.text):
+            return
+
+        left, right, top, bottom = self.get_workspace_coords(coords)
+        style = self.style
+        halign = style['halign']
+        valign = style['valign']
+        rotated = self.style['rotation']
+        text = self.text
+        if style['stacked']:
+            rotated = 0.
+            text = '\n'.join((char for char in self.text.replace('\n', ' ')))
+        xpos = (left, float(left+right)/2., right)[halign]
+        ypos = (bottom, float(bottom+top)/2., top)[valign-1]
+        mtext = MText(  # using dxfwrite MText() composite, because it works
+            text,
+            (xpos, ypos),
+            linespacing=self.style['linespacing'],
+            style=self.style['textstyle'],
+            height=self.style['textheight'],
+            rotation=rotated,
+            xscale=self.style['xscale'],
+            halign=halign,
+            valign=valign,
+            color=self.style['textcolor'],
+            layer=layer,
+        )
+        mtext.render(layout)
+
+
+class BlockCell(Cell):
+    """
+    Cell that contains a block reference.
+    """
+    def __init__(self, table, blockdef, style='default', attribs=None, span=(1, 1)):
+        """
+        Constructor
+
+        Args:
+            table: assigned data table
+            blockdef: block definition
+            attribs: dict, with ATTRIB-Tags as keys
+            style: style name as string
+            span: tuple(spanrows, spancols), count of cells that cell covers
+
+        see also Cell.__init__()
+        """
+        if attribs is None:
+            attribs = {}
+        super(BlockCell, self).__init__(table, style, span)
+        self.blockdef = blockdef  # dxf block definition!
+        self.attribs = attribs
+
+    def render(self, layout, coords, layer):
+        """
+        Create the cell content as INSERT-entity with trailing ATTRIB-Entities.
+
+        Args:
+            layout: target layout
+            coords: tuple of border-coordinates : left, right, top, bottom
+            layer: layer for cell content
+        """
+        left, right, top, bottom = self.get_workspace_coords(coords)
+        style = self.style
+        halign = style['halign']
+        valign = style['valign']
+        xpos = (left, float(left + right) / 2., right)[halign]
+        ypos = (bottom, float(bottom + top) / 2., top)[valign-1]
+        layout.add_auto_blockref(
+            name=self.blockdef.name,
+            insert=(xpos, ypos),
+            values=self.attribs,
+            dxfattribs={
+                'xscale': style['xscale'],
+                'yscale': style['yscale'],
+                'rotation': style['rotation'],
+                'layer': layer,
+            }
+        )

+ 6 - 0
venv/lib/python3.7/site-packages/ezdxf/audit/__init__.py

@@ -0,0 +1,6 @@
+# Purpose: DXF audit package
+# Created: 10.03.2017
+# Copyright (C) 2017, Manfred Moitzi
+# License: MIT License
+from ezdxf.audit.auditor import Auditor
+from ezdxf.lldxf.const import Error

+ 71 - 0
venv/lib/python3.7/site-packages/ezdxf/audit/__main__.py

@@ -0,0 +1,71 @@
+# Purpose: audit runner
+# Created: 21.01.2018
+# Copyright (C) 2018, Manfred Moitzi
+# License: MIT License
+import sys
+import argparse
+import os
+import glob
+from ezdxf import readfile, options
+from ezdxf.lldxf.const import DXFError
+from ezdxf.lldxf.validator import is_dxf_file
+
+
+def audit(filename: str, ignore_zero_pointers: bool = False) -> None:
+    try:
+        dwg = readfile(filename, legacy_mode=True)
+    except IOError:
+        print("Unable to read DXF file '{}'.".format(filename))
+        sys.exit(1)
+    except DXFError as e:
+        print(str(e))
+        sys.exit(2)
+
+    auditor = dwg.auditor()
+    errors = auditor.run()
+    if ignore_zero_pointers:
+        errors = auditor.filter_zero_pointers(errors)
+    auditor.print_report(errors)
+
+
+def processing_msg(text: str) -> None:
+    print(text)
+    print('-' * len(text))
+
+
+def main() -> None:
+    print()
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        'files',
+        metavar='FILE',
+        nargs='+',
+        help='audit DXF files',
+    )
+    parser.add_argument(
+        '-z', '--ignore_zero_pointers',
+        action='store_true',
+        help='ignore zero pointers',
+    )
+
+    args = parser.parse_args(sys.argv[1:])
+
+    options.compress_binary_data = True
+    for pattern in args.files:
+        names = list(glob.glob(pattern))
+        if len(names) == 0:
+            print("File(s) '{}' not found.".format(pattern))
+            continue
+        for filename in names:
+            if not os.path.exists(filename):
+                print("File '{}' not found.".format(filename))
+                continue
+            if not is_dxf_file(filename):
+                print("File '{}' is not a DXF file.".format(filename))
+                continue
+            processing_msg(filename)
+            audit(filename, args.ignore_zero_pointers)
+
+
+if __name__ == "__main__":
+    main()

+ 284 - 0
venv/lib/python3.7/site-packages/ezdxf/audit/auditor.py

@@ -0,0 +1,284 @@
+# Purpose: auditor module
+# Created: 10.03.2017
+# Copyright (C) 2017, Manfred Moitzi
+# License: MIT License
+"""
+audit(drawing, stream): check a DXF drawing for errors.
+"""
+from typing import TYPE_CHECKING, Iterable, List, Set, TextIO, Any, Optional
+
+import sys
+from ezdxf.lldxf.types import is_pointer_code, DXFTag
+from ezdxf.lldxf.const import Error
+from ezdxf.lldxf.validator import is_valid_layer_name, is_adsk_special_layer
+from ezdxf.dxfentity import DXFEntity
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFEntity, Drawing, ExtendedTags
+
+REQUIRED_ROOT_DICT_ENTRIES = ('ACAD_GROUP', 'ACAD_PLOTSTYLENAME')
+
+
+class ErrorEntry:
+    def __init__(self, code: int, message: str = '', dxf_entity: 'DXFEntity' = None, data: Any = None):
+        self.code = code
+        self.entity = dxf_entity
+        self.message = message
+        self.data = data
+
+
+def target_pointers(tags: Iterable[DXFTag]) -> Iterable[DXFTag]:
+    for tag in tags:
+        if is_pointer_code(tag.code):
+            yield tag
+
+
+class Auditor:
+    def __init__(self, drawing: 'Drawing'):
+        self.drawing = drawing
+        self.errors = []  # type: List[ErrorEntry]
+        self.undefined_targets = set()  # type: Set[str]
+
+    def reset(self) -> None:
+        self.errors = []
+        self.undefined_targets = set()
+
+    def __len__(self) -> int:
+        return len(self.errors)
+
+    def __bool__(self) -> bool:
+        return self.__len__() > 0
+
+    def __iter__(self) -> Iterable[ErrorEntry]:
+        return iter(self.errors)
+
+    def print_report(self, errors: List[ErrorEntry] = None, stream: TextIO = None) -> None:
+        def entity_str(count, code, entity):
+            if entity is not None:
+                return "{:4d}. Issue [{}] in {} #{}".format(count, code, entity.dxftype(), entity.dxf.handle)
+            else:
+                return "{:4d}. Issue [{}]".format(count, code)
+
+        if errors is None:
+            errors = self.errors
+        else:
+            errors = list(errors)  # generator?
+
+        if stream is None:
+            stream = sys.stdout
+
+        if len(errors) == 0:
+            stream.write('No issues found.\n\n')
+        else:
+            stream.write('{} issues found.\n\n'.format(len(errors)))
+            for count, error in enumerate(errors):
+                stream.write(entity_str(count + 1, error.code, error.entity) + '\n')
+                stream.write('   ' + error.message + '\n\n')
+
+    @staticmethod
+    def filter_zero_pointers(errors: Iterable[ErrorEntry]) -> Iterable[ErrorEntry]:
+        for error in errors:
+            if error.code == Error.POINTER_TARGET_NOT_EXISTS and error.data.value == '0':
+                continue
+            yield error
+
+    def add_error(self, code: int, message: str = '', dxf_entity: 'DXFEntity' = None, data: Any = None) -> None:
+        error = ErrorEntry(code, message, dxf_entity, data)
+        self.errors.append(error)
+
+    def run(self) -> List[ErrorEntry]:
+        self.reset()
+        dxfversion = self.drawing.dxfversion
+        if dxfversion > 'AC1009':  # modern style DXF13 or later
+            self.check_root_dict()
+            self.check_classes_section()
+        self.check_table_entries()
+        self.check_database_entities()
+        return self.errors
+
+    def check_root_dict(self) -> None:
+        root_dict = self.drawing.rootdict
+        for name in REQUIRED_ROOT_DICT_ENTRIES:
+            if name not in root_dict:
+                self.add_error(
+                    code=Error.MISSING_REQUIRED_ROOT_DICT_ENTRY,
+                    message='Missing root dict entry: {}'.format(name),
+                    dxf_entity=root_dict,
+                )
+
+    def check_table_entries(self) -> None:
+        tables = self.drawing.sections.tables
+
+        def check_table(name):
+            if name in tables:
+                tables[name].audit(self)
+
+        check_table('layers')
+        check_table('linetypes')
+        check_table('styles')
+        check_table('dimstyles')
+        check_table('ucs')
+        check_table('appids')
+        check_table('views')
+        if self.drawing.dxfversion > 'AC1009':
+            check_table('block_records')
+
+    def check_database_entities(self) -> None:
+        for handle in self.drawing.entitydb.keys():
+            entity = self.drawing.get_dxf_entity(handle)
+            entity.audit(self)
+
+    def check_if_linetype_exists(self, entity: 'DXFEntity') -> None:
+        """
+        Check for usage of undefined line types. AutoCAD does not load DXF files with undefined line types.
+        """
+        if not entity.supports_dxf_attrib('linetype'):
+            return
+        linetype = entity.dxf.linetype
+        if linetype.lower() in ('bylayer', 'byblock'):  # no table entry in linetypes required
+            return
+
+        if linetype not in self.drawing.linetypes:
+            self.add_error(
+                code=Error.UNDEFINED_LINETYPE,
+                message='Undefined linetype: {}'.format(linetype),
+                dxf_entity=entity,
+            )
+
+    def check_if_text_style_exists(self, entity: 'DXFEntity') -> None:
+        """
+        Check for usage of undefined text styles.
+        """
+        if not entity.supports_dxf_attrib('style'):
+            return
+        style = entity.dxf.style
+        if style not in self.drawing.styles:
+            self.add_error(
+                code=Error.UNDEFINED_TEXT_STYLE,
+                message='Undefined dimstyle: {}'.format(style),
+                dxf_entity=entity,
+            )
+
+    def check_if_dimension_style_exists(self, entity: 'DXFEntity') -> None:
+        """
+        Check for usage of undefined dimension styles.
+        """
+        if not entity.supports_dxf_attrib('dimstyle'):
+            return
+        dimstyle = entity.dxf.dimstyle
+        if dimstyle not in self.drawing.dimstyles:
+            self.add_error(
+                code=Error.UNDEFINED_DIMENSION_STYLE,
+                message='Undefined dimstyle: {}'.format(dimstyle),
+                dxf_entity=entity,
+            )
+
+    def check_for_valid_layer_name(self, entity: 'DXFEntity') -> None:
+        """
+        Check layer names for invalid characters: <>/\":;?*|='
+        """
+        if not entity.supports_dxf_attrib('layer'):
+            return
+        name = entity.dxf.layer
+        if not is_valid_layer_name(name):
+            if self.drawing.dxfversion > 'AC1009' and is_adsk_special_layer(name):
+                return
+            self.add_error(
+                code=Error.INVALID_LAYER_NAME,
+                message='Invalid layer name: {}'.format(name),
+                dxf_entity=entity,
+            )
+
+    def check_for_valid_color_index(self, entity: 'DXFEntity') -> None:
+        if not entity.supports_dxf_attrib('color'):
+            return
+        color = entity.dxf.color
+        # 0 == BYBLOCK
+        # 256 == BYLAYER
+        # 257 == BYOBJECT
+        if color < 0 or color > 257:
+            self.add_error(
+                code=Error.INVALID_COLOR_INDEX,
+                message='Invalid color index: {}'.format(color),
+                dxf_entity=entity,
+            )
+
+    def check_for_existing_owner(self, entity: 'DXFEntity') -> None:
+        if not entity.supports_dxf_attrib('owner'):
+            return
+        owner_handle = entity.dxf.owner
+        if owner_handle not in self.drawing.entitydb:
+            self.add_error(
+                code=Error.INVALID_OWNER_HANDLE,
+                message='Invalid owner handle: #{}'.format(owner_handle),
+                dxf_entity=entity,
+            )
+
+    def check_pointer_target_exists(self, entity: 'DXFEntity',
+                                    zero_pointer_valid: bool = False,
+                                    ignore_codes: Iterable[int] = None) -> None:
+        assert isinstance(entity, DXFEntity)
+        if ignore_codes is None:
+            ignore_codes = set()
+        else:
+            ignore_codes = set(ignore_codes)
+
+        db = self.drawing.entitydb
+        for tag in target_pointers(entity.tags):
+            group_code, handle = tag
+            if group_code in ignore_codes:
+                continue
+            if handle not in db:
+                if handle == '0' and zero_pointer_valid:  # default unset pointer
+                    continue
+                if handle in self.undefined_targets:  # for every undefined pointer add just one error message
+                    continue
+                self.add_error(
+                    code=Error.POINTER_TARGET_NOT_EXISTS,
+                    message='Pointer target does not exist: ({}, #{})'.format(group_code, handle),
+                    dxf_entity=entity,
+                    data=tag,
+                )
+                self.undefined_targets.add(handle)
+
+    def check_handles_exists(self, entity: 'DXFEntity',
+                             handles: Iterable[str],
+                             zero_pointer_valid: bool = False) -> None:
+        db = self.drawing.entitydb
+        for handle in handles:
+            if handle not in db:
+                if handle == '0' and zero_pointer_valid:  # default unset pointer
+                    continue
+                if handle in self.undefined_targets:  # for every undefined pointer add just one error message
+                    continue
+                self.add_error(
+                    code=Error.POINTER_TARGET_NOT_EXISTS,
+                    message='handle target does not exist: (#{})'.format(handle),
+                    dxf_entity=entity,
+                    data=DXFTag(-1, handle),  # DXFTag is expected
+                )
+                self.undefined_targets.add(handle)
+
+    def check_classes_section(self) -> None:
+        def check_invalid_group_codes(valid_codes: Set[int]) -> None:
+            def find_invalid_group_code(tags: 'ExtendedTags') -> Optional[int]:
+                for code, value in tags.noclass:
+                    if code not in valid_codes:
+                        return code
+                return None
+
+            for cls in self.drawing.sections.classes:
+                invalid_code = find_invalid_group_code(cls.tags)
+                if invalid_code is not None:
+                    self.add_error(
+                        code=Error.INVALID_GROUP_CODE_IN_CLASS_DEFINITION,
+                        message='Invalid group code {} in CLASS definition: {}.'.format(invalid_code, cls.dxf.name),
+                    )
+
+        dxfversion = self.drawing.dxfversion
+        if dxfversion <= 'AC1009':
+            return
+        if dxfversion < 'AC1018':
+            check_invalid_group_codes(valid_codes={0, 1, 2, 3, 90, 280, 281})
+        else:
+            check_invalid_group_codes(valid_codes={0, 1, 2, 3, 90, 91, 280, 281})

+ 120 - 0
venv/lib/python3.7/site-packages/ezdxf/database.py

@@ -0,0 +1,120 @@
+# Purpose: database module
+# Created: 11.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import Optional, Iterable, Tuple
+from ezdxf.tools.handle import HandleGenerator
+from ezdxf.lldxf.const import DXFValueError
+from ezdxf.lldxf.tags import DXFTag
+from ezdxf.lldxf.extendedtags import ExtendedTags
+from ezdxf.dxfentity import DXFEntity
+
+
+class EntityDB:
+    """ A simple key/value database a.k.a. dict(), but can be replaced by other
+    classes that implements all of the methods of `EntityDB`. The entities
+    have no order.
+
+    The Data Model
+
+    Every entity/object, except tables and sections, are represented as
+    tag-list (see ExtendedTags Class), this lists are stored in the drawing-associated
+    database, database-key is the 'handle' tag (code == 5 or 105).
+
+    For the entity/object manipulation this tag-list will be wrapped into
+    separated classes, which are generated by the dxffactory-object.
+    The dxffactory-object generates DXF-Version specific wrapper classes.
+
+    """
+
+    def __init__(self):
+        self._database = {}
+        self.handles = HandleGenerator()
+
+    def __delitem__(self, handle: str) -> None:
+        del self._database[handle]
+
+    def __getitem__(self, handle: str) -> ExtendedTags:
+        return self._database[handle]
+
+    def get(self, handle: str) -> Optional[ExtendedTags]:
+        try:
+            return self.__getitem__(handle)
+        except KeyError:  # internal exception
+            return None
+
+    def __setitem__(self, handle: str, tags: ExtendedTags) -> None:
+        self._database[handle] = tags
+
+    def __contains__(self, handle: str) -> bool:
+        """ Database contains handle? """
+        return handle in self._database
+
+    def __len__(self) -> int:
+        """ Count of database items. """
+        return len(self._database)
+
+    def __iter__(self) -> Iterable[str]:
+        """ Iterate over all handles. """
+        return iter(self._database.keys())
+
+    def keys(self) -> Iterable[str]:
+        """ Iterate over all handles. """
+        return self._database.keys()
+
+    def values(self) -> Iterable[ExtendedTags]:
+        """ Iterate over all entities. """
+        return self._database.values()
+
+    def items(self) -> Iterable[Tuple[str, ExtendedTags]]:
+        """ Iterate over all (handle, entities) pairs. """
+        return self._database.items()
+
+    def add_tags(self, tags: ExtendedTags) -> str:
+        try:
+            handle = tags.get_handle()
+        except DXFValueError:  # create new handle
+            handle = self.get_unique_handle()
+            handle_code = 105 if tags.dxftype() == 'DIMSTYLE' else 5  # legacy shit!!!
+            tags.noclass.insert(1, DXFTag(handle_code, handle))  # handle should be the 2. tag
+
+        self.__setitem__(handle, tags)
+        return handle
+
+    def delete_entity(self, entity: DXFEntity) -> None:
+        entity.destroy()
+        self.delete_handle(entity.dxf.handle)
+
+    def delete_handle(self, handle: str) -> None:
+        del self._database[handle]
+
+    def get_unique_handle(self)-> str:
+        while True:
+            handle = self.handles.next()
+            if handle not in self._database:  # you can not trust $HANDSEED value
+                return handle
+
+    def duplicate_tags(self, tags: ExtendedTags) -> ExtendedTags:
+        """
+        Deep copy of tags with new handle and duplicated linked entities (VERTEX, ATTRIB, SEQEND) with also new handles.
+        An existing owner tag is not changed because this is not the domain of the EntityDB() class.
+        The new entity tags are added to the drawing database.
+
+        This is not a deep copy in the meaning of Python, because handle and link is changed.
+        
+        """
+        new_tags = tags.clone()
+        new_tags.noclass.replace_handle(self.get_unique_handle())  # set new handle
+        self.add_tags(new_tags)  # add new tags to database
+        source_link = tags.link  # follow link structure of original entity
+        parent_copy = new_tags
+        while source_link is not None:  # duplicate linked entities (VERTEX, ATTRIB, SEQEND)
+            source_linked_entity = self.get(source_link)  # extended tags
+            linked_entity_copy = source_linked_entity.clone()
+            new_handle = self.get_unique_handle()
+            linked_entity_copy.noclass.replace_handle(new_handle)  # set new handle
+            self.add_tags(linked_entity_copy)  # add new tags to database
+            parent_copy.link = new_handle
+            source_link = source_linked_entity.link  # follow link structure of original entity
+            parent_copy = linked_entity_copy
+        return new_tags

+ 457 - 0
venv/lib/python3.7/site-packages/ezdxf/dimstyleoverride.py

@@ -0,0 +1,457 @@
+from typing import Any, TYPE_CHECKING, Tuple
+from ezdxf.lldxf import const
+from ezdxf.lldxf.const import DXFAttributeError, DIMJUST, DIMTAD
+from ezdxf.render.arrows import ARROWS
+from ezdxf.math import Vector
+import logging
+
+logger = logging.getLogger('ezdxf')
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Dimension, UCS, Drawing, DimStyle, Vertex, BaseDimensionRenderer
+
+
+class DimStyleOverride:
+    def __init__(self, dimension: 'Dimension', override: dict = None):
+        self.dimension = dimension  # type: Dimension
+        dim_style_name = dimension.get_dxf_attrib('dimstyle', 'STANDARD')
+        self.dimstyle = self.drawing.dimstyles.get(dim_style_name)  # type: DimStyle
+        self.dimstyle_attribs = self.get_dstyle_dict()  # type: dict
+
+        # special ezdxf attributes beyond the DXF reference, therefore not stored in the DSTYLE data.
+        # This are only rendering effects or data transfer objects
+        # user_location: Vector - user location override if not None
+        # relative_user_location: bool - user location override relative to dimline center if True
+        # text_shift_h: float - shift text in text direction, relative to standard text location
+        # text_shift_v: float - shift text perpendicular to text direction, relative to standard text location
+        self.update(override or {})
+
+    @property
+    def drawing(self) -> 'Drawing':
+        return self.dimension.drawing
+
+    @property
+    def dxfversion(self) -> str:
+        return self.dimension.drawing.dxfversion
+
+    def get_dstyle_dict(self) -> dict:
+        return self.dimension.get_acad_dstyle(self.dimstyle)
+
+    def get(self, attribute: str, default: Any = None) -> Any:
+        if attribute in self.dimstyle_attribs:
+            result = self.dimstyle_attribs[attribute]
+        else:
+            # Return default value for attributes not supported by DXF R12.
+            # This is a hack to use the same algorithm to render DXF R2000 and DXF R12 DIMENSION entities.
+            # But the DXF R2000 attributes are not stored in the DXF R12 file!!!
+            # Does not catch invalid attributes names! Look into debug log for ignored DIMSTYLE attributes.
+            try:
+                result = self.dimstyle.get_dxf_attrib(attribute, default)
+            except DXFAttributeError:
+                # return default value
+                result = default
+        return result
+
+    def pop(self, attribute: str, default: Any = None) -> Any:
+        value = self.get(attribute, default)
+        # delete just from override dict
+        del self[attribute]
+        return value
+
+    def update(self, attribs: dict) -> None:
+        self.dimstyle_attribs.update(attribs)
+
+    def __getitem__(self, item: str) -> Any:
+        return self.get(item)
+
+    def __setitem__(self, key: str, value: Any) -> None:
+        self.dimstyle_attribs[key] = value
+
+    def __delitem__(self, key: str) -> None:
+        try:
+            del self.dimstyle_attribs[key]
+        except KeyError:  # silent discard
+            pass
+
+    def commit(self) -> None:
+        """
+        Write overwritten DIMSTYLE attributes into XDATA section of the DIMENSION entity.
+
+        """
+
+        def set_arrow_handle(attrib_name, block_name):
+            attrib_name += '_handle'
+            if block_name in ARROWS:  # create all arrows on demand
+                block_name = ARROWS.create_block(blocks, block_name)
+            if block_name == '_CLOSEDFILLED':  # special arrow
+                handle = '0'  # set special #0 handle for closed filled arrow
+            else:
+                block = blocks.get(block_name)
+                handle = block.block_record_handle
+            self.dimstyle_attribs[attrib_name] = handle
+
+        def set_linetype_handle(attrib_name, linetype_name):
+            ltype = self.drawing.linetypes.get(linetype_name)
+            self.dimstyle_attribs[attrib_name + '_handle'] = ltype.dxf.handle
+
+        if self.drawing.dxfversion > 'AC1009':
+            # transform block names into block record handles
+            blocks = self.drawing.blocks
+            for attrib_name in ('dimblk', 'dimblk1', 'dimblk2', 'dimldrblk'):
+                try:
+                    block_name = self.dimstyle_attribs.pop(attrib_name)
+                except KeyError:
+                    pass
+                else:
+                    set_arrow_handle(attrib_name, block_name)
+
+        if self.drawing.dxfversion >= 'AC1021':
+            # transform linetype names into LTYPE entry handles
+            for attrib_name in ('dimltype', 'dimltex1', 'dimltex2'):
+                try:
+                    linetype_name = self.dimstyle_attribs.pop(attrib_name)
+                except KeyError:
+                    pass
+                else:
+                    set_linetype_handle(attrib_name, linetype_name)
+
+        self.dimension.set_acad_dstyle(self.dimstyle_attribs)
+
+    def set_arrows(self, blk: str = None, blk1: str = None, blk2: str = None, ldrblk: str = None,
+                   size: float = None) -> None:
+        """
+        Set arrows or user defined blocks and disable oblique stroke as tick.
+
+        Args:
+            blk: defines both arrows at once as name str or user defined block (name)
+            blk1: defines left arrow as name str or as user defined block (name)
+            blk2: defines right arrow as name str or as user defined block (name)
+            ldrblk: defines leader arrow as name str or as user defined block (name)
+            size: arrow size in drawing units
+
+        """
+
+        def set_arrow(dimvar: str, name: str) -> None:
+            self.dimstyle_attribs[dimvar] = name
+
+        if size is not None:
+            self.dimstyle_attribs['dimasz'] = float(size)
+        if blk is not None:
+            set_arrow('dimblk', blk)
+            self.dimstyle_attribs['dimsah'] = 0
+            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
+        if blk1 is not None:
+            set_arrow('dimblk1', blk1)
+            self.dimstyle_attribs['dimsah'] = 1
+            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
+        if blk2 is not None:
+            set_arrow('dimblk2', blk2)
+            self.dimstyle_attribs['dimsah'] = 1
+            self.dimstyle_attribs['dimtsz'] = 0.  # use arrows
+        if ldrblk is not None:
+            set_arrow('dimldrblk', ldrblk)
+
+    def get_arrow_names(self) -> Tuple[str, str]:
+        """
+        Get arrows as name strings like 'ARCHTICK'.
+
+        """
+        dimtsz = self.get('dimtsz')
+        blk1, blk2 = None, None
+        if dimtsz == 0.:
+            if bool(self.get('dimsah')):
+                blk1 = self.get('dimblk1')
+                blk2 = self.get('dimblk2')
+            else:
+                blk = self.get('dimblk')
+                blk1 = blk
+                blk2 = blk
+        return blk1, blk2
+
+    def set_tick(self, size: float = 1) -> None:
+        """
+        Use oblique stroke as tick, disables arrows.
+
+        Args:
+            size: arrow size in daring units
+
+        """
+        self.dimstyle_attribs['dimtsz'] = float(size)
+
+    def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None:
+        """
+        Set measurement text alignment, `halign` defines the horizontal alignment, `valign` defines the vertical
+        alignment, `above1` and `above2` means above extension line 1 or 2 and aligned with extension line.
+
+        Args:
+            halign: `left`, `right`, `center`, `above1`, `above2`, requires DXF R2000+
+            valign: `above`, `center`, `below`
+            vshift: vertical text shift, if `valign` is `center`; >0 shift upward, <0 shift downwards
+
+        """
+        if halign:
+            self.dimstyle_attribs['dimjust'] = DIMJUST[halign.lower()]
+
+        if valign:
+            valign = valign.lower()
+            self.dimstyle_attribs['dimtad'] = DIMTAD[valign]
+            if valign == 'center' and vshift is not None:
+                self.dimstyle_attribs['dimtvp'] = float(vshift)
+
+    def set_tolerance(self, upper: float, lower: float = None, hfactor: float = None,
+                      align: str = None, dec: int = None, leading_zeros: bool = None,
+                      trailing_zeros: bool = None) -> None:
+        """
+        Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and
+        trailing zero suppression.
+
+        Args:
+            upper: upper tolerance value
+            lower: lower tolerance value, if None same as upper
+            hfactor: tolerance text height factor in relation to the dimension text height
+            align: tolerance text alignment "TOP", "MIDDLE", "BOTTOM"
+            dec: Sets the number of decimal places displayed
+            leading_zeros: suppress leading zeros for decimal dimensions if False
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False
+
+        """
+        self.dimstyle_attribs['dimtol'] = 1
+        self.dimstyle_attribs['dimlim'] = 0
+        self.dimstyle_attribs['dimtp'] = float(upper)
+        if lower is not None:
+            self.dimstyle_attribs['dimtm'] = float(lower)
+        else:
+            self.dimstyle_attribs['dimtm'] = float(upper)
+        if hfactor is not None:
+            self.dimstyle_attribs['dimtfac'] = float(hfactor)
+        if align is not None:
+            self.dimstyle_attribs['dimtolj'] = const.MTEXT_INLINE_ALIGN[align.upper()]
+        if dec is not None:
+            self.dimstyle_attribs['dimtdec'] = dec
+
+        # works only with decimal dimensions not inch and feet, US user set dimzin directly
+        if leading_zeros is not None or trailing_zeros is not None:
+            dimtzin = 0
+            if leading_zeros is False:
+                dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+            if trailing_zeros is False:
+                dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+            self.dimstyle_attribs['dimtzin'] = dimtzin
+
+    def set_limits(self, upper: float, lower: float, hfactor: float = None,
+                   dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
+        """
+        Set limits text format, upper and lower limit values, text height factor, number of decimal places or
+        leading and trailing zero suppression.
+
+        Args:
+            upper: upper limit value added to measurement value
+            lower: lower lower value subtracted from measurement value
+            hfactor: limit text height factor in relation to the dimension text height
+            dec: Sets the number of decimal places displayed, required DXF R2000+
+            leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+
+
+        """
+        # exclusive limits
+        self.dimstyle_attribs['dimlim'] = 1
+        self.dimstyle_attribs['dimtol'] = 0
+        self.dimstyle_attribs['dimtp'] = float(upper)
+        self.dimstyle_attribs['dimtm'] = float(lower)
+        if hfactor is not None:
+            self.dimstyle_attribs['dimtfac'] = float(hfactor)
+
+        # works only with decimal dimensions not inch and feet, US user set dimzin directly
+        if leading_zeros is not None or trailing_zeros is not None:
+            dimtzin = 0
+            if leading_zeros is False:
+                dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+            if trailing_zeros is False:
+                dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+            self.dimstyle_attribs['dimtzin'] = dimtzin
+
+        if dec is not None:
+            self.dimstyle_attribs['dimtdec'] = int(dec)
+
+    def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None,
+                        leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
+        """
+        Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places.
+
+        Args:
+            prefix: dimension text prefix text as string
+            postfix: dimension text postfix text as string
+            rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to 0.25, all
+                 distances round to the nearest 0.25 unit. If you set DIMRND to 1.0, all distances round to the nearest
+                 integer.
+            dec: Sets the number of decimal places displayed for the primary units of a dimension. requires DXF R2000+
+            sep: "." or "," as decimal separator
+            leading_zeros: suppress leading zeros for decimal dimensions if False
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False
+
+        """
+        if prefix or postfix:
+            self.dimstyle_attribs['dimpost'] = prefix + '<>' + postfix
+        if rnd is not None:
+            self.dimstyle_attribs['dimrnd'] = rnd
+        if dec is not None:
+            self.dimstyle_attribs['dimdec'] = dec
+        if sep is not None:
+            self.dimstyle_attribs['dimdsep'] = ord(sep)
+        # works only with decimal dimensions not inch and feet, US user set dimzin directly
+        if leading_zeros is not None or trailing_zeros is not None:
+            dimzin = 0
+            if leading_zeros is False:
+                dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+            if trailing_zeros is False:
+                dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+            self.dimstyle_attribs['dimzin'] = dimzin
+
+    def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None,
+                           extension: float = None, disable1: bool = None, disable2: bool = None):
+        """
+        Set dimension line properties
+
+        Args:
+            color: color index
+            linetype: linetype as string
+            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
+            extension: extension length
+            disable1: True to suppress first part of dimension line
+            disable2: True to suppress second part of dimension line
+
+        """
+        if color is not None:
+            self.dimstyle_attribs['dimclrd'] = color
+        if linetype is not None:
+            self.dimstyle_attribs['dimltype'] = linetype
+        if lineweight is not None:
+            self.dimstyle_attribs['dimlwd'] = lineweight
+        if extension is not None:
+            self.dimstyle_attribs['dimdle'] = extension
+        if disable1 is not None:
+            self.dimstyle_attribs['dimsd1'] = disable1
+        if disable2 is not None:
+            self.dimstyle_attribs['dimsd2'] = disable2
+
+    def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None,
+                           offset: float = None, fixed_length: float = None):
+        """
+        Set common extension line attributes.
+
+        Args:
+            color: color index
+            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
+            extension: extension length above dimension line
+            offset: offset from measurement point
+            fixed_length: set fixed length extension line, length below the dimension line
+
+        """
+        if color is not None:
+            self.dimstyle_attribs['dimclre'] = color
+        if lineweight is not None:
+            self.dimstyle_attribs['dimlwe'] = lineweight
+        if extension is not None:
+            self.dimstyle_attribs['dimexe'] = extension
+        if offset is not None:
+            self.dimstyle_attribs['dimexo'] = offset
+        if fixed_length is not None:
+            self.dimstyle_attribs['dimflxon'] = 1
+            self.dimstyle_attribs['dimflx'] = fixed_length
+
+    def set_extline1(self, linetype: str = None, disable=False):
+        """
+        Set extension line 1 attributes.
+
+        Args:
+            linetype: linetype for extension line 1
+            disable: disable extension line 1 if True
+
+        """
+        if linetype is not None:
+            self.dimstyle_attribs['dimltex1'] = linetype
+        if disable:
+            self.dimstyle_attribs['dimse1'] = 1
+
+    def set_extline2(self, linetype: str = None, disable=False):
+        """
+        Set extension line 2 attributes.
+
+        Args:
+            linetype: linetype for extension line 2
+            disable: disable extension line 2 if True
+
+        """
+        if linetype is not None:
+            self.dimstyle_attribs['dimltex2'] = linetype
+        if disable:
+            self.dimstyle_attribs['dimse2'] = 1
+
+    def set_text(self, text='<>') -> None:
+        """
+        Set dimension text.
+
+            - text == ' ' ... suppress dimension text
+            - text == '' or '<>' ... use measured distance as dimension text
+            - else use text literally
+
+        Args:
+            text: string
+
+        """
+        self.dimension.dxf.text = text
+
+    def shift_text(self, dh: float, dv: float) -> None:
+        """
+        Set relative text movement, implemented as user location override without leader.
+
+        Args:
+            dh: shift text in text direction
+            dv: shift text perpendicular to text direction
+
+        """
+        self.dimstyle_attribs['text_shift_h'] = dh
+        self.dimstyle_attribs['text_shift_v'] = dv
+
+    def set_location(self, location: 'Vertex', leader=False, relative=False):
+        self.dimstyle_attribs['dimtmove'] = 1 if leader else 2
+        self.dimension.set_flag_state(self.dimension.USER_LOCATION_OVERRIDE, state=True, name='dimtype')
+        self.dimstyle_attribs['user_location'] = Vector(location)
+        self.dimstyle_attribs['relative_user_location'] = relative
+
+    def get_renderer(self, ucs: 'UCS' = None):
+        return self.drawing.dimension_renderer.dispatch(self, ucs)
+
+    def render(self, ucs: 'UCS' = None, discard=False) -> 'BaseDimensionRenderer':
+        """
+        Initiate dimension line rendering process and also writes overridden dimension style attributes into the DSTYLE
+        XDATA section.
+
+        For a friendly CAD applications like BricsCAD you can discard the dimension line rendering, because it is done
+        automatically by BricsCAD, if no dimension rendering BLOCK is available and it is likely to get better results
+        as by ezdxf.
+
+        AutoCAD does not render DIMENSION entities automatically, so I rate AutoCAD as unfriendly CAD application.
+
+        Args:
+            ucs: user coordinate system
+            discard: discard rendering done by ezdxf (works with BricsCAD, but not with AutoCAD)
+
+        Returns: used renderer for analytics
+
+        """
+
+        renderer = self.get_renderer(ucs)
+        if discard:
+            self.drawing.add_acad_incompatibility_message('DIMENSION without geometry as BLOCK (discard=True)')
+        else:
+            block = self.drawing.blocks.new_anonymous_block(type_char='D')
+            self.dimension.dxf.geometry = block.name
+            renderer.render(block)
+
+        # should be called after rendering
+        renderer.finalize()
+
+        if len(self.dimstyle_attribs):
+            self.commit()
+        return renderer

+ 549 - 0
venv/lib/python3.7/site-packages/ezdxf/drawing.py

@@ -0,0 +1,549 @@
+# Created: 11.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, TextIO, Iterable
+from datetime import datetime
+import io
+import logging
+from itertools import chain
+from ezdxf.database import EntityDB
+from ezdxf.lldxf.const import DXFVersionError, acad_release, BLK_XREF, BLK_EXTERNAL, DXFValueError
+from ezdxf.lldxf.loader import load_dxf_structure, fill_database
+from ezdxf.dxffactory import dxffactory
+from ezdxf.templates import TemplateLoader
+from ezdxf.options import options
+from ezdxf.tools.codepage import tocodepage, toencoding
+from ezdxf.sections.sections import Sections
+from ezdxf.tools.juliandate import juliandate
+from ezdxf.lldxf import repair
+from ezdxf.tools import guid
+from ezdxf.tracker import Tracker
+from ezdxf.query import EntityQuery
+from ezdxf.groupby import groupby
+from ezdxf.render.dimension import DimensionRenderer
+
+logger = logging.getLogger('ezdxf')
+
+if TYPE_CHECKING:
+    from .eztypes import HandleGenerator, DXFTag, LayoutType, SectionDict
+    from .eztypes import GroupManager, MaterialManager, MLeaderStyleManager, MLineStyleManager
+    from .eztypes import SectionType, HeaderSection, BlocksSection, Table, ViewportTable
+
+
+class Drawing:
+    """
+    The Central Data Object
+    """
+
+    def __init__(self, tagger: Iterable['DXFTag']):
+        """
+        Build a new DXF drawing from a steam of DXF tags.
+
+        Args:
+             tagger: generator or list of DXF tags as DXFTag() objects
+        """
+
+        def get_header(sections: 'SectionDict') -> 'SectionType':
+            from .sections.header import HeaderSection
+            header_entities = sections.get('HEADER', [None])[0]  # all tags in the first DXF structure entity
+            return HeaderSection(header_entities)
+
+        self.tracker = Tracker()
+        self._dimension_renderer = DimensionRenderer()  # set DIMENSION rendering engine
+        self._groups = None  # type: GroupManager  # read only
+        self._materials = None  # type: MaterialManager # read only
+        self._mleader_styles = None  # type: MLeaderStyleManager # read only
+        self._mline_styles = None  # type: MLineStyleManager # read only
+        self._acad_compatible = True  # will generated DXF file compatible with AutoCAD
+        self._acad_incompatibility_reason = set()  # avoid multiple warnings for same reason
+        self.filename = None  # type: str # read/write
+        self.entitydb = EntityDB()  # read only
+        sections = load_dxf_structure(tagger)  # load complete DXF entity structure
+        # create section HEADER
+        header = get_header(sections)
+        self.dxfversion = header.get('$ACADVER', 'AC1009')  # type: str # read only
+        self.dxffactory = dxffactory(self)  # read only, requires self.dxfversion
+        self.encoding = toencoding(header.get('$DWGCODEPAGE', 'ANSI_1252'))  # type: str # read/write
+        # get handle seed
+        seed = header.get('$HANDSEED', str(self.entitydb.handles))  # type: str
+        # setup handles
+        self.entitydb.handles.reset(seed)
+        # store all necessary DXF entities in the drawing database
+        fill_database(self.entitydb, sections, dxfversion=self.dxfversion)
+        # create sections: TABLES, BLOCKS, ENTITIES, CLASSES, OBJECTS
+        self.sections = Sections(sections, drawing=self, header=header)
+
+        if self.dxfversion > 'AC1009':
+            self.rootdict = self.objects.rootdict
+            self.objects.setup_objects_management_tables(self.rootdict)  # create missing tables
+            if self.dxfversion in ('AC1012', 'AC1014'):  # releases R13 and R14
+                repair.upgrade_to_ac1015(self)
+            # some applications don't setup properly the model and paper space layouts
+            repair.setup_layouts(self)
+            self._groups = self.objects.groups()
+            self._materials = self.objects.materials()
+            self._mleader_styles = self.objects.mleader_styles()
+            self._mline_styles = self.objects.mline_styles()
+        else:  # dxfversion <= 'AC1009' do cleanup work, before building layouts
+            if self.dxfversion < 'AC1009':  # legacy DXF version
+                repair.upgrade_to_ac1009(self)  # upgrade to DXF format AC1009 (DXF R12)
+            repair.cleanup_r12(self)
+            # ezdxf puts automatically handles into all entities added to the entities database
+            # write R12 without handles, by setting $HANDLING = 0
+            self.header['$HANDLING'] = 1  # write handles by default
+
+        self.layouts = self.dxffactory.get_layouts()
+
+    @property
+    def acad_release(self) -> str:
+        return acad_release.get(self.dxfversion, "unknown")
+
+    @property
+    def acad_compatible(self) -> bool:
+        return self._acad_compatible
+
+    def add_acad_incompatibility_message(self, msg: str):
+        self._acad_compatible = False
+        if msg not in self._acad_incompatibility_reason:
+            self._acad_incompatibility_reason.add(msg)
+            logger.warning('Drawing is incompatible to AutoCAD, because {}.'.format(msg))
+
+    @property
+    def _handles(self) -> 'HandleGenerator':
+        return self.entitydb.handles
+
+    @property
+    def header(self) -> 'HeaderSection':
+        return self.sections.header
+
+    @property
+    def layers(self) -> 'Table':
+        return self.sections.tables.layers
+
+    @property
+    def linetypes(self) -> 'Table':
+        return self.sections.tables.linetypes
+
+    @property
+    def styles(self) -> 'Table':
+        return self.sections.tables.styles
+
+    @property
+    def dimstyles(self) -> 'Table':
+        return self.sections.tables.dimstyles
+
+    @property
+    def ucs(self) -> 'Table':
+        return self.sections.tables.ucs
+
+    @property
+    def appids(self) -> 'Table':
+        return self.sections.tables.appids
+
+    @property
+    def views(self) -> 'Table':
+        return self.sections.tables.views
+
+    @property
+    def block_records(self) -> 'Table':
+        return self.sections.tables.block_records
+
+    @property
+    def viewports(self) -> 'ViewportTable':
+        return self.sections.tables.viewports
+
+    @property
+    def blocks(self) -> 'BlocksSection':
+        return self.sections.blocks
+
+    @property
+    def groups(self) -> 'GroupManager':
+        if self.dxfversion <= 'AC1009':
+            raise DXFVersionError('Groups not supported in DXF version R12.')
+        return self._groups
+
+    @property
+    def materials(self) -> 'MaterialManager':
+        if self.dxfversion <= 'AC1009':
+            raise DXFVersionError('Materials not supported in DXF version R12.')
+        return self._materials
+
+    @property
+    def mleader_styles(self) -> 'MLeaderStyleManager':
+        if self.dxfversion <= 'AC1009':
+            raise DXFVersionError('MLeaderStyles not supported in DXF version R12.')
+        return self._mleader_styles
+
+    @property
+    def mline_styles(self) -> 'MLineStyleManager':
+        if self.dxfversion <= 'AC1009':
+            raise DXFVersionError('MLineStyles not supported in DXF version R12.')
+        return self._mline_styles
+
+    @property
+    def dimension_renderer(self) -> DimensionRenderer:
+        return self._dimension_renderer
+
+    @dimension_renderer.setter
+    def dimension_renderer(self, renderer: DimensionRenderer) -> None:
+        """
+        Set your own dimension line renderer if needed.
+
+        see also: ezdxf.render.dimension
+
+        """
+        self._dimension_renderer = renderer
+
+    def modelspace(self) -> 'LayoutType':
+        return self.layouts.modelspace()
+
+    def layout(self, name: str = None) -> 'LayoutType':
+        return self.layouts.get(name)
+
+    def layout_names(self) -> Iterable[str]:
+        return list(self.layouts.names())
+
+    def delete_layout(self, name):
+        if self.dxfversion > 'AC1009':
+            if name not in self.layouts:
+                raise DXFValueError("Layout '{}' does not exist.".format(name))
+            else:
+                self.layouts.delete(name)
+        else:
+            raise DXFVersionError('delete_layout() not supported for DXF version R12.')
+
+    def new_layout(self, name, dxfattribs=None):
+        if self.dxfversion > 'AC1009':
+            if name in self.layouts:
+                raise DXFValueError("Layout '{}' already exists.".format(name))
+            else:
+                return self.layouts.new(name, dxfattribs)
+        else:
+            raise DXFVersionError('new_layout() not supported for DXF version R12.')
+
+    def layouts_and_blocks(self):
+        """
+        Iterate over all layouts (mode space and paper space) and all block definitions.
+
+        Returns: yields Layout() objects
+
+        """
+        # DXF R12: model space and paper space layouts not linked into the associated BLOCK entity
+        if self.dxfversion <= 'AC1009':
+            return chain(self.layouts, self.blocks)
+        # DXF R2000+: all layout spaces linked into their associated BLOCK entity
+        else:
+            return iter(self.blocks)
+
+    def chain_layouts_and_blocks(self):
+        """
+        Chain entity spaces of all layouts and blocks. Yields an iterator for all entities in all layouts and blocks.
+
+        Returns: yields all entities as DXFEntity() objects
+
+        """
+        layouts = list(self.layouts_and_blocks())
+        return chain.from_iterable(layouts)
+
+    def get_active_layout_key(self):
+        if self.dxfversion > 'AC1009':
+            try:
+                active_layout_block_record = self.block_records.get('*Paper_Space')  # block names are case insensitive
+                return active_layout_block_record.dxf.handle
+            except DXFValueError:
+                return None
+        else:
+            return self.layout().layout_key  # AC1009 supports just one layout and this is the active one
+
+    def get_active_entity_space_layout_keys(self):
+        layout_keys = [self.modelspace().layout_key]
+        active_layout_key = self.get_active_layout_key()
+        if active_layout_key is not None:
+            layout_keys.append(active_layout_key)
+        return layout_keys
+
+    @property
+    def entities(self):
+        return self.sections.entities
+
+    @property
+    def objects(self):
+        return self.sections.objects
+
+    def get_dxf_entity(self, handle):
+        """
+        Get entity by *handle* from entity database.
+
+        Low level access to DXF entities database. Raises *KeyError* if handle don't exists.
+        Returns DXFEntity() or inherited.
+
+        If you just need the raw DXF tags use::
+
+            tags = Drawing.entitydb[handle]  # raises KeyError, if handle don't exist
+            tags = Drawing.entitydb.get(handle)  # returns a default value, if handle don't exist (None by default)
+
+        type of tags: ExtendedTags()
+        """
+        return self.dxffactory.wrap_handle(handle)
+
+    def add_image_def(self, filename, size_in_pixel, name=None):
+        """
+        Add an image definition to the objects section.
+
+        For AutoCAD works best with absolute image paths but not good, you have to update external references manually
+        in AutoCAD, which is not possible in TrueView. If you drawing units differ from 1 meter, you also have to use:
+        Drawing.set_raster_variables().
+
+        Args:
+            filename: image file name (absolute path works best for AutoCAD)
+            size_in_pixel: image size in pixel as (x, y) tuple
+            name: image name for internal use, None for using filename as name (best for AutoCAD)
+
+        """
+        if self.dxfversion < 'AC1015':
+            raise DXFVersionError('The IMAGE entity needs at least DXF version R2000 or later.')
+
+        if 'ACAD_IMAGE_VARS' not in self.rootdict:
+            self.objects.set_raster_variables(frame=0, quality=1, units=3)
+        if name is None:
+            name = filename
+        return self.objects.add_image_def(filename, size_in_pixel, name)
+
+    def set_raster_variables(self, frame=0, quality=1, units='m'):
+        """
+        Set raster variables.
+
+        Args:
+            frame: 0 = do not show image frame; 1 = show image frame
+            quality: 0 = draft; 1 = high
+            units: units for inserting images. This is what one drawing unit is equal to for the purpose of inserting
+                   and scaling images with an associated resolution
+
+                   'mm' = Millimeter
+                   'cm' = Centimeter
+                   'm' = Meter (ezdxf default)
+                   'km' = Kilometer
+                   'in' = Inch
+                   'ft' = Foot
+                   'yd' = Yard
+                   'mi' = Mile
+                   everything else is None
+
+        """
+        self.objects.set_raster_variables(frame=frame, quality=quality, units=units)
+
+    def set_wipeout_variables(self, frame=0):
+        """
+        Set wipeout variables.
+
+        Args:
+            frame: 0 = do not show image frame; 1 = show image frame
+
+        """
+        self.objects.set_wipeout_variables(frame=frame)
+
+    def add_underlay_def(self, filename, format='ext', name=None):
+        """
+        Add an underlay definition to the objects section.
+
+        Args:
+            format: file format as string pdf|dwf|dgn or ext=get format from filename extension
+            name: underlay name, None for an auto-generated name
+
+        """
+        if self.dxfversion < 'AC1015':
+            raise DXFVersionError('The UNDERLAY entity needs at least DXF version R2000 or later.')
+        if format == 'ext':
+            format = filename[-3:]
+        return self.objects.add_underlay_def(filename, format, name)
+
+    def add_xref_def(self, filename, name, flags=BLK_XREF | BLK_EXTERNAL):
+        """
+        Add an external reference (xref) definition to the blocks section.
+
+        Add xref to a layout by `layout.add_blockref(name, insert=(0, 0))`.
+
+        Args:
+            filename: external reference filename
+            name: name of the xref block
+            flags: block flags
+
+        """
+        self.blocks.new(name=name, dxfattribs={
+            'flags': flags,
+            'xref_path': filename
+        })
+
+    def _get_encoding(self):
+        codepage = self.header.get('$DWGCODEPAGE', 'ANSI_1252')
+        return toencoding(codepage)
+
+    @staticmethod
+    def new(dxfversion='AC1009'):
+        from .lldxf.const import versions_supported_by_new, acad_release_to_dxf_version
+
+        dxfversion = dxfversion.upper()
+        dxfversion = acad_release_to_dxf_version.get(dxfversion, dxfversion)  # translates 'R12' -> 'AC1009'
+        if dxfversion not in versions_supported_by_new:
+            raise DXFVersionError("Can not create DXF drawings, unsupported DXF version '{}'.".format(dxfversion))
+        finder = TemplateLoader(options.template_dir)
+        stream = finder.getstream(dxfversion.upper())
+        try:
+            dwg = Drawing.read(stream)
+        finally:
+            stream.close()
+        dwg._setup_metadata()
+        return dwg
+
+    def _setup_metadata(self):
+        self.header['$TDCREATE'] = juliandate(datetime.now())
+
+    @staticmethod
+    def read(stream: TextIO, legacy_mode: bool = False, dxfversion: str = None) -> 'Drawing':
+        """ Open an existing drawing. """
+        from .lldxf.tagger import low_level_tagger, tag_compiler
+
+        tagger = low_level_tagger(stream)
+        if legacy_mode:
+            if dxfversion is not None and dxfversion <= 'AC1009':
+                tagger = repair.filter_subclass_marker(tagger)
+            tagger = repair.tag_reorder_layer(tagger)
+        tagreader = tag_compiler(tagger)
+        return Drawing(tagreader)
+
+    def saveas(self, filename, encoding=None):
+        self.filename = filename
+        self.save(encoding=encoding)
+
+    def save(self, encoding=None):
+        # DXF R12, R2000, R2004 - ASCII encoding
+        # DXF R2007 and newer - UTF-8 encoding
+        if encoding is None:
+            enc = 'utf-8' if self.dxfversion >= 'AC1021' else self.encoding
+        else:  # override default encoding, for applications that handles encoding different than AutoCAD
+            enc = encoding
+        # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode characters.
+        with io.open(self.filename, mode='wt', encoding=enc, errors='dxfreplace') as fp:
+            self.write(fp)
+
+    def write(self, stream):
+        from .lldxf.tagwriter import TagWriter
+        if self.dxfversion == 'AC1009':
+            handles = bool(self.header['$HANDLING'])
+        else:
+            handles = True
+        if self.dxfversion > 'AC1009':
+            self._register_required_classes()
+            if self.dxfversion < 'AC1018':
+                # remove unsupported group code 91
+                repair.fix_classes(self)
+
+        self._create_appids()
+        self._update_metadata()
+        tagwriter = TagWriter(stream, write_handles=handles)
+        self.sections.write(tagwriter)
+
+    def query(self, query='*'):
+        """
+        Entity query over all layouts and blocks.
+
+        Excluding the OBJECTS section!
+
+        Args:
+            query: query string
+
+        Returns: EntityQuery() container
+
+        """
+        return EntityQuery(self.chain_layouts_and_blocks(), query)
+
+    def groupby(self, dxfattrib="", key=None):
+        """
+        Groups DXF entities of all layouts and blocks by an DXF attribute or a key function.
+
+        Excluding the OBJECTS section!
+
+        Args:
+            dxfattrib: grouping DXF attribute like 'layer'
+            key: key function, which accepts a DXFEntity as argument, returns grouping key of this entity or None for ignore
+                 this object. Reason for ignoring: a queried DXF attribute is not supported by this entity
+
+        Returns: dict
+
+        """
+        return groupby(self.chain_layouts_and_blocks(), dxfattrib, key)
+
+    def cleanup(self, groups=True):
+        """
+        Cleanup drawing. Call it before saving the drawing but only if necessary, the process could take a while.
+
+        Args:
+            groups (bool): removes deleted and invalid entities from groups
+        """
+        if groups and self.groups is not None:
+            self.groups.cleanup()
+
+    def auditor(self):
+        """
+        Get auditor for this drawing.
+
+        Returns:
+            Auditor() object
+
+        """
+        from ezdxf.audit.auditor import Auditor
+        return Auditor(self)
+
+    def validate(self, print_report=True):
+        """
+        Simple way to run an audit process.
+
+        Args:
+            print_report: print report to stdout
+
+        Returns: True if no errors occurred else False
+
+        """
+        auditor = self.auditor()
+        result = list(auditor.filter_zero_pointers(auditor.run()))
+        if len(result):
+            if print_report:
+                auditor.print_report()
+            return False
+        else:
+            return True
+
+    def update_class_instance_counters(self):
+        if 'classes' in self.sections:
+            self._register_required_classes()
+            self.sections.classes.update_instance_counters()
+
+    def _register_required_classes(self):
+        register = self.sections.classes.register
+        for dxftype in self.tracker.dxftypes:
+            cls = self.dxffactory.get_wrapper_class(dxftype)
+            if cls.CLASS is not None:
+                register(cls.CLASS)
+
+    def _update_metadata(self):
+        now = datetime.now()
+        self.header['$TDUPDATE'] = juliandate(now)
+        self.header['$HANDSEED'] = str(self.entitydb.handles)
+        self.header['$DWGCODEPAGE'] = tocodepage(self.encoding)
+        self.reset_versionguid()
+
+    def _create_appids(self):
+        def create_appid_if_not_exist(name, flags=0):
+            if name not in self.appids:
+                self.appids.new(name, {'flags': flags})
+
+        if 'HATCH' in self.tracker.dxftypes:
+            create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0)
+
+    def reset_fingerprintguid(self):
+        if self.dxfversion > 'AC1009':
+            self.header['$FINGERPRINTGUID'] = guid()
+
+    def reset_versionguid(self):
+        if self.dxfversion > 'AC1009':
+            self.header['$VERSIONGUID'] = guid()

+ 521 - 0
venv/lib/python3.7/site-packages/ezdxf/dxfentity.py

@@ -0,0 +1,521 @@
+# Created: 11.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Any, Iterable, Mapping, List, cast, Tuple
+from ezdxf.lldxf.const import DXFStructureError, DXFAttributeError, DXFInvalidLayerName, DXFValueError, DXFXDataError
+from ezdxf.lldxf.validator import is_valid_layer_name
+from ezdxf.lldxf.tags import Tags
+from ezdxf.lldxf.extendedtags import ExtendedTags
+
+from ezdxf.tools import set_flag_state
+from ezdxf.math import OCS
+
+if TYPE_CHECKING:  # import forward dependencies
+    from ezdxf.eztypes import TagValue, IterableTags, DXFAttr, DXFDictionary
+    from ezdxf.eztypes import Drawing, EntityDB, DXFFactoryType, GenericLayoutType
+
+ACAD_REACTORS = '{ACAD_REACTORS'
+ACAD_XDICTIONARY = '{ACAD_XDICTIONARY'
+
+
+class NotFoundException(Exception):
+    pass
+
+
+class DXFNamespace:
+    """
+    Provides the dxf namespace for GenericWrapper.
+
+    """
+    __slots__ = ('_wrapper',)
+
+    def __init__(self, wrapper: 'DXFEntity'):
+        # DXFNamespace.__setattr__ can not set _wrapper
+        super(DXFNamespace, self).__setattr__('_wrapper', wrapper)
+
+    def __getattr__(self, attrib: str):
+        """
+        Returns value of DXF attribute *attrib*. usage: value = DXFEntity.dxf.attrib
+
+        """
+        return self._wrapper.get_dxf_attrib(attrib)
+
+    def __setattr__(self, attrib: str, value) -> None:
+        """
+        Set DXF attribute *attrib* to *value.  usage: DXFEntity.dxf.attrib = value
+
+        """
+        return self._wrapper.set_dxf_attrib(attrib, value)
+
+    def __delattr__(self, attrib: str) -> None:
+        """
+        Remove DXF attribute *attrib*.  usage: del DXFEntity.dxf.attrib
+
+        """
+        return self._wrapper.del_dxf_attrib(attrib)
+
+
+class DXFEntity:
+    __slots__ = ('tags', 'dxf', 'drawing')
+    TEMPLATE = None
+    CLASS = None
+    DXFATTRIBS = {}
+
+    def __init__(self, tags: ExtendedTags, drawing: 'Drawing' = None):
+        self.tags = tags  # DXF tags stored as DXFTag (and inherited) in an ExtendedTags container
+        self.dxf = DXFNamespace(self)  # type: Any # dynamic DXF attribute dispatching, e.g. DXFEntity.dxf.layer
+        self.drawing = drawing
+
+    def __str__(self) -> str:
+        """
+        Returns a simple string representation.
+
+        """
+        return "{}(#{})".format(self.dxftype(), self.dxf.handle)
+
+    def __repr__(self) -> str:
+        """
+        Returns a simple string representation including the class.
+
+        """
+        return str(self.__class__) + " " + str(self)
+
+    @property
+    def dxffactory(self) -> 'DXFFactoryType':
+        return self.drawing.dxffactory
+
+    @property
+    def dxfversion(self) -> str:
+        return self.drawing.dxfversion
+
+    @property
+    def entitydb(self) -> 'EntityDB':
+        return self.drawing.entitydb
+
+    @classmethod
+    def new(cls, handle: str, dxfattribs: dict = None, drawing: 'Drawing' = None) -> 'DXFEntity':
+        if cls.TEMPLATE is None:
+            raise NotImplementedError("new() for type %s not implemented." % cls.__name__)
+        entity = cls(cls.TEMPLATE.clone(), drawing)
+        entity.dxf.handle = handle
+        if dxfattribs is not None:
+            if 'layer' in dxfattribs:
+                layer_name = dxfattribs['layer']
+                if not is_valid_layer_name(layer_name):
+                    raise DXFInvalidLayerName("Invalid layer name '{}'".format(layer_name))
+            entity.update_dxf_attribs(dxfattribs)
+        entity.post_new_hook()
+        return entity
+
+    def post_new_hook(self) -> None:
+        """
+        Called after entity creation.
+
+        """
+        pass
+
+    def _new_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
+        """
+        Create new entity with same layout settings as *self*.
+
+        Used by INSERT & POLYLINE to create appended DXF entities, don't use it to create new standalone entities.
+
+        """
+        entity = self.dxffactory.create_db_entry(type_, dxfattribs)
+        self.dxffactory.copy_layout(self, entity)
+        return entity
+
+    def __copy__(self) -> 'DXFEntity':
+        """
+        Deep copy of DXFEntity with new handle and duplicated linked entities (VERTEX, ATTRIB, SEQEND).
+        The new entity is not included in any layout space, so the owner tag is set to '0' for undefined owner/layout.
+
+        Use Layout.add_entity(new_entity) to add the duplicated entity to a layout, layout can be the model space,
+        a paper space layout or a block layout.
+
+        It is not called __deepcopy__, because this is not a deep copy in the meaning of Python, because handle, link
+        and owner is changed.
+
+        """
+        new_tags = self.entitydb.duplicate_tags(self.tags)
+        entity = self.dxffactory.wrap_entity(new_tags)
+        if self.supports_dxf_attrib('owner'):  # R2000+
+            entity.dxf.owner = '0'  # reset ownership/layout
+        return entity
+
+    copy = __copy__
+
+    def linked_entities(self) -> Iterable['DXFEntity']:
+        """
+        Iterate over all linked entities, only POLYLINE and INSERT has linked entities (VERTEX, ATTRIB, SEQEND)
+
+        Yields: DXFEntity() objects
+
+        """
+        link = self.tags.link  # type: str
+        wrap = self.dxffactory.wrap_handle
+        while link is not None:
+            entity = wrap(link)
+            yield entity
+            link = entity.tags.link
+
+    def copy_to_layout(self, layout: 'GenericLayoutType') -> 'DXFEntity':
+        """
+        Copy entity to another layout.
+
+        Args:
+            layout: any layout (model space, paper space, block)
+
+        Returns: new created entity as DXFEntity() object
+
+        """
+        new_entity = self.copy()
+        layout.add_entity(new_entity)
+        return new_entity
+
+    def move_to_layout(self, layout: 'GenericLayoutType', source: 'GenericLayoutType' = None) -> None:
+        """
+        Move entity from model space or a paper space layout to another layout. For block layout as source, the
+        block layout has to be specified.
+
+        Args:
+            layout: any layout (model space, paper space, block)
+            source: provide source layout, faster for DXF R12, if entity is in a block layout
+
+        """
+        if source is None:
+            source = self.get_layout()
+            if source is None:
+                raise DXFValueError('Source layout for entity not found.')
+        source.move_to_layout(self, layout)
+
+    def dxftype(self) -> str:
+        return self.tags.noclass[0].value
+
+    def _get_dxfattr_definition(self, key: str) -> 'DXFAttr':
+        try:
+            return self.DXFATTRIBS[key]
+        except KeyError:
+            raise DXFAttributeError(key)
+
+    def get_dxf_attrib(self, key: str, default: Any = DXFValueError) -> 'TagValue':
+        dxfattr = self._get_dxfattr_definition(key)
+        return dxfattr.get_attrib(self, key, default)
+
+    def set_dxf_attrib(self, key: str, value: 'TagValue') -> None:
+        dxfattr = self._get_dxfattr_definition(key)
+        dxfattr.set_attrib(self, key, value)
+
+    def del_dxf_attrib(self, key: str) -> None:
+        dxfattr = self._get_dxfattr_definition(key)
+        dxfattr.del_attrib(self)
+
+    def supports_dxf_attrib(self, key: str) -> bool:
+        """
+        Returns True if DXF attribute key is supported else False. Does not grant that attribute key really exists.
+
+        """
+        dxfattr = self.DXFATTRIBS.get(key, None)
+        if dxfattr is None:
+            return False
+        if dxfattr.dxfversion is None:
+            return True
+        return self.drawing.dxfversion >= dxfattr.dxfversion
+
+    def dxf_attrib_exists(self, key: str) -> bool:
+        """
+        Returns True if DXF attrib key really exists else False. Raises AttributeError if key isn't supported.
+
+        """
+        # attributes with default values don't raise an exception!
+        return self.get_dxf_attrib(key, default=None) is not None
+
+    def valid_dxf_attrib_names(self) -> Iterable[str]:
+        """
+        Returns a list of supported DXF attribute names.
+
+        """
+        return [key for key, attrib in self.DXFATTRIBS.items() if
+                attrib.dxfversion is None or (attrib.dxfversion <= self.drawing.dxfversion)]
+
+    def get_dxf_default_value(self, key: str) -> 'TagValue':
+        """
+        Returns the default value as defined in the DXF standard.
+
+        """
+        return self._get_dxfattr_definition(key).default
+
+    def has_dxf_default_value(self, key: str) -> bool:
+        """
+        Returns True if the DXF attribute key has a DXF standard default value.
+
+        """
+        return self._get_dxfattr_definition(key).default is not None
+
+    def dxfattribs(self) -> dict:
+        """
+        Clones defined and existing DXF attributes as dict.
+
+        """
+        dxfattribs = {}
+        for key in self.DXFATTRIBS.keys():
+            value = self.get_dxf_attrib(key, default=None)
+            if value is not None:
+                dxfattribs[key] = value
+        return dxfattribs
+
+    clone_dxf_attribs = dxfattribs
+
+    def update_dxf_attribs(self, dxfattribs: Mapping) -> None:
+        for key, value in dxfattribs.items():
+            self.set_dxf_attrib(key, value)
+
+    def set_flag_state(self, flag: int, state: bool = True, name: str = 'flags') -> None:
+        flags = self.get_dxf_attrib(name, 0)
+        self.set_dxf_attrib(name, set_flag_state(flags, flag, state=state))
+
+    def get_flag_state(self, flag: int, name: str = 'flags') -> bool:
+        return bool(self.get_dxf_attrib(name, 0) & flag)
+
+    def ocs(self) -> OCS:
+        extrusion = self.get_dxf_attrib('extrusion', default=(0, 0, 1))
+        return OCS(extrusion)
+
+    def destroy(self) -> None:
+        if self.has_extension_dict():
+            xdict = self.get_extension_dict()
+            self.drawing.objects.delete_entity(xdict)
+
+    def has_app_data(self, appid: str) -> bool:
+        return self.tags.has_app_data(appid)
+
+    def get_app_data(self, appid: str) -> Tags:
+        return self.tags.get_app_data_content(appid)
+
+    def set_app_data(self, appid: str, app_data_tags: 'IterableTags') -> None:
+        if self.tags.has_app_data(appid):
+            self.tags.set_app_data_content(appid, app_data_tags)
+        else:
+            self.tags.new_app_data(appid, app_data_tags)
+
+    def has_xdata(self, appid: str) -> bool:
+        return self.tags.has_xdata(appid)
+
+    def get_xdata(self, appid: str) -> Tags:
+        return Tags(self.tags.get_xdata(appid)[1:])  # without app id tag
+
+    def set_xdata(self, appid: str, xdata_tags: 'IterableTags') -> None:
+        if self.tags.has_xdata(appid):
+            self.tags.set_xdata(appid, xdata_tags)
+        else:
+            self.tags.new_xdata(appid, xdata_tags)
+
+    def has_xdata_list(self, appid: str, name: str) -> bool:
+        """
+        Returns if list `name` from XDATA `appid` exists.
+
+        Args:
+            appid: APPID
+            name: list name
+
+        """
+        try:
+            self.get_xdata_list(appid, name)
+        except DXFValueError:
+            return False
+        else:
+            return True
+
+    def get_xdata_list(self, appid: str, name: str) -> List[Tuple]:
+        """
+        Get list `name` from XDATA `appid`.
+
+        Args:
+            appid: APPID
+            name: list name
+
+        Returns: list of DXFTags including list name and curly braces '{' '}' tags
+
+        Raises:
+            DXFValueError: XDATA `appid` do not exist or list `name` do not exist
+
+        """
+        xdata = self.get_xdata(appid)
+        try:
+            return get_named_list_from_xdata(name, xdata)
+        except NotFoundException:
+            raise DXFValueError('No data list "{}" not found for APPID "{}"'.format(name, appid))
+
+    def set_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None:
+        """
+        Create new list `name` of XDATA `appid` with `xdata_tags` and replaces list `name` if already exists.
+
+        Args:
+            appid: APPID
+            name: list name
+            xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will
+                        be added
+        """
+        if not self.tags.has_xdata(appid):
+            self.tags.new_xdata(appid, xdata_list(name, xdata_tags))
+        else:
+            self.replace_xdata_list(appid, name, xdata_tags)
+
+    def discard_xdata_list(self, appid: str, name: str) -> None:
+        """
+        Deletes list `name` from XDATA `appid`. Ignores silently if XDATA `appid` or list `name` not exists.
+
+        Args:
+            appid: APPID
+            name: list name
+
+        """
+        try:
+            xdata = self.get_xdata(appid)
+        except DXFValueError:
+            pass
+        else:
+            try:
+                tags = remove_named_list_from_xdata(name, xdata)
+            except NotFoundException:
+                pass
+            else:
+                self.set_xdata(appid, tags)
+
+    def replace_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None:
+        """
+        Replaces list `name` of existing XDATA `appid` with `xdata_tags`. Appends new list if list `name` do not exist,
+        but raises `DXFValueError` if XDATA `appid` do not exist.
+
+        Low level interface, if not sure use `set_xdata_list()` instead.
+
+        Args:
+            appid: APPID
+            name: list name
+            xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will
+                        be added
+        Raises:
+            DXFValueError: XDATA `appid` do not exist
+
+        """
+        xdata = self.get_xdata(appid)
+        try:
+            tags = remove_named_list_from_xdata(name, xdata)
+        except NotFoundException:
+            tags = xdata
+        tags.extend(xdata_list(name, xdata_tags))
+        self.tags.set_xdata(appid, tags)
+
+    def has_reactors(self) -> bool:
+        return self.has_app_data(ACAD_REACTORS)
+
+    def get_reactors(self) -> List[str]:
+        reactor_tags = self.get_app_data(ACAD_REACTORS)
+        return [tag.value for tag in reactor_tags]
+
+    def set_reactors(self, reactor_handles: Iterable[str]) -> None:
+        reactor_tags = [(330, handle) for handle in reactor_handles]
+        self.set_app_data(ACAD_REACTORS, reactor_tags)
+
+    def append_reactor_handle(self, handle: str) -> None:
+        reactors = set(self.get_reactors())
+        reactors.add(handle)
+        self.set_reactors(sorted(reactors, key=lambda x: int(x, base=16)))
+
+    def remove_reactor_handle(self, handle: str) -> None:
+        reactors = set(self.get_reactors())
+        reactors.discard(handle)
+        self.set_reactors(reactors)
+
+    def has_extension_dict(self) -> bool:
+        return self.has_app_data(ACAD_XDICTIONARY)
+
+    def get_extension_dict(self) -> 'DXFDictionary':
+        """
+        Get associated extension dictionary as DXFDictionary() object.
+
+        """
+        app_data = self.get_app_data(ACAD_XDICTIONARY)
+        if len(app_data) == 0 or app_data[0].code != 360:
+            raise DXFStructureError("XDICTIONARY error in entity: " + str(self))
+        # are more than one XDICTIONARY possible?
+        xdict_handle = app_data[0].value
+        return cast('DXFDictionary', self.dxffactory.wrap_handle(xdict_handle))
+
+    def new_extension_dict(self) -> 'DXFDictionary':
+        """
+        Creates and assigns a new extensions dictionary. Link to an existing extension dictionary will be lost.
+
+        """
+        xdict = self.drawing.objects.add_dictionary(owner=self.dxf.handle)
+        self.set_app_data(ACAD_XDICTIONARY, [(360, xdict.dxf.handle)])
+        return xdict
+
+    def get_layout(self) -> 'GenericLayoutType':
+        return self.dxffactory.get_layout_for_entity(self)
+
+    def audit(self, auditor):
+        """
+        Audit entity for errors.
+
+        Args:
+            auditor: Audit() object
+
+        """
+        pass
+
+    def has_embedded_objects(self) -> bool:
+        return any(tags.has_embedded_objects() for tags in self.tags.subclasses)
+
+
+OPEN_LIST = (1002, '{')
+CLOSE_LIST = (1002, '}')
+
+
+def xdata_list(name: str, xdata_tags: 'IterableTags') -> List[Tuple]:
+    tags = []
+    if name:
+        tags.append((1000, name))
+    tags.append(OPEN_LIST)
+    tags.extend(xdata_tags)
+    tags.append(CLOSE_LIST)
+    return tags
+
+
+def remove_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]:
+    start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
+    del tags[start: end]
+    return tags
+
+
+def get_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]:
+    start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
+    return tags[start: end]
+
+
+def get_start_and_end_of_named_list_in_xdata(name: str, tags: List[Tuple]) -> Tuple[int, int]:
+    start = None
+    end = None
+    level = 0
+    for index in range(len(tags)):
+        tag = tags[index]
+
+        if start is None and tag == (1000, name):
+            next_tag = tags[index + 1]
+            if next_tag == OPEN_LIST:
+                start = index
+                continue
+        if start is not None:
+            if tag == OPEN_LIST:
+                level += 1
+            elif tag == CLOSE_LIST:
+                level -= 1
+            if level == 0:
+                end = index
+                break
+
+    if start is None:
+        raise NotFoundException
+    if end is None:
+        raise DXFXDataError('Invalid XDATA structure: missing  (1002, "}").')
+    return start, end + 1

+ 18 - 0
venv/lib/python3.7/site-packages/ezdxf/dxffactory.py

@@ -0,0 +1,18 @@
+# Purpose: dxf-factory-factory
+# Created: 11.03.2011
+# Copyright (C) 2011, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING
+from ezdxf.legacy import LegacyDXFFactory
+from ezdxf.modern import ModernDXFFactory
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFFactoryType, Drawing
+
+
+def dxffactory(drawing: 'Drawing') -> 'DXFFactoryType':
+    dxfversion = drawing.dxfversion
+    factory_class = LegacyDXFFactory if dxfversion <= 'AC1009' else ModernDXFFactory
+    return factory_class(drawing)
+
+

+ 47 - 0
venv/lib/python3.7/site-packages/ezdxf/entityspace.py

@@ -0,0 +1,47 @@
+# Purpose: entity space
+# Created: 13.03.2011
+# Copyright (C) 2011, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # import forward declarations
+    from ezdxf.eztypes import EntityDB, ExtendedTags, TagWriter, DXFEntity
+
+
+class EntitySpace(list):
+    """
+    An EntitySpace is a collection of drawing entities.
+    The ENTITY section is such an entity space, but also blocks.
+    The EntitySpace stores only handles to the drawing entity database.
+
+    """
+
+    def __init__(self, entitydb: 'EntityDB'):
+        self._entitydb = entitydb
+
+    def get_tags_by_handle(self, handle: str) -> 'ExtendedTags':
+        return self._entitydb[handle]
+
+    def store_tags(self, tags: 'ExtendedTags') -> str:
+        handle = tags.get_handle()
+        self.append(handle)
+        return handle
+
+    def write(self, tagwriter: 'TagWriter') -> None:
+        for handle in self:
+            # write linked entities
+            while handle is not None:
+                tags = self._entitydb[handle]
+                tagwriter.write_tags(tags)
+                handle = tags.link
+
+    def delete_entity(self, entity: 'DXFEntity') -> None:
+        # do not delete database objects - entity space just manage handles
+        self.remove(entity.dxf.handle)
+
+    def delete_all_entities(self) -> None:
+        # do not delete database objects - entity space just manage handles
+        del self[:]
+
+    def add_handle(self, handle: str) -> None:
+        self.append(handle)

+ 113 - 0
venv/lib/python3.7/site-packages/ezdxf/eztypes.py

@@ -0,0 +1,113 @@
+# Created: 11.12.2018
+# Copyright (c) 2018, Manfred Moitzi
+# License: MIT License
+"""
+ezdxf typing collection
+
+Only usable in type checking mode:
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFTag
+
+"""
+from typing import *
+
+if TYPE_CHECKING:
+    # Low level stuff
+    from ezdxf.math.vector import Vector, Vec2
+    from ezdxf.math.matrix44 import Matrix44
+    from ezdxf.math.bbox import BoundingBox, BoundingBox2d
+    from ezdxf.tools.handle import HandleGenerator
+    from ezdxf.lldxf.types import DXFTag, DXFBinaryTag, DXFVertex
+    from ezdxf.lldxf.attributes import XType, DXFAttr
+    from ezdxf.lldxf.tags import Tags
+    from ezdxf.lldxf.extendedtags import ExtendedTags
+    from ezdxf.lldxf.tagwriter import TagWriter
+    from ezdxf.tools.complex_ltype import ComplexLineTypePart
+
+    # Entity factories
+    from ezdxf.legacy.factory import LegacyDXFFactory
+    from ezdxf.modern.factory import ModernDXFFactory
+
+    from ezdxf.legacy.layouts import DXF12Layout, DXF12BlockLayout
+    from ezdxf.modern.layouts import Layout, BlockLayout
+
+    # Entities manager
+    from ezdxf.entityspace import EntitySpace
+    from ezdxf.drawing import Drawing
+    from ezdxf.database import EntityDB
+
+    # Sections and Tables
+    from ezdxf.sections.table import Table, ViewportTable
+    from ezdxf.sections.blocks import BlocksSection
+    from ezdxf.sections.header import HeaderSection
+    from ezdxf.sections.tables import TablesSection
+    from ezdxf.sections.blocks import BlocksSection
+    from ezdxf.sections.classes import ClassesSection
+    from ezdxf.sections.objects import ObjectsSection
+    from ezdxf.sections.entities import EntitySection
+    from ezdxf.sections.unsupported import UnsupportedSection
+
+    # Table entries
+    from ezdxf.modern.tableentries import BlockRecord, Layer, Linetype, Style, DimStyle
+    from ezdxf.modern.tableentries import UCS, View, AppID, VPort
+
+    # Style Manager
+    from ezdxf.modern.dxfgroups import GroupManager
+    from ezdxf.modern.material import MaterialManager
+    from ezdxf.modern.mleader import MLeaderStyleManager
+    from ezdxf.modern.mline import MLineStyleManager
+    from ezdxf.dimstyleoverride import DimStyleOverride
+
+    # DXF objects
+    from ezdxf.modern.dxfobjects import DXFObject
+    from ezdxf.modern.dxfdict import DXFDictionary
+    from ezdxf.modern.geodata import GeoData
+    from ezdxf.modern.sortentstable import SortEntitiesTable
+
+    # DXF entities
+    from ezdxf.dxfentity import DXFEntity
+    from ezdxf.legacy.graphics import Line, Point, Circle, Arc, Shape
+    from ezdxf.legacy.trace import Solid, Trace, Face
+    from ezdxf.legacy.polyline import Polyline, Polyface, Polymesh, DXFVertex
+    from ezdxf.legacy.insert import Insert
+    from ezdxf.legacy.attrib import Attdef, Attrib
+    from ezdxf.legacy.dimension import Dimension
+    from ezdxf.legacy.text import Text
+
+    from ezdxf.modern.spline import Spline
+    from ezdxf.modern.viewport import Viewport
+    from ezdxf.modern.block import Block
+    from ezdxf.modern.image import ImageDef, Image
+    from ezdxf.modern.underlay import UnderlayDef, Underlay
+    from ezdxf.modern.mesh import Mesh
+    from ezdxf.modern.hatch import Hatch
+    from ezdxf.modern.lwpolyline import LWPolyline
+    from ezdxf.modern.ellipse import Ellipse
+    from ezdxf.modern.ray import Ray, XLine
+    from ezdxf.modern.mtext import MText
+    from ezdxf.modern.solid3d import Solid3d, Body, Region
+    from ezdxf.modern.surface import Surface, ExtrudedSurface, RevolvedSurface, LoftedSurface, SweptSurface
+    from ezdxf.render.dimension import BaseDimensionRenderer
+
+    # other
+    from ezdxf.audit import Auditor
+    from ezdxf.lldxf.tags import DXFInfo
+
+    # Type compositions
+    Vertex = Union[Sequence[float], Vector, Vec2]
+    VecXY = Union[Vec2, Vector]  # Vector with x and y attributes
+    TagValue = Union[str, int, float, Sequence[float], Vector]
+    RGB = Tuple[int, int, int]
+    IterableTags = Iterable[Tuple[int, TagValue]]
+    SectionDict = Dict[str, List[Union[Tags, ExtendedTags]]]
+    KeyFunc = Callable[['DXFEntity'], Hashable]
+    FaceType = Sequence[Vertex]
+
+    # Type Unions
+    DXFFactoryType = Union[LegacyDXFFactory, ModernDXFFactory]
+    LayoutType = Union[DXF12Layout, Layout]
+    BlockLayoutType = Union[DXF12BlockLayout, BlockLayout]
+    GenericLayoutType = Union[LayoutType, BlockLayoutType]
+    SectionType = Union[
+        HeaderSection, TablesSection, BlocksSection, ClassesSection, ObjectsSection, EntitySection, UnsupportedSection]

+ 177 - 0
venv/lib/python3.7/site-packages/ezdxf/filemanagement.py

@@ -0,0 +1,177 @@
+# Purpose: DXF file testing and opening
+# Created: 05.01.2018
+# Copyright (C) 2018, Manfred Moitzi
+# License: MIT License
+# Local imports to avoid cyclic import
+from typing import TextIO, TYPE_CHECKING, Union, Sequence
+from ezdxf.tools.standards import setup_drawing
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Drawing, DXFInfo
+
+
+def new(dxfversion: str = 'AC1009', setup: Union[str, bool, Sequence[str]] = None) -> 'Drawing':
+    """
+    Create a new DXF drawing.
+
+    new() can create drawings for following DXF versions:
+
+    - AC1009 or R12: AutoCAD R12 (DXF R12)
+    - AC1015 or R2000: AutoCAD 2000 (DXF R2000)
+    - AC1018 or R2004: AutoCAD 2004 (DXF R2004)
+    - AC1021 or R2007: AutoCAD 2007 (DXF R2007)
+    - AC1024 or R2010: AutoCAD 2010 (DXF R2010)
+    - AC1027 or R2013: AutoCAD 2013 (DXF R2013)
+    - AC1032 or R2018: AutoCAD 2018 (DXF R2018)
+
+    Args:
+        dxfversion: DXF version specifier, default is AC1009
+        setup: setup drawing standard for linetypes, text styles, dimension styles
+               None or False: no setup
+               'all' or True: setup all
+               list of topics as strings:
+                  - 'linetypes' ... setup line types
+                  - 'styles'  ... setup text styles
+                  - 'dimstyles ... setup all dimension styles
+                  - 'dimstyles:metric' ... setup metric dimension styles
+                  - 'dimstyles:imperial' ... setup imperial dimension styles (not implemented yet)
+
+    """
+    from ezdxf.drawing import Drawing
+
+    dwg = Drawing.new(dxfversion)
+    if dwg.dxfversion > 'AC1009':
+        dwg.reset_fingerprintguid()
+        dwg.reset_versionguid()
+    if setup:
+        setup_drawing(dwg, topics=setup)
+    return dwg
+
+
+def read(stream: TextIO, legacy_mode: bool = True, dxfversion: str = None) -> 'Drawing':
+    """
+    Read DXF drawing from a text stream, which only needs a readline() method.
+
+    Supported DXF versions:
+
+    - pre AC1009 DXF versions will be upgraded to AC1009, requires encoding set by header var $DWGCODEPAGE
+    - AC1009: AutoCAD R12 (DXF R12), requires encoding set by header var $DWGCODEPAGE
+    - AC1012: AutoCAD R13 upgraded to AC1015, requires encoding set by header var $DWGCODEPAGE
+    - AC1014: AutoCAD R14 upgraded to AC1015, requires encoding set by header var $DWGCODEPAGE
+    - AC1015: AutoCAD 2000, requires encoding set by header var $DWGCODEPAGE
+    - AC1018: AutoCAD 2004, requires encoding set by header var $DWGCODEPAGE
+    - AC1021: AutoCAD 2007, requires encoding='utf-8'
+    - AC1024: AutoCAD 2010, requires encoding='utf-8'
+    - AC1027: AutoCAD 2013, requires encoding='utf-8'
+    - AC1032: AutoCAD 2018, requires encoding='utf-8'
+
+    To detect the required encoding, use the helper function info=dxf_stream_info(stream)
+    and reopen the stream with the detected info.encoding.
+
+    Args:
+        stream: input text stream opened with correct encoding, requires only a readline() method.
+        legacy_mode:  True - adds an extra trouble shooting import layer; False - requires DXF file from modern CAD apps
+        dxfversion: DXF version, None = auto detect, just important for legacy mode.
+
+    """
+    from ezdxf.drawing import Drawing
+
+    return Drawing.read(stream, legacy_mode=legacy_mode, dxfversion=dxfversion)
+
+
+def dxf_file_info(filename: str) -> 'DXFInfo':
+    """
+    Reads basic file information from DXF files: DXF version, encoding and handle seed.
+
+    Returns:
+        DXF info object with attributes: version, release, handseed, encoding
+
+    """
+    with open(filename, mode='rt', encoding='utf-8', errors='ignore') as fp:
+        return dxf_stream_info(fp)
+
+
+def dxf_stream_info(stream: TextIO) -> 'DXFInfo':
+    """
+    Reads basic DXF information from a text stream: DXF version, encoding and handle seed.
+
+    Returns:
+        DXF info object with attributes: version, release, handseed, encoding
+
+    """
+    from ezdxf.lldxf.tags import dxf_info
+
+    info = dxf_info(stream)
+    if info.version >= 'AC1021':  # R2007 files and later are always encoded as UTF-8
+        info.encoding = 'utf-8'
+    return info
+
+
+def readfile(filename: str, encoding: str = None, legacy_mode: bool = False) -> 'Drawing':
+    """
+    Read DXF drawing specified by *filename* from file system.
+
+    Supported DXF versions:
+
+    - pre AC1009 DXF versions will be upgraded to AC1009
+    - AC1009: AutoCAD R12 (DXF R12)
+    - AC1012: AutoCAD R13 upgraded to AC1015
+    - AC1014: AutoCAD R14 upgraded to AC1015
+    - AC1015: AutoCAD 2000
+    - AC1018: AutoCAD 2004
+    - AC1021: AutoCAD 2007, fixates encoding='utf-8'
+    - AC1024: AutoCAD 2010, fixates encoding='utf-8'
+    - AC1027: AutoCAD 2013, fixates encoding='utf-8'
+    - AC1032: AutoCAD 2018, fixates encoding='utf-8'
+
+    Args:
+        filename: DXF filename
+        encoding: use None for auto detect, or set a specific encoding like 'utf-8'
+        legacy_mode: True - adds an extra trouble shooting import layer; False - requires DXF file from modern CAD apps
+
+    """
+    from ezdxf.lldxf.validator import is_dxf_file
+    from ezdxf.tools.codepage import is_supported_encoding
+
+    if not is_dxf_file(filename):
+        raise IOError("File '{}' is not a DXF file.".format(filename))
+
+    info = dxf_file_info(filename)
+    with open(filename, mode='rt', encoding=info.encoding, errors='ignore') as fp:
+        dwg = read(fp, legacy_mode=legacy_mode, dxfversion=info.version)
+
+    dwg.filename = filename
+    if encoding is not None and is_supported_encoding(encoding):
+        dwg.encoding = encoding
+    return dwg
+
+
+def readzip(zipfile: str, filename: str = None) -> 'Drawing':
+    """
+    Read DXF drawing specified by filename from a zip archive, or if filename is None the first DXF file in the zip
+    archive.
+
+    Supported DXF versions:
+
+    - pre AC1009 DXF versions will be upgraded to AC1009
+    - AC1009: AutoCAD R12 (DXF12)
+    - AC1012: AutoCAD R13 upgraded to AC1015
+    - AC1014: AutoCAD R14 upgraded to AC1015
+    - AC1015: AutoCAD 2000
+    - AC1018: AutoCAD 2004
+    - AC1021: AutoCAD 2007
+    - AC1024: AutoCAD 2010
+    - AC1027: AutoCAD 2013
+    - AC1032: AutoCAD 2018
+
+    Args:
+        zipfile: name of the zip archive
+        filename: filename of DXF file, or None to read the first DXF file from the zip archive.
+
+    """
+    from ezdxf.tools.zipmanager import ctxZipReader
+
+    with ctxZipReader(zipfile, filename) as zipstream:
+        dwg = read(zipstream, dxfversion=zipstream.dxfversion)
+        dwg.filename = zipstream.dxf_file_name
+    return dwg

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1094 - 0
venv/lib/python3.7/site-packages/ezdxf/graphicsfactory.py


+ 44 - 0
venv/lib/python3.7/site-packages/ezdxf/groupby.py

@@ -0,0 +1,44 @@
+# Purpose: Grouping entities by DXF attributes or a key function.
+# Created: 03.02.2017
+# Copyright (C) 2017, Manfred Moitzi
+# License: MIT License
+from typing import Callable, Iterable, Hashable, Dict, List, TYPE_CHECKING
+
+from ezdxf.lldxf.const import DXFValueError, DXFAttributeError
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFEntity, KeyFunc
+
+
+def groupby(entities: Iterable['DXFEntity'], dxfattrib: str = '', key: 'KeyFunc' = None) \
+        -> Dict[Hashable, List['DXFEntity']]:
+    """
+    Groups a sequence of DXF entities by an DXF attribute like 'layer', returns the result as dict. Just specify
+    argument `dxfattrib` OR a `key` function.
+
+    Args:
+        entities: sequence of DXF entities to group by a key
+        dxfattrib: grouping DXF attribute like 'layer'
+        key: key function, which accepts a DXFEntity as argument, returns grouping key of this entity or None for ignore
+             this object. Reason for ignoring: a queried DXF attribute is not supported by this entity
+
+    Returns: dict
+
+    """
+    if all((dxfattrib, key)):
+        raise DXFValueError('Specify a dxfattrib or a key function, but not both.')
+    if dxfattrib != '':
+        key = lambda entity: entity.get_dxf_attrib(dxfattrib, None)
+    if key is None:
+        raise DXFValueError('no valid argument found, specify a dxfattrib or a key function, but not both.')
+
+    result = dict()
+    for dxf_entity in entities:
+        try:
+            group_key = key(dxf_entity)
+        except DXFAttributeError:  # ignore DXF entities, which do not support all query attributes
+            continue
+        if group_key is not None:
+            group = result.setdefault(group_key, [])
+            group.append(dxf_entity)
+    return result

+ 5 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/__init__.py

@@ -0,0 +1,5 @@
+# Purpose: legacy dxf factory for R12/AC1009
+# Created: 11.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from .factory import LegacyDXFFactory

+ 200 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/attrib.py

@@ -0,0 +1,200 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from ezdxf.lldxf import const
+from ezdxf.tools import set_flag_state
+
+from .graphics import ExtendedTags, make_attribs, DXFAttr, XType
+from .text import Text
+
+_ATTRIB_TPL = """0
+ATTRIB
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+1
+DEFAULTTEXT
+2
+TAG
+70
+0
+50
+0.0
+51
+0.0
+41
+1.0
+7
+STANDARD
+71
+0
+72
+0
+73
+0
+74
+0
+11
+0.0
+21
+0.0
+31
+0.0
+"""
+
+
+class Attrib(Text):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_ATTRIB_TPL)
+    DXFATTRIBS = make_attribs({
+        'insert': DXFAttr(10, xtype=XType.any_point),
+        'height': DXFAttr(40),
+        'text': DXFAttr(1),
+        'tag': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'field_length': DXFAttr(73, default=0),
+        'rotation': DXFAttr(50, default=0.0),
+        'oblique': DXFAttr(51, default=0.0),
+        'width': DXFAttr(41, default=1.0),  # width factor
+        'style': DXFAttr(7, default='STANDARD'),
+        'text_generation_flag': DXFAttr(71, default=0),  # 2 = backward (mirr-x), 4 = upside down (mirr-y)
+        'halign': DXFAttr(72, default=0),  # horizontal justification
+        'valign': DXFAttr(74, default=0),  # vertical justification
+        'align_point': DXFAttr(11, xtype=XType.any_point),
+    })
+
+    @property
+    def is_const(self) -> bool:
+        """
+        This is a constant attribute.
+        """
+        return bool(self.dxf.flags & const.ATTRIB_CONST)
+
+    @is_const.setter
+    def is_const(self, state: bool) -> None:
+        """
+        This is a constant attribute.
+        """
+        self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_CONST, state)
+
+    @property
+    def is_invisible(self) -> bool:
+        """
+        Attribute is invisible (does not appear).
+        """
+        return bool(self.dxf.flags & const.ATTRIB_INVISIBLE)
+
+    @is_invisible.setter
+    def is_invisible(self, state: bool) -> None:
+        """
+        Attribute is invisible (does not appear).
+        """
+        self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_INVISIBLE, state)
+
+    @property
+    def is_verify(self) -> bool:
+        """
+        Verification is required on input of this attribute. (CAD application feature)
+        """
+        return bool(self.dxf.flags & const.ATTRIB_VERIFY)
+
+    @is_verify.setter
+    def is_verify(self, state: bool) -> None:
+        """
+        Verification is required on input of this attribute. (CAD application feature)
+        """
+        self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_VERIFY, state)
+
+
+    @property
+    def is_preset(self) -> bool:
+        """
+        No prompt during insertion. (CAD application feature)
+        """
+        return bool(self.dxf.flags & const.ATTRIB_IS_PRESET)
+
+    @is_preset.setter
+    def is_preset(self, state: bool) -> None:
+        """
+        No prompt during insertion. (CAD application feature)
+        """
+        self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_IS_PRESET, state)
+
+
+_ATTDEF_TPL = """0
+ATTDEF
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+1
+DEFAULTTEXT
+3
+PROMPTTEXT
+2
+TAG
+70
+0
+50
+0.0
+51
+0.0
+41
+1.0
+7
+STANDARD
+71
+0
+72
+0
+73
+0
+74
+0
+11
+0.0
+21
+0.0
+31
+0.0
+"""
+
+
+class Attdef(Attrib):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_ATTDEF_TPL)
+    DXFATTRIBS = make_attribs({
+        'insert': DXFAttr(10, xtype=XType.any_point),
+        'height': DXFAttr(40),
+        'text': DXFAttr(1),
+        'prompt': DXFAttr(3),
+        'tag': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'field_length': DXFAttr(73, default=0),
+        'rotation': DXFAttr(50, default=0.0),
+        'oblique': DXFAttr(51, default=0.0),
+        'width': DXFAttr(41, default=1.0),  # width factor
+        'style': DXFAttr(7, default='STANDARD'),
+        'text_generation_flag': DXFAttr(71, default=0),  # 2 = backward (mirr-x), 4 = upside down (mirr-y)
+        'halign': DXFAttr(72, default=0),  # horizontal justification
+        'valign': DXFAttr(74, default=0),  # vertical justification
+        'align_point': DXFAttr(11, xtype=XType.any_point),
+    })
+

+ 68 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/block.py

@@ -0,0 +1,68 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from ezdxf.lldxf.const import LAYOUT_NAMES
+
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, DXFAttributes, DefSubclass, XType
+
+_BLOCK_TPL = """0
+BLOCK
+5
+0
+8
+0
+2
+BLOCKNAME
+3
+BLOCKNAME
+70
+0
+10
+0.0
+20
+0.0
+30
+0.0
+1
+
+"""
+
+
+class Block(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_BLOCK_TPL)
+    DXFATTRIBS = make_attribs({
+        'name': DXFAttr(2),
+        'name2': DXFAttr(3),
+        'flags': DXFAttr(70),
+        'base_point': DXFAttr(10, xtype=XType.any_point),
+        'xref_path': DXFAttr(1),
+    })
+    # block entity flags
+    # This is an anonymous block generated by hatching, associative dimensioning,
+    # other internal operations, or an application
+    ANONYMOUS = 1
+
+    # This block has non-constant attribute definitions (this bit is not set if the block has
+    # any attribute definitions that are constant, or has no attribute definitions at all)
+    NON_CONSTANT_ATTRIBUTES = 2
+    XREF = 4  # This block is an external reference (xref)
+    XREF_OVERLAY = 8  # This block is an xref overlay
+    EXTERNAL = 16  # This block is externally dependent
+    RESOLVED = 32  # This is a resolved external reference, or dependent of an external reference (ignored on input)
+    REFERENCED = 64  # This definition is a referenced external reference (ignored on input)
+
+    @property
+    def is_layout_block(self) -> bool:
+        """
+        True if block is a model space or paper space block definition.
+
+        """
+        name = self.dxf.name.lower()
+        return any(name.startswith(layout_name) for layout_name in LAYOUT_NAMES)
+
+
+class EndBlk(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text("  0\nENDBLK\n  5\n0\n")
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {'handle': DXFAttr(5)}))

+ 183 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/dimension.py

@@ -0,0 +1,183 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Type
+from ezdxf.lldxf.const import DXFInternalEzdxfError, DXFValueError
+from ezdxf.lldxf.types import get_xcode_for
+from ezdxf.tools import take2
+import logging
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, XType, DXFAttributes
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DimStyle
+
+logger = logging.getLogger('ezdxf')
+
+_DIMENSION_TPL = """0
+DIMENSION
+5
+0
+8
+0
+2
+*BLOCKNAME
+3
+DIMSTYLE
+10
+0.0
+20
+0.0
+30
+0.0
+11
+0.0
+21
+0.0
+31
+0.0
+70
+0
+1
+
+13
+0.0
+23
+0.0
+33
+0.0
+14
+0.0
+24
+0.0
+34
+0.0
+40
+1.0
+50
+0.0
+"""
+
+
+class Dimension(GraphicEntity):
+    __slots__ = ()
+    LINEAR = 0
+    ALIGNED = 1
+    ANGULAR = 2
+    DIAMETER = 3
+    RADIUS = 4
+    ANGULAR_3P = 5
+    ORDINATE = 6
+    ORDINATE_TYPE = 64
+    USER_LOCATION_OVERRIDE = 128
+
+    TEMPLATE = ExtendedTags.from_text(_DIMENSION_TPL)
+    DXFATTRIBS = make_attribs({
+        'geometry': DXFAttr(2),  # name of pseudo-Block containing the current dimension  entity geometry
+        'dimstyle': DXFAttr(3, default='STANDARD'),  # dimension style name
+        # The dimension style is stored in Drawing.sections.tables.dimstyles,
+        # shortcut Drawings.dimstyles property
+        'defpoint': DXFAttr(10, xtype=XType.any_point),  # WCS, definition point for all dimension types
+        'text_midpoint': DXFAttr(11, xtype=XType.any_point),  # OCS, middle point of dimension text !!! projected onto
+        # the dimension line, not below or above, -> LINEAR: both defpoint and text_midpoint are located at the dimline
+        'insert': DXFAttr(12, xtype=XType.point3d),  # OCS, Insertion point for clones of a dimension — Baseline and Continue
+        'dimtype': DXFAttr(70, default=0),  # Dimension type:
+        # Values 0–6 are integer values that represent the dimension type.
+        # Values 64 and 128 are bit values, which are added to the integer values
+        # 0 = Rotated, horizontal, or vertical;
+        # 1 = Aligned
+        # 2 = Angular;
+        # 3 = Diameter;
+        # 4 = Radius
+        # 5 = Angular 3 point;
+        # 6 = Ordinate
+        # 64 = Ordinate type. This is a bit value (bit 7) used only with integer
+        # value 6. If set, ordinate is X-type; if not set, ordinate is Y-type
+        # 128 = This is a bit value (bit 8) added to the other group 70 values if
+        # the dimension text has been positioned at a user-defined location
+        # rather than at the default location
+        'text': DXFAttr(1),  # dimension text explicitly entered by the user.
+        # If null or "<>", the dimension measurement is drawn as the text,
+        # if " " [one blank space], the text is suppressed.
+        # Anything else is drawn as the text.
+        'defpoint2': DXFAttr(13, xtype=XType.any_point),  # WCS, definition point for linear and angular dimensions
+        'defpoint3': DXFAttr(14, xtype=XType.any_point),  # WCS, definition point for linear and angular dimensions
+        'defpoint4': DXFAttr(15, xtype=XType.any_point),  # WCS, definition point for diameter, radius, and angular dimensions
+        'defpoint5': DXFAttr(16, xtype=XType.any_point),  # OCS, point defining dimension arc for angular dimensions
+        'leader_length': DXFAttr(40),  # leader length for radius and diameter dimensions
+        'angle': DXFAttr(50),  # angle of rotated, horizontal, or vertical linear dimensions
+        'horizontal_direction': DXFAttr(51),
+        # In addition, all dimension types have an optional group
+        # (code 51) that indicates the horizontal direction for the
+        # Dimension entity. This determines the orientation of
+        # dimension text and dimension lines for horizontal,
+        # vertical, and rotated linear dimensions. The group value
+        # is the negative of the Entity Coordinate Systems (ECS)
+        # angle of the UCS X axis in effect when the Dimension was
+        # drawn. The X axis of the UCS in effect when the Dimension
+        # was drawn is always parallel to the XY plane for the
+        # Dimension's ECS, and the angle between the UCS X axis and
+        # the ECS X axis is a single 2D angle. The value in group 51
+        # is the angle from horizontal (the effective X axis) to the
+        # ECS X axis. Entity Coordinate Systems (ECS) are described
+        # later in this section.
+        'oblique_angle': DXFAttr(52),
+        # Linear dimension types with an oblique angle have an
+        # optional group (code 52).When added to the rotation angle
+        # of the linear dimension (group code 50) this gives the
+        # angle of the extension lines
+        'text_rotation': DXFAttr(53),
+        # The optional group code 53  is the rotation angle of the
+        # dimension text away from its default orientation (the direction
+        # of the dimension line).
+    })
+
+    @property
+    def dim_type(self) -> int:
+        return self.dxf.dimtype & 7
+
+    def dim_style(self) -> 'DimStyle':
+        if self.drawing is None:
+            raise DXFInternalEzdxfError('Dimension.drawing attribute not initialized.')
+
+        dim_style_name = self.dxf.dimstyle
+        # raises ValueError if not exists, but all used dim styles should exists!
+        return self.drawing.dimstyles.get(dim_style_name)
+
+    def cast(self) -> 'Dimension':  # for modern dimension lines
+        return self
+
+    def dim_style_attributes(self)->'DXFAttributes':
+        return self.dim_style().DXFATTRIBS
+
+    def set_acad_dstyle(self, data: dict) -> None:
+        tags = []
+        dim_style_attributes = self.dim_style_attributes()
+        for key, value in data.items():
+            if key not in dim_style_attributes:  # ignore unknown attributes, but log
+                logging.debug('ignore unknown DIMSTYLE attribute: "{}"'.format(key))
+                continue
+            dxf_attr = dim_style_attributes.get(key)
+            if dxf_attr and dxf_attr.code > 0:  # skip internal and virtual tags
+                code = dxf_attr.code
+                tags.append((1070, code))
+                if code == 5:  # DimStyle 'dimblk' has group code 5 but is not a handle
+                    tags.append((1000, value))
+                else:
+                    tags.append((get_xcode_for(code), value))
+
+        if len(tags):
+            self.set_xdata_list('ACAD', 'DSTYLE', tags)
+
+    def get_acad_dstyle(self, dim_style: 'DimStyle') -> dict:
+        try:
+            data = self.get_xdata_list('ACAD', 'DSTYLE')
+        except DXFValueError:
+            return {}
+        attribs = {}
+        codes = dim_style.CODE_TO_DXF_ATTRIB
+        for code_tag, value_tag in take2(data):
+            group_code = code_tag.value
+            value = value_tag.value
+            if group_code in codes:
+                attribs[codes[group_code]] = value
+        return attribs

+ 259 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/facemixins.py

@@ -0,0 +1,259 @@
+# Created: 2011-04-30
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import Tuple, TYPE_CHECKING, Iterable, Dict, Sequence, List, cast
+from itertools import chain
+
+from ezdxf.lldxf import const
+
+from .polyfacebuilder import PolyfaceBuilder
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, FaceType, DXFVertex, Polyline
+
+
+class PolymeshMixin:
+    __slots__ = ()
+
+    def set_mesh_vertex(self, pos: Tuple[int, int], point: 'Vertex', dxfattribs: dict = None):
+        """
+        Set location and DXF attributes of a single mesh vertex.
+
+        Args:
+            pos: 0-based (row, col)-tuple, position of mesh vertex
+            point: (x, y, z)-tuple, new 3D coordinates of the mesh vertex
+            dxfattribs: dict of DXF attributes
+
+        """
+        dxfattribs = dxfattribs or {}
+        dxfattribs['location'] = point
+        vertex = self.get_mesh_vertex(pos)
+        vertex.update_dxf_attribs(dxfattribs)
+
+    def get_mesh_vertex(self, pos: Tuple[int, int]) -> 'DXFVertex':
+        """
+        Get location of a single mesh vertex.
+
+        Args:
+            pos: 0-based (row, col)-tuple, position of mesh vertex
+
+        """
+        polyline = cast('Polyline', self)
+        m_count = polyline.dxf.m_count
+        n_count = polyline.dxf.n_count
+        m, n = pos
+        if 0 <= m < m_count and 0 <= n < n_count:
+            pos = m * n_count + n
+            return polyline.__getitem__(pos)
+        else:
+            raise const.DXFIndexError(repr(pos))
+
+    def get_mesh_vertex_cache(self) -> 'MeshVertexCache':
+        """
+        Get a MeshVertexCache() object for this Polymesh. The caching object provides fast access to the location
+        attributes of the mesh vertices.
+
+        """
+        return MeshVertexCache(cast('Polyline', self))
+
+
+class MeshVertexCache:
+    __slots__ = ('vertices',)
+    """
+    Cache mesh vertices in a dict, keys are 0-based (row, col)-tuples.
+
+    vertices:
+       Dict of mesh vertices, keys are 0-based (row, col)-tuples. Writing to this dict doesn't change the DXF entity.
+
+    """
+
+    def __init__(self, mesh: 'Polyline'):
+        self.vertices = self._setup(mesh, mesh.dxf.m_count, mesh.dxf.n_count)  # type: Dict[Tuple[int, int], DXFVertex]
+
+    def _setup(self, mesh: 'Polyline', m_count: int, n_count: int) -> dict:
+        cache = {}  # type: Dict[Tuple[int, int], DXFVertex]
+        vertices = iter(mesh.vertices())
+        for m in range(m_count):
+            for n in range(n_count):
+                cache[(m, n)] = next(vertices)
+        return cache
+
+    def __getitem__(self, pos: Tuple[int, int]) -> 'Vertex':
+        """
+        Get mesh vertex location as (x, y, z)-tuple.
+        """
+        try:
+            return self.vertices[pos].dxf.location
+        except KeyError:
+            raise const.DXFIndexError(repr(pos))
+
+    def __setitem__(self, pos: Tuple[int, int], location: 'Vertex') -> None:
+        """
+        Get mesh vertex location as (x, y, z)-tuple.
+        """
+        try:
+            self.vertices[pos].dxf.location = location
+        except KeyError:
+            raise const.DXFIndexError(repr(pos))
+
+
+class PolyfaceMixin:
+    __slots__ = ()
+    """
+    Order of mesh_vertices and face_records is important (DXF R2010):
+
+        1. mesh_vertices: the polyface mesh vertex locations
+        2. face_records: indices of the face forming vertices
+
+    """
+
+    def append_face(self, face: 'FaceType', dxfattribs: dict = None) -> None:
+        """
+        Appends a single face. Appending single faces is very inefficient, try collecting single faces and use
+        Polyface.append_faces().
+
+        Args:
+            face: list of (x, y, z)-tuples
+            dxfattribs: dict of DXF attributes
+
+        """
+        self.append_faces([face], dxfattribs)
+
+    def append_faces(self, faces: Iterable['FaceType'], dxfattribs: dict = None) -> None:
+        """
+        Append multiple *faces*. *faces* is a list of single faces and a single face is a list of (x, y, z)-tuples.
+
+        Args:
+            faces: list of (list of (x, y, z)-tuples)
+            dxfattribs: dict of DXF attributes
+
+        """
+
+        def new_face_record() -> 'DXFVertex':
+            dxfattribs['flags'] = const.VTX_3D_POLYFACE_MESH_VERTEX
+            return self._new_entity('VERTEX', dxfattribs)
+
+        dxfattribs = dxfattribs or {}
+
+        existing_vertices, existing_faces = self.indexed_faces()
+        # existing_faces is a generator, can't append new data
+        new_faces = []  # type: List[FaceProxy]
+        for face in faces:
+            # convert face point coordinates to DXF Vertex() objects.
+            face_mesh_vertices = cast('Polyline', self)._points_to_dxf_vertices(face, {})  # type: List[DXFVertex]
+            # index of first new vertex
+            index = len(existing_vertices)
+            existing_vertices.extend(face_mesh_vertices)
+            # create a new face_record with all indices set to 0
+            face_record = FaceProxy(new_face_record(), existing_vertices)
+            # set correct indices
+            face_record.indices = tuple(range(index, index + len(face_mesh_vertices)))
+            new_faces.append(face_record)
+        self._rebuild(chain(existing_faces, new_faces))
+
+    def _rebuild(self, faces: Iterable['FaceProxy'], precision: int = 6) -> None:
+        """
+        Build a valid Polyface structure out of *faces*.
+
+        Args:
+            faces: iterable of FaceProxy objects.
+
+        """
+        polyface_builder = PolyfaceBuilder(faces, precision=precision)
+        polyline = cast('Polyline', self)
+        polyline._unlink_all_vertices()  # but don't remove it from database
+        polyline._append_vertices(polyface_builder.get_vertices())
+        self.update_count(polyface_builder.nvertices, polyface_builder.nfaces)
+
+    def update_count(self, nvertices: int, nfaces: int) -> None:
+        polyline = cast('Polyline', self)
+        polyline.dxf.m_count = nvertices
+        polyline.dxf.n_count = nfaces
+
+    def optimize(self, precision: int = 6) -> None:
+        """
+        Rebuilds polyface with vertex optimization. Merges vertices with nearly same vertex locations.
+        Polyfaces created by *ezdxf* are optimized automatically.
+
+        Args:
+            precision: decimal precision for determining identical vertex locations
+
+        """
+        vertices, faces = self.indexed_faces()
+        self._rebuild(faces, precision)
+
+    def faces(self) -> Iterable['DXFVertex']:
+        """
+        Iterate over all faces, a face is a tuple of vertices.
+        result is a list: vertex, vertex, vertex, [vertex,] face_record
+
+        """
+        _, faces = self.indexed_faces()  # just need the faces generator
+        for face in faces:
+            face_vertices = list(face)
+            face_vertices.append(face.face_record)
+            yield face_vertices
+
+    def indexed_faces(self) -> Tuple[List['DXFVertex'], Iterable['FaceProxy']]:
+        """
+        Returns a list of all vertices and a generator of FaceProxy() objects.
+
+        """
+        polyline = cast('Polyline', self)
+        vertices = []
+        face_records = []
+        for vertex in polyline.vertices():  # type: DXFVertex
+            (vertices if vertex.is_poly_face_mesh_vertex else face_records).append(vertex)
+
+        faces = (FaceProxy(face_record, vertices) for face_record in face_records)
+        return vertices, faces
+
+
+class FaceProxy:
+    __slots__ = ('vertices', 'face_record', 'indices')
+    """
+    Represents a single face of a polyface structure.
+
+    vertices:
+
+        List of all polyface vertices.
+
+    face_record:
+
+        The face forming vertex of type ``AcDbFaceRecord``, contains the indices to the face building vertices. Indices
+        of the DXF structure are 1-based and a negative index indicates the beginning of an invisible edge.
+        Face.face_record.dxf.color determines the color of the face.
+
+    indices:
+
+        Indices to the face building vertices as tuple. This indices are 0-base and are used to get vertices from the
+        list *Face.vertices*.
+
+    """
+
+    def __init__(self, face_record: 'DXFVertex', vertices: Sequence['DXFVertex']):
+        self.vertices = vertices  # type: Sequence[DXFVertex]
+        self.face_record = face_record  # type: DXFVertex
+        self.indices = self._indices()  # type: Sequence[int]
+
+    def __len__(self) -> int:
+        return len(self.indices)
+
+    def __getitem__(self, pos: int) -> 'DXFVertex':
+        return self.vertices[self.indices[pos]]
+
+    def __iter__(self) -> Iterable['DXFVertex']:
+        return (self.vertices[index] for index in self.indices)
+
+    def points(self) -> Iterable['Vertex']:
+        return (vertex.dxf.location for vertex in self)
+
+    def _raw_indices(self) -> Iterable[int]:
+        return (self.face_record.get_dxf_attrib(name, 0) for name in const.VERTEXNAMES)
+
+    def _indices(self) -> Sequence[int]:
+        return tuple(abs(index) - 1 for index in self._raw_indices() if index != 0)
+
+    def is_edge_visible(self, pos: int) -> bool:
+        name = const.VERTEXNAMES[pos]
+        return self.face_record.get_dxf_attrib(name) > 0

+ 137 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/factory.py

@@ -0,0 +1,137 @@
+# Purpose: legacy dxf factory for DXF R12/AC1009
+# Created: 11.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Any, Optional
+from ezdxf.lldxf.const import DXFValueError, DXFKeyError
+
+from . import tableentries
+from . import graphics
+from . import polyline
+from . import trace
+from . import text
+from . import attrib
+from . import block
+from . import insert
+from . import dimension
+from . import viewport
+from .layouts import DXF12Layouts, DXF12BlockLayout
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Drawing, EntityDB, HandleGenerator, BlocksSection, DXFEntity, ExtendedTags, \
+        GenericLayoutType
+
+ENTITY_WRAPPERS = {
+    # tables entries
+    'LAYER': tableentries.Layer,
+    'DIMSTYLE': tableentries.DimStyle,
+    'LTYPE': tableentries.Linetype,
+    'APPID': tableentries.AppID,
+    'STYLE': tableentries.Style,
+    'UCS': tableentries.UCS,
+    'VIEW': tableentries.View,
+    'VPORT': tableentries.VPort,
+    # dxf entities
+    'LINE': graphics.Line,
+    'POINT': graphics.Point,
+    'CIRCLE': graphics.Circle,
+    'ARC': graphics.Arc,
+    'TRACE': trace.Trace,
+    'SOLID': trace.Solid,
+    '3DFACE': trace.Face,
+    'TEXT': text.Text,
+    'ATTRIB': attrib.Attrib,
+    'ATTDEF': attrib.Attdef,
+    'INSERT': insert.Insert,
+    'BLOCK': block.Block,
+    'ENDBLK': block.EndBlk,
+    'POLYLINE': polyline.Polyline,
+    'VERTEX': polyline.DXFVertex,
+    'SEQEND': graphics.SeqEnd,
+    'SHAPE': graphics.Shape,
+    'VIEWPORT': viewport.Viewport,
+    'DIMENSION': dimension.Dimension,
+}
+
+
+class LegacyDXFFactory:
+    DEFAULT_WRAPPER = graphics.GraphicEntity
+
+    def __init__(self, drawing: 'Drawing'):
+        self.ENTITY_WRAPPERS = dict(ENTITY_WRAPPERS)
+        self.drawing = drawing
+
+    @property
+    def entitydb(self) -> 'EntityDB':
+        return self.drawing.entitydb
+
+    @property
+    def handles(self) -> 'HandleGenerator':
+        return self.entitydb.handles
+
+    @property
+    def blocks(self) -> 'BlocksSection':
+        return self.drawing.blocks
+
+    @property
+    def dxfversion(self) -> str:
+        return self.drawing.dxfversion
+
+    def new_entity(self, dxftype: str, handle: str, dxfattribs: dict) -> 'DXFEntity':
+        """ Create a new entity. """
+        wrapper_class = self.get_wrapper_class(dxftype)
+        entity = wrapper_class.new(handle, dxfattribs, self.drawing)
+        # track used DXF types, but only for new created DXF entities
+        self.drawing.tracker.dxftypes.add(dxftype)
+        return entity
+
+    def get_wrapper_class(self, dxftype: str) -> Any:
+        # future feature: to track all used DXF types, this is the right place
+        return self.ENTITY_WRAPPERS.get(dxftype, self.DEFAULT_WRAPPER)
+
+    def wrap_entity(self, tags: 'ExtendedTags') -> 'DXFEntity':
+        wrapper_class = self.get_wrapper_class(tags.dxftype())
+        entity = wrapper_class(tags, self.drawing)
+        if hasattr(entity, 'cast'):
+            entity = entity.cast()
+        return entity
+
+    def wrap_handle(self, handle: str) -> 'DXFEntity':
+        try:
+            tags = self.entitydb[handle]
+        except KeyError:
+            raise DXFKeyError('invalid handle #{}'.format(handle))
+
+        return self.wrap_entity(tags)
+
+    def create_db_entry(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
+        """ Create new entity and add to drawing-database. """
+        handle = self.handles.next()
+        dbentry = self.new_entity(type_, handle, dxfattribs)
+        self.entitydb[handle] = dbentry.tags
+        return dbentry
+
+    def get_layouts(self) -> DXF12Layouts:
+        return DXF12Layouts(self.drawing)
+
+    def create_block_entry_in_block_records_table(self, block_layout: 'DXF12BlockLayout') -> None:
+        # required for DXFVERSION > ac1009: Entry in the BLOCK_RECORDS section
+        pass
+
+    def new_block_layout(self, block_handle: str, endblk_handle: str) -> DXF12BlockLayout:
+        return DXF12BlockLayout(self.entitydb, self, block_handle, endblk_handle)
+
+    def copy_layout(self, source_entity: 'DXFEntity', target_entity: 'DXFEntity') -> None:
+        # Place target_entity in same layout as source_entity
+        target_entity.dxf.paperspace = source_entity.dxf.paperspace
+
+    def get_layout_for_entity(self, entity: 'DXFEntity') -> Optional['GenericLayoutType']:
+        dwg = self.drawing
+        layout = dwg.layouts.get_layout_for_entity(entity)
+        if layout is not None:
+            return layout
+        handle = entity.dxf.handle
+        for block in dwg.blocks:  # search block definitions
+            if handle in block._entity_space:
+                return block
+        return None

+ 205 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/graphics.py

@@ -0,0 +1,205 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING
+from ezdxf.lldxf.extendedtags import ExtendedTags
+from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
+from ezdxf.dxfentity import DXFEntity
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Auditor
+
+
+def make_attribs(additional: dict = None) -> DXFAttributes:
+    dxfattribs = {
+        'handle': DXFAttr(5),
+        'layer': DXFAttr(8, default='0'),  # layer name as string, mandatory according to the DXF Reference
+        'linetype': DXFAttr(6, default='BYLAYER'),  # linetype as string, special names BYLAYER/BYBLOCK
+        'color': DXFAttr(62, default=256),  # dxf color index, 0 .. BYBLOCK, 256 .. BYLAYER
+        'thickness': DXFAttr(39, default=0),  # thickness of 2D elements
+        'paperspace': DXFAttr(67, default=0),  # 0=modelspace; 1=paperspace
+        'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),  # Z-axis of OCS (Object-Coordinate-System)
+    }
+    if additional is not None:
+        dxfattribs.update(additional)
+    return DXFAttributes(DefSubclass(None, dxfattribs))
+
+
+class GraphicEntity(DXFEntity):
+    __slots__ = ()
+    """ Default graphic entity wrapper, allows access to following dxf attributes:
+
+     - handle
+     - layer
+     - linetype
+     - color
+     - paperspace
+     - extrusion
+
+     Wrapper for all unsupported graphic entities.
+    """
+    DXFATTRIBS = make_attribs()
+
+    def audit(self, auditor: 'Auditor') -> None:
+        auditor.check_for_valid_layer_name(self)
+        auditor.check_if_linetype_exists(self)
+        auditor.check_for_valid_color_index(self)
+
+
+_LINE_TPL = """0
+LINE
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+11
+1.0
+21
+1.0
+31
+1.0
+"""
+
+
+class Line(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_LINE_TPL)
+    DXFATTRIBS = make_attribs({
+        'start': DXFAttr(10, xtype=XType.any_point),
+        'end': DXFAttr(11, xtype=XType.any_point),
+    })
+
+
+_POINT_TPL = """0
+POINT
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+"""
+
+
+class Point(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_POINT_TPL)
+    DXFATTRIBS = make_attribs({
+        'location': DXFAttr(10, xtype=XType.any_point),
+    })
+
+
+_CIRCLE_TPL = """0
+CIRCLE
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+"""
+
+
+class Circle(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_CIRCLE_TPL)
+    DXFATTRIBS = make_attribs({
+        'center': DXFAttr(10, xtype=XType.any_point),
+        'radius': DXFAttr(40),
+    })
+
+
+_ARC_TPL = """0
+ARC
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+50
+0
+51
+360
+"""
+
+
+class Arc(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_ARC_TPL)
+    DXFATTRIBS = make_attribs({
+        'center': DXFAttr(10, xtype=XType.any_point),
+        'radius': DXFAttr(40),
+        'start_angle': DXFAttr(50),
+        'end_angle': DXFAttr(51),
+    })
+
+
+class SeqEnd(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text("  0\nSEQEND\n  5\n0\n")
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'paperspace': DXFAttr(67, default=0),
+    }))
+
+
+_SHAPE_TPL = """0
+SHAPE
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+2
+NAME
+50
+0.0
+41
+1.0
+51
+0.0
+"""
+
+
+# SHAPE is not tested with real world DXF drawings!
+class Shape(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_SHAPE_TPL)
+    DXFATTRIBS = make_attribs({
+        'insert': DXFAttr(10, xtype=XType.any_point),
+        'size': DXFAttr(40),
+        'name': DXFAttr(2),
+        'rotation': DXFAttr(50, default=0.0),
+        'xscale': DXFAttr(41, default=1.0),
+        'oblique': DXFAttr(51, default=0.0),
+    })

+ 143 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/headervars.py

@@ -0,0 +1,143 @@
+# Copyright (c) 2014-2018, Manfred Moitzi
+# License: MIT License
+# Just one header var definition for all DXF version needed, because AutoCAD ignores unknown header vars
+from functools import partial
+
+from ezdxf.lldxf.hdrvars import SingleValue, Point2D, Point3D
+
+VARMAP = {
+    '$ACADVER': partial(SingleValue, code=1),
+    '$ANGBASE': partial(SingleValue, code=50),
+    '$ANGDIR': partial(SingleValue, code=70),
+    '$ATTDIA': partial(SingleValue, code=70),
+    '$ATTMODE': partial(SingleValue, code=70),
+    '$ATTREQ': partial(SingleValue, code=70),
+    '$AUNITS': partial(SingleValue, code=70),  # 0: decimal degrees; 1: deg/min/sec; 2: grad; 3: radians; 4: surveyor's units
+    '$AUPREC': partial(SingleValue, code=70),
+    '$BLIPMODE': partial(SingleValue, code=70),
+    '$CECOLOR': partial(SingleValue, code=62),
+    '$CELTYPE': partial(SingleValue, code=6),
+    '$CHAMFERA': partial(SingleValue, code=40),
+    '$CHAMFERB': partial(SingleValue, code=40),
+    '$CLAYER': partial(SingleValue, code=8),
+    '$COORDS': partial(SingleValue, code=70),
+    '$DIMALT': partial(SingleValue, code=70),
+    '$DIMALTD': partial(SingleValue, code=70),
+    '$DIMALTF': partial(SingleValue, code=40),
+    '$DIMAPOST': partial(SingleValue, code=1),
+    '$DIMASO': partial(SingleValue, code=70),
+    '$DIMASZ': partial(SingleValue, code=40),
+    '$DIMBLK': partial(SingleValue, code=1),
+    '$DIMBLK1': partial(SingleValue, code=1),
+    '$DIMBLK2': partial(SingleValue, code=1),
+    '$DIMCEN': partial(SingleValue, code=40),
+    '$DIMCLRD': partial(SingleValue, code=70),
+    '$DIMCLRE': partial(SingleValue, code=70),
+    '$DIMCLRT': partial(SingleValue, code=70),
+    '$DIMDLE': partial(SingleValue, code=40),
+    '$DIMDLI': partial(SingleValue, code=40),
+    '$DIMEXE': partial(SingleValue, code=40),
+    '$DIMEXO': partial(SingleValue, code=40),
+    '$DIMGAP': partial(SingleValue, code=40),
+    '$DIMLFAC': partial(SingleValue, code=40),
+    '$DIMLIM': partial(SingleValue, code=70),
+    '$DIMPOST': partial(SingleValue, code=1),
+    '$DIMRND': partial(SingleValue, code=40),
+    '$DIMSAH': partial(SingleValue, code=70),
+    '$DIMSCALE': partial(SingleValue, code=40),
+    '$DIMSE1': partial(SingleValue, code=70),
+    '$DIMSE2': partial(SingleValue, code=70),
+    '$DIMSHO': partial(SingleValue, code=70),
+    '$DIMSOXD': partial(SingleValue, code=70),
+    '$DIMSTYLE': partial(SingleValue, code=2),
+    '$DIMTAD': partial(SingleValue, code=70),
+    '$DIMTFAC': partial(SingleValue, code=40),
+    '$DIMTIH': partial(SingleValue, code=70),
+    '$DIMTIX': partial(SingleValue, code=70),
+    '$DIMTM': partial(SingleValue, code=40),
+    '$DIMTOFL': partial(SingleValue, code=70),
+    '$DIMTOH': partial(SingleValue, code=70),
+    '$DIMTOL': partial(SingleValue, code=70),
+    '$DIMTP': partial(SingleValue, code=40),
+    '$DIMTSZ': partial(SingleValue, code=40),
+    '$DIMTVP': partial(SingleValue, code=40),
+    '$DIMTXT': partial(SingleValue, code=40),
+    '$DIMZIN': partial(SingleValue, code=70),
+    '$DWGCODEPAGE': partial(SingleValue, code=3),
+    '$DRAGMODE': partial(SingleValue, code=70),
+    '$ELEVATION': partial(SingleValue, code=40),
+    '$EXTMAX': Point3D,
+    '$EXTMIN': Point3D,
+    '$FILLETRAD': partial(SingleValue, code=40),
+    '$FILLMODE': partial(SingleValue, code=70),
+    '$HANDLING': partial(SingleValue, code=70),
+    '$HANDSEED': partial(SingleValue, code=5),
+    '$INSBASE': Point3D,
+    '$LIMCHECK': partial(SingleValue, code=70),
+    '$LIMMAX': Point2D,
+    '$LIMMIN': Point2D,
+    '$LTSCALE': partial(SingleValue, code=40),
+    '$LUNITS': partial(SingleValue, code=70),
+    '$LUPREC': partial(SingleValue, code=70),
+    '$MAXACTVP': partial(SingleValue, code=70),
+    '$MENU': partial(SingleValue, code=1),
+    '$MIRRTEXT': partial(SingleValue, code=70),
+    '$ORTHOMODE': partial(SingleValue, code=70),
+    '$OSMODE': partial(SingleValue, code=70),
+    '$PDMODE': partial(SingleValue, code=70),
+    '$PDSIZE': partial(SingleValue, code=40),
+    '$PELEVATION': partial(SingleValue, code=40),
+    '$PEXTMAX': Point3D,
+    '$PEXTMIN': Point3D,
+    '$PLIMCHECK': partial(SingleValue, code=70),
+    '$PLIMMAX': Point2D,
+    '$PLIMMIN': Point2D,
+    '$PLINEGEN': partial(SingleValue, code=70),
+    '$PLINEWID': partial(SingleValue, code=40),
+    '$PSLTSCALE': partial(SingleValue, code=70),
+    '$PUCSNAME': partial(SingleValue, code=2),
+    '$PUCSORG': Point3D,
+    '$PUCSXDIR': Point3D,
+    '$PUCSYDIR': Point3D,
+    '$QTEXTMODE': partial(SingleValue, code=70),
+    '$REGENMODE': partial(SingleValue, code=70),
+    '$SHADEDGE': partial(SingleValue, code=70),
+    '$SHADEDIF': partial(SingleValue, code=70),
+    '$SKETCHINC': partial(SingleValue, code=40),
+    '$SKPOLY': partial(SingleValue, code=70),
+    '$SPLFRAME': partial(SingleValue, code=70),
+    '$SPLINESEGS': partial(SingleValue, code=70),
+    '$SPLINETYPE': partial(SingleValue, code=70),
+    '$SURFTAB1': partial(SingleValue, code=70),
+    '$SURFTAB2': partial(SingleValue, code=70),
+    '$SURFTYPE': partial(SingleValue, code=70),
+    '$SURFU': partial(SingleValue, code=70),
+    '$SURFV': partial(SingleValue, code=70),
+    '$TDCREATE': partial(SingleValue, code=40),
+    '$TDINDWG': partial(SingleValue, code=40),
+    '$TDUPDATE': partial(SingleValue, code=40),
+    '$TDUSRTIMER': partial(SingleValue, code=40),
+    '$TEXTSIZE': partial(SingleValue, code=40),
+    '$TEXTSTYLE': partial(SingleValue, code=7),
+    '$THICKNESS': partial(SingleValue, code=40),
+    '$TILEMODE': partial(SingleValue, code=70),
+    '$TRACEWID': partial(SingleValue, code=40),
+    '$UCSNAME': partial(SingleValue, code=2),
+    '$UCSORG': Point3D,
+    '$UCSXDIR': Point3D,
+    '$UCSYDIR': Point3D,
+    '$UNITMODE': partial(SingleValue, code=70),
+    '$USERI1': partial(SingleValue, code=70),
+    '$USERI2': partial(SingleValue, code=70),
+    '$USERI3': partial(SingleValue, code=70),
+    '$USERI4': partial(SingleValue, code=70),
+    '$USERI5': partial(SingleValue, code=70),
+    '$USERR1': partial(SingleValue, code=40),
+    '$USERR2': partial(SingleValue, code=40),
+    '$USERR3': partial(SingleValue, code=40),
+    '$USERR4': partial(SingleValue, code=40),
+    '$USERR5': partial(SingleValue, code=40),
+    '$USRTIMER': partial(SingleValue, code=70),
+    '$VISRETAIN': partial(SingleValue, code=70),
+    '$WORLDVIEW': partial(SingleValue, code=70),
+}

+ 289 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/insert.py

@@ -0,0 +1,289 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, Tuple, Union, Optional, cast
+from ezdxf.lldxf.const import DXFValueError, DXFKeyError
+
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, XType
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Attrib, Attdef, Vertex
+
+_INSERT_TPL = """0
+INSERT
+5
+0
+8
+0
+2
+BLOCKNAME
+10
+0.0
+20
+0.0
+30
+0.0
+41
+1.0
+42
+1.0
+43
+1.0
+50
+0.0
+"""
+
+
+# IMPORTANT: Bug in AutoCAD 2010
+# attribsfollow = 0, for NO attribsfollow, does not work with ACAD 2010
+# if no attribs attached to the INSERT entity, omit attribsfollow tag
+
+
+class Insert(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_INSERT_TPL)
+    DXFATTRIBS = make_attribs({
+        'attribs_follow': DXFAttr(66, default=0),
+        'name': DXFAttr(2),
+        'insert': DXFAttr(10, xtype=XType.any_point),
+        'xscale': DXFAttr(41, default=1.0),
+        'yscale': DXFAttr(42, default=1.0),
+        'zscale': DXFAttr(43, default=1.0),
+        'rotation': DXFAttr(50, default=0.0),
+        'column_count': DXFAttr(70, default=1),
+        'row_count': DXFAttr(71, default=1),
+        'column_spacing': DXFAttr(44, default=0.0),
+        'row_spacing': DXFAttr(45, default=0.0),
+    })
+
+    def attribs(self) -> Iterable['Attrib']:
+        """
+        Iterate over all appended ATTRIB entities, yields Attrib() objects.
+
+        """
+        if self.dxf.attribs_follow == 0:
+            return
+        dxffactory = self.dxffactory
+        handle = self.tags.link
+        while handle is not None:
+            entity = dxffactory.wrap_handle(handle)
+            next_entity = entity.tags.link
+            if next_entity is None:  # found SeqEnd
+                return
+            else:
+                yield entity
+                handle = next_entity
+
+    def place(self, insert: 'Vertex' = None,
+              scale: Tuple[float, float, float] = None,
+              rotation: float = None) -> 'Insert':
+        """
+        Set placing attributes of the INSERT entity.
+
+        Args:
+            insert: insert position as (x, y [,z]) tuple
+            scale: (scale_x, scale_y, scale_z) tuple
+            rotation (float): rotation angle in degrees
+
+        Returns:
+            Insert object (fluent interface)
+
+        """
+        if insert is not None:
+            self.dxf.insert = insert
+        if scale is not None:
+            if len(scale) != 3:
+                raise DXFValueError("Parameter scale has to be a (x, y, z)-tuple.")
+            x, y, z = scale
+            self.dxf.xscale = x
+            self.dxf.yscale = y
+            self.dxf.zscale = z
+        if rotation is not None:
+            self.dxf.rotation = rotation
+        return self
+
+    def grid(self, size: Tuple[int, int] = (1, 1), spacing: Tuple[float, float] = (1, 1)) -> 'Insert':
+        """
+        Set grid placing attributes of the INSERT entity.
+
+        Args:
+            size: grid size as (row_count, column_count) tuple
+            spacing: distance between placing as (row_spacing, column_spacing) tuple
+
+        Returns:
+            Insert object (fluent interface)
+
+        """
+        if len(size) != 2:
+            raise DXFValueError("Parameter size has to be a (row_count, column_count)-tuple.")
+        if len(spacing) != 2:
+            raise DXFValueError("Parameter spacing has to be a (row_spacing, column_spacing)-tuple.")
+        self.dxf.row_count = size[0]
+        self.dxf.column_count = size[1]
+        self.dxf.row_spacing = spacing[0]
+        self.dxf.column_spacing = spacing[1]
+        return self
+
+    def get_attrib(self, tag: str, search_const: bool = False) -> Optional[Union['Attrib', 'Attdef']]:
+        """
+        Get attached ATTRIB entity by *tag*.
+
+        Args:
+            tag: tag name
+            search_const: search also const ATTDEF entities
+
+        Returns:
+            Attrib or Attdef object
+
+        """
+        for attrib in self.attribs():
+            if tag == attrib.dxf.tag:
+                return attrib
+        if search_const and self.drawing is not None:
+            block = self.drawing.blocks[self.dxf.name]  # raises KeyError() if not found
+            for attdef in block.get_const_attdefs():
+                if tag == attdef.dxf.tag:
+                    return attdef
+        return None
+
+    def get_attrib_text(self, tag: str, default: str = None, search_const: bool = False) -> str:
+        """
+        Get content text of attached ATTRIB entity *tag*.
+
+        Args:
+            tag: tag name
+            default: default value if tag is absent
+            search_const: search also const ATTDEF entities
+
+        Returns:
+            content text as str
+
+        """
+        attrib = self.get_attrib(tag, search_const)
+        if attrib is None:
+            return default
+        return attrib.dxf.text
+
+    def has_attrib(self, tag: str, search_const: bool = False) -> bool:
+        """
+        Check if ATTRIB for *tag* exists.
+
+        Args:
+            tag: tag name
+            search_const: search also const ATTDEF entities
+
+        """
+        return self.get_attrib(tag, search_const) is not None
+
+    def add_attrib(self, tag: str, text: str, insert: 'Vertex' = (0, 0), dxfattribs: dict = None) -> 'Attrib':
+        """
+        Add new ATTRIB entity.
+
+        Args:
+            tag: tag name
+            text: content text
+            insert: insert position as tuple (x, y[, z])
+            dxfattribs: additional DXF attributes
+
+        Returns:
+            Attrib object
+
+        """
+        dxfattribs = dxfattribs or {}
+        dxfattribs['tag'] = tag
+        dxfattribs['text'] = text
+        dxfattribs['insert'] = insert
+        attrib_entity = cast('Attrib', self._new_entity('ATTRIB', dxfattribs))
+        self._append_attrib_entity(attrib_entity)
+        return attrib_entity
+
+    def _append_attrib_entity(self, entity: 'Attrib') -> None:
+        has_no_attribs_attached = self.tags.link is None
+        if has_no_attribs_attached or self.dxf.attribs_follow == 0:
+            prev = self
+            seqend = self._new_entity('SEQEND', {})
+        else:
+            attribs = list(self.attribs())
+            prev = attribs[-1]
+            seqend = self.dxffactory.wrap_handle(prev.tags.link)
+
+        prev.tags.link = entity.dxf.handle
+        entity.tags.link = seqend.dxf.handle
+        self.dxf.attribs_follow = 1
+
+    def delete_attrib(self, tag: str, ignore=False) -> None:
+        """
+        Delete attached ATTRIB entity `tag`, raises a KeyError exception if `tag` does not exist, set `ignore` to True,
+        to ignore not existing ATTRIB entities.
+        
+        Args:
+            tag: ATTRIB name
+            ignore: False -> raise KeyError exception if `tag` does not exist
+
+        """
+        if self.dxf.attribs_follow == 0:
+            if ignore:
+                return
+            else:
+                raise DXFKeyError(tag)
+
+        dxffactory = self.dxffactory
+        handle = self.tags.link
+        prev = self
+        while handle is not None:
+            entity = dxffactory.wrap_handle(handle)
+            next_entity = entity.tags.link
+            if next_entity is None:  # found SeqEnd
+                break
+            else:
+                if entity.dxf.tag == tag:
+                    prev.tags.link = next_entity  # remove entity from linked list
+                    self.entitydb.delete_entity(entity)
+                    self._fix_attribs()
+                    return
+                prev = entity
+                handle = next_entity
+        if not ignore:
+            raise DXFKeyError(tag)
+
+    def delete_all_attribs(self) -> None:
+        """
+        Delete all ATTRIB entities attached to the INSERT entity and the following SEQEND entity. Ignores the value
+        of dxf.attribs_follow.
+         
+        """
+        db = self.entitydb
+        handle = self.tags.link
+        while handle is not None:
+            entity_tags = db[handle]
+            db.delete_handle(handle)
+            handle = entity_tags.link
+            entity_tags.link = None
+
+        self.tags.link = None
+        self.dxf.attribs_follow = 0
+
+    def _fix_attribs(self) -> None:
+        if self.dxf.attribs_follow == 0:
+            self.delete_all_attribs()
+        else:
+            handle = self.tags.link
+            if handle is None:
+                self.dxf.attribs.follow = 0
+                return
+            entity = self.dxffactory.wrap_handle(handle)
+            if entity.dxftype() == 'SEQEND':
+                # last attrib was deleted, only the SEQEND entity remains
+                self.entitydb.delete_entity(entity)
+                self.dxf.attribs_follow = 0
+                self.tags.link = None
+                return
+
+    def destroy(self) -> None:
+        """
+        Delete all attached ATTRIB entities from entity database.
+
+        Caution: this method is meant for internal usage.
+
+        """
+        self.delete_all_attribs()

+ 574 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/layouts.py

@@ -0,0 +1,574 @@
+# Created: 21.03.2011
+# Copyright (C) 2011, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Union, List, Iterable, Tuple, Dict, Hashable, Sequence, Optional
+from ezdxf.graphicsfactory import GraphicsFactory
+from ezdxf.entityspace import EntitySpace
+from ezdxf.query import EntityQuery
+from ezdxf.groupby import groupby
+from ezdxf.lldxf.const import STD_SCALES, DXFValueError
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Drawing, EntityDB, LayoutType, DXFEntity, TagWriter, DXFFactoryType, EntitySpace
+    from ezdxf.eztypes import KeyFunc, GenericLayoutType
+
+
+class DXF12Layouts:
+    """
+    The Layout container.
+
+    """
+
+    def __init__(self, drawing: 'Drawing'):
+        entities = drawing.sections.entities
+        model_space = entities.model_space_entities()
+        self._modelspace = DXF12Layout(model_space, drawing.dxffactory, 0)
+        paper_space = entities.active_layout_entities()
+        self._paperspace = DXF12Layout(paper_space, drawing.dxffactory, 1)
+        entities.clear()  # remove entities for entities section -> stored in layouts
+
+    def __iter__(self) -> 'LayoutType':
+        yield self._modelspace
+        yield self._paperspace
+
+    def __len__(self) -> int:
+        return 2
+
+    def modelspace(self) -> 'LayoutType':
+        return self._modelspace
+
+    def get(self, name: str = "") -> 'LayoutType':
+        # AC1009 supports only one paperspace/layout
+        return self._paperspace
+
+    def names(self) -> List[str]:
+        return []
+
+    def get_layout_for_entity(self, entity: 'DXFEntity') -> 'LayoutType':
+        # paperspace attribute defaults to 0 if not present
+        if entity in self._modelspace:
+            return self._modelspace
+        elif entity in self._paperspace:
+            return self._paperspace
+        else:
+            return None
+
+    def active_layout(self) -> 'LayoutType':
+        return self._paperspace
+
+    def write_entities_section(self, tagwriter: 'TagWriter') -> None:
+        self._modelspace.write(tagwriter)
+        self._paperspace.write(tagwriter)
+
+
+class BaseLayout(GraphicsFactory):
+    """
+    Base class for DXF12Layout() and DXF12BlockLayout()
+
+    Entities are wrapped into class GraphicEntity() or inherited.
+
+    """
+
+    def __init__(self, dxffactory: 'DXFFactoryType', entity_space: 'EnitySpace'):
+        super(BaseLayout, self).__init__(dxffactory)
+        self._entity_space = entity_space
+
+    def __len__(self) -> int:
+        """
+        Entities count.
+
+        """
+        return len(self._entity_space)
+
+    def __iter__(self) -> Iterable['DXFEntity']:
+        """
+        Iterate over all drawing entities in this layout.
+
+        Returns: :class:`DXFEntity`
+
+        """
+        wrap = self._dxffactory.wrap_handle
+        for handle in self._entity_space:
+            yield wrap(handle)
+
+    @property
+    def entitydb(self) -> 'EnityDB':
+        return self._dxffactory.entitydb
+
+    @property
+    def drawing(self) -> 'Drawing':
+        return self._dxffactory.drawing
+
+    def build_and_add_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
+        """
+        Create entity in drawing database and add entity to the entity space.
+
+        Args:
+            type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
+            dxfattribs (dict): DXF attributes for the new entity
+
+        """
+        entity = self.build_entity(type_, dxfattribs)
+        self.add_entity(entity)
+        return entity
+
+    def build_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
+        """
+        Create entity in drawing database, returns a wrapper class inherited from GraphicEntity().
+
+        Adds entity to the drawing database.
+
+        Args:
+            type_ (str): DXF type string, like 'LINE', 'CIRCLE' or 'LWPOLYLINE'
+            dxfattribs(dict): DXF attributes for the new entity
+
+        """
+        entity = self._dxffactory.create_db_entry(type_, dxfattribs)
+        self._set_paperspace(entity)
+        return entity
+
+    def add_entity(self, entity: 'DXFEntity') -> None:
+        """
+        Add an existing :class:`DXFEntity` to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
+        from the previous owner layout.
+
+        """
+        self._entity_space.append(entity.dxf.handle)
+        self._set_paperspace(entity)
+        for linked_entity in entity.linked_entities():
+            self._set_paperspace(linked_entity)
+
+    def unlink_entity(self, entity: 'DXFEntity') -> None:
+        """
+        Unlink `entity` from layout but does not delete entity from the drawing database.
+
+        Removes `entity` just from  entity space but not from the drawing database.
+
+        Args:
+            entity: :class:`DXFEntity`
+
+        """
+        self._entity_space.delete_entity(entity)
+        entity.dxf.paperspace = -1  # set invalid paper space
+        if entity.supports_dxf_attrib('owner'):  # R2000
+            entity.dxf.owner = '0'
+
+    def delete_entity(self, entity: 'DXFEntity') -> None:
+        """
+        Delete `entity` from layout (entity space) and drawing database.
+
+        Args:
+            entity: :class:`DXFEntity`
+
+        """
+        self.entitydb.delete_entity(entity)  # 1. delete from drawing database
+        self.unlink_entity(entity)  # 2. unlink from entity space
+
+    def delete_all_entities(self) -> None:
+        """
+        Delete all entities from Layout (entity space) and from drawing database.
+
+        """
+        # noinspection PyTypeChecker
+        for entity in list(self):  # temp list, because delete modifies the base data structure of the iterator
+            self.delete_entity(entity)
+
+    def _set_paperspace(self, entity: 'DXFEntity') -> None:
+        pass  # only for DXF 2000 and later necessary
+
+    def get_entity_by_handle(self, handle: str) -> 'DXFEntity':
+        """
+        Get entity by handle as GraphicEntity() or inherited.
+
+        """
+        return self._dxffactory.wrap_handle(handle)
+
+    def query(self, query='*') -> EntityQuery:
+        """
+        Get all DXF entities matching the :ref:`entity query string`.
+
+        Args:
+            query: eintity query string
+
+        Returns: :class:`EntityQuery`
+
+        """
+        return EntityQuery(iter(self), query)
+
+    def groupby(self, dxfattrib: str = "", key: 'KeyFunc' = None) -> Dict[Hashable, List['DXFEntity']]:
+        """
+        Returns a dict of entity lists, where entities are grouped by a `dxfattrib` or a `key` function.
+
+        Args:
+            dxfattrib: grouping by DXF attribute like "layer"
+            key: key function, which accepts a :class:`DXFEntity` as argument, returns grouping key of this entity or
+                 None to ignore this object. Reason for ignoring: a queried DXF attribute is not supported by this
+                 entity.
+
+        """
+        return groupby(iter(self), dxfattrib, key)
+
+    def move_to_layout(self, entity: 'DXFEntity', layout: 'GenericLayoutType') -> None:
+        """
+        Move entity from block layout to another layout.
+
+        Args:
+            entity: DXF entity to move
+            layout: any layout (model space, paper space, block)
+
+        """
+        if entity.dxf.handle in self._entity_space:
+            self.unlink_entity(entity)
+            layout.add_entity(entity)
+        else:
+            raise DXFValueError('Layout does not contain entity.')
+
+
+class DXF12Layout(BaseLayout):
+    """
+    Layout representation
+
+    """
+
+    def __init__(self, entityspace: 'EntitySpace', dxffactory: 'DXFFactoryType', paperspace: int = 0):
+        super(DXF12Layout, self).__init__(dxffactory, entityspace)
+        self._paperspace = paperspace
+
+    # start of public interface
+
+    def __contains__(self, entity: Union[str, 'DXFEntity']):
+        """
+        Test if the layout contains the drawing element `entity` (aka `in` operator).
+
+        """
+        if not hasattr(entity, 'dxf'):  # entity is a handle and not a wrapper class
+            entity = self.get_entity_by_handle(entity)
+        return entity.dxf.paperspace == self._paperspace and entity.dxf.handle in self._entity_space
+
+    # end of public interface
+
+    def _set_paperspace(self, entity: 'DXFEntity'):
+        entity.dxf.paperspace = self._paperspace
+
+    def page_setup(self, size: Tuple[int, int] = (297, 210),
+                   margins: Tuple[int, int, int, int] = (0, 0, 0, 0),
+                   units: str = 'mm',
+                   offset: Tuple[int, int] = (0, 0),
+                   rotation: float = 0,
+                   scale: int = 16) -> None:
+        if self._paperspace == 0:
+            raise DXFTypeError("No paper setup for model space.")
+
+        # remove existing viewports
+        for viewport in self.viewports():
+            self.delete_entity(viewport)
+
+        if int(rotation) not in (0, 1, 2, 3):
+            raise DXFValueError("valid rotation values: 0-3")
+
+        if isinstance(scale, int):
+            scale = STD_SCALES.get(scale, (1, 1))
+
+        if scale[0] == 0:
+            raise DXFValueError("scale numerator can't be 0.")
+        if scale[1] == 0:
+            raise DXFValueError("scale denominator can't be 0.")
+
+        scale_factor = scale[1] / scale[0]
+
+        # TODO: don't know how to set inch or mm mode in R12
+        units = units.lower()
+        if units.startswith('inch'):
+            units = 'Inches'
+            plot_paper_units = 0
+            unit_factor = 25.4  # inch to mm
+        elif units == 'mm':
+            units = 'MM'
+            plot_paper_units = 1
+            unit_factor = 1.0
+        else:
+            raise DXFValueError('Supported units: "mm" and "inch"')
+
+        # all viewport parameters are scaled paper space units
+        def paper_units(value):
+            return value * scale_factor
+
+        # TODO: don't know how paper setup in DXF R12 works
+        paper_width, paper_height = size
+
+        # TODO: don't know how margins setup in DXF R12 works
+        margin_top, margin_right, margin_bottom, margin_left = margins
+
+        paper_width = paper_units(size[0])
+        paper_height = paper_units(size[1])
+
+        plimmin = self.drawing.header['$PLIMMIN'] = (0, 0)
+        plimmax = self.drawing.header['$PLIMMAX'] = (paper_width, paper_height)
+
+        # TODO: don't know how paper setup in DXF R12 works
+
+        pextmin = self.drawing.header['$PEXTMIN'] = (0, 0, 0)
+        pextmax = self.drawing.header['$PEXTMAX'] = (paper_width, paper_height, 0)
+
+        # printing area
+        printable_width = paper_width - paper_units(margin_left) - paper_units(margin_right)
+        printable_height = paper_height - paper_units(margin_bottom) - paper_units(margin_top)
+
+        # AutoCAD viewport (window) size
+        vp_width = paper_width * 1.1
+        vp_height = paper_height * 1.1
+
+        # center of printing area
+        center = (printable_width / 2, printable_height / 2)
+
+        # create 'main' viewport
+        main_viewport = self.add_viewport(
+            center=center,  # no influence to 'main' viewport?
+            size=(vp_width, vp_height),  # I don't get it, just use paper size!
+            view_center_point=center,  # same as center
+            view_height=vp_height,  # view height in paper space units
+        )
+        main_viewport.dxf.id = 1  # set as main viewport
+        main_viewport.dxf.status = 2  # AutoCAD default value
+        with main_viewport.edit_data() as vpdata:
+            vpdata.view_mode = 1000  # AutoDesk default
+
+    def get_paper_limits(self) -> Tuple[float, float]:
+        """
+        Returns paper limits in plot paper units
+
+        """
+        limmin = self.drawing.header.get('$PLIMMIN', (0, 0))
+        limmax = self.drawing.header.get('$PLIMMAX', (0, 0))
+        return limmin, limmax
+
+    @property
+    def layout_key(self) -> int:
+        return self._paperspace
+
+    def viewports(self) -> List['DXFEntity']:
+        """
+        Get all VIEWPORT entities defined in the layout. Returns a list of Viewport() objects, sorted by id, the first
+        entity is always the paper space view with the id=1.
+
+        """
+        vports = [entity for entity in self if entity.dxftype() == 'VIEWPORT']
+        vports.sort(key=lambda e: e.dxf.id)
+        return vports
+
+    def renumber_viewports(self) -> None:
+        for num, viewport in enumerate(self.viewports(), start=1):
+            viewport.dxf.id = num
+
+    def write(self, tagwriter: 'TagWriter') -> None:
+        self._entity_space.write(tagwriter)
+
+    def add_viewport(self,
+                     center: Tuple[float, float],
+                     size: Tuple[float, float],
+                     view_center_point: Tuple[float, float],
+                     view_height: float,
+                     dxfattribs: dict = None) -> 'DXFEntity':
+        if dxfattribs is None:
+            dxfattribs = {}
+        else:
+            dxfattribs = dict(dxfattribs)
+        width, height = size
+        attribs = {
+            'center': center,
+            'width': width,
+            'height': height,
+            'status': 1,  # by default highest priority (stack order)
+            'layer': 'VIEWPORTS',  # use separated layer to turn off for plotting
+        }
+        attribs.update(dxfattribs)
+        # DXF R12 (AC1009): view_center_point and view_height (as many other viewport attributes) are not usual
+        # DXF attributes, they are stored as extended DXF tags.
+        viewport = self.build_and_add_entity('VIEWPORT', attribs)
+        viewport.dxf.id = viewport.get_next_viewport_id()
+        with viewport.edit_data() as vp_data:
+            vp_data.view_center_point = view_center_point
+            vp_data.view_height = view_height
+        return viewport
+
+
+class DXF12BlockLayout(BaseLayout):
+    """
+    BlockLayout has the same factory-function as Layout, but is managed
+    in the BlocksSection() class. It represents a DXF Block definition.
+
+    Attributes:
+        _block_handle: db handle to BLOCK entity
+        _endblk_handle: db handle to ENDBLK entity
+        _entityspace: is the block content
+
+    """
+
+    def __init__(self, entitydb: 'EntityDB', dxffactory: 'DXFFactoryType', block_handle: str, endblk_handle: str):
+        super(DXF12BlockLayout, self).__init__(dxffactory, EntitySpace(entitydb))
+        self._block_handle = block_handle
+        self._endblk_handle = endblk_handle
+
+    # start of public interface
+
+    def __contains__(self, entity: 'DXFEntity') -> bool:
+        """
+        Returns True if block contains entity else False. *entity* can be a handle-string, Tags(),
+        ExtendedTags() or a wrapped entity.
+
+        """
+        if hasattr(entity, 'get_handle'):
+            handle = entity.get_handle()
+        elif hasattr(entity, 'dxf'):  # it's a wrapped entity
+            handle = entity.dxf.handle
+        else:
+            handle = entity
+        return handle in self._entity_space
+
+    @property
+    def block(self) -> 'DXFEntity':
+        """ Get associated BLOCK entity. """
+        return self.get_entity_by_handle(self._block_handle)
+
+    @property
+    def endblk(self) -> 'DXFEntity':
+        """ Get associated ENDBLK entity. """
+        return self.get_entity_by_handle(self._endblk_handle)
+
+    @property
+    def name(self) -> str:
+        """ Get block name """
+        return self.block.dxf.name
+
+    @name.setter
+    def name(self, new_name) -> None:
+        """ Set block name """
+        block = self.block
+        block.dxf.name = new_name
+        block.dxf.name2 = new_name
+
+    @property
+    def is_layout_block(self) -> bool:
+        """
+        True if block is a model space or paper space block definition.
+
+        """
+        return self.block.is_layout_block
+
+    def add_attdef(self, tag: str, insert: Sequence[float] = (0, 0), text: str = '',
+                   dxfattribs: dict = None) -> 'DXFEntity':
+        """
+        Add an :class:`Attdef` entity.
+
+        Set position and alignment by the idiom::
+
+            myblock.add_attdef('NAME').set_pos((2, 3), align='MIDDLE_CENTER')
+
+        Args:
+            tag: attribute name (tag) as string without spaces
+            insert: attribute insert point relative to block origin (0, 0, 0)
+            text: preset text for attribute
+
+        """
+        if dxfattribs is None:
+            dxfattribs = {}
+        dxfattribs['tag'] = tag
+        dxfattribs['insert'] = insert
+        dxfattribs['text'] = text
+        return self.build_and_add_entity('ATTDEF', dxfattribs)
+
+    def attdefs(self) -> Iterable['DXFEntity']:
+        """
+        Iterate for all :class:`Attdef` entities.
+
+        """
+        return (entity for entity in self if entity.dxftype() == 'ATTDEF')
+
+    def has_attdef(self, tag: str) -> bool:
+        """
+        Returns `True` if an :class:`Attdef` for `tag` exists else `False`.
+
+        Args:
+            tag: tag name
+
+        """
+        return self.get_attdef(tag) is not None
+
+    def get_attdef(self, tag: str) -> Optional['DXFEntity']:
+        """
+        Get attached :class:`Attdef` entity by `tag`.
+
+        Args:
+            tag: tag name
+
+        Returns: :class:`Attdef`
+
+        """
+        for attdef in self.attdefs():
+            if tag == attdef.dxf.tag:
+                return attdef
+
+    def get_attdef_text(self, tag: str, default: str = None) -> str:
+        """
+        Get content text for :class:`Attdef` `tag` as string or returns `default` if no :class:`Attdef` for `tag` exists.
+
+        Args:
+            tag: tag name
+            default: default value if tag is absent
+
+        """
+        attdef = self.get_attdef(tag)
+        if attdef is None:
+            return default
+        return attdef.dxf.text
+
+    # end of public interface
+
+    def add_entity(self, entity: 'DXFEntity') -> None:
+        """
+        Add an existing DXF entity to a layout, but be sure to unlink (:meth:`~Layout.unlink_entity()`) first the entity
+        from the previous owner layout.
+
+        Args:
+            entity: :class:`DXFEntity`
+
+        """
+        self.add_handle(entity.dxf.handle)
+        entity.dxf.paperspace = 0  # set a model space, because paper space layout is a different class
+        for linked_entity in entity.linked_entities():
+            linked_entity.dxf.paperspace = 0
+
+    def add_handle(self, handle: str) -> None:
+        """
+        Add entity by handle to the block entity space.
+
+        """
+        self._entity_space.append(handle)
+
+    def write(self, tagwriter: 'TagWriter') -> None:
+        def write_tags(handle):
+            tags = self._entity_space.get_tags_by_handle(handle)
+            tagwriter.write_tags(tags)
+
+        write_tags(self._block_handle)
+        self._entity_space.write(tagwriter)
+        write_tags(self._endblk_handle)
+
+    def delete_all_entities(self) -> None:
+        # 1. delete from database
+        for handle in self._entity_space:
+            del self.entitydb[handle]
+        # 2. delete from entity space
+        self._entity_space.delete_all_entities()
+
+    def destroy(self) -> None:
+        self.delete_all_entities()
+        del self.entitydb[self._block_handle]
+        del self.entitydb[self._endblk_handle]
+
+    def get_const_attdefs(self) -> Iterable['DXFEntity']:
+        """
+        Returns a generator for constant ATTDEF entities.
+
+        """
+        return (attdef for attdef in self.attdefs() if attdef.is_const)

+ 54 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/polyfacebuilder.py

@@ -0,0 +1,54 @@
+# Created: 04.04.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, List
+from ezdxf.lldxf.const import VERTEXNAMES
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFVertex
+    from .facemixins import FaceProxy
+
+
+class PolyfaceBuilder:
+    def __init__(self, faces: Iterable['FaceProxy'], precision: int = 6):
+        self.precision = precision
+        self.faces = []
+        self.vertices = []
+        self.index_mapping = {}
+        self.build(faces)
+
+    @property
+    def nvertices(self) -> int:
+        return len(self.vertices)
+
+    @property
+    def nfaces(self) -> int:
+        return len(self.faces)
+
+    def get_vertices(self) -> List['DXFVertex']:
+        vertices = self.vertices[:]
+        vertices.extend(self.faces)
+        return vertices
+
+    def build(self, faces: Iterable['FaceProxy']) -> None:
+        for face in faces:
+            face_record = face.face_record
+            for vertex, name in zip(face, VERTEXNAMES):
+                index = self.add(vertex)
+                # preserve sign of old index value
+                sign = -1 if face_record.get_dxf_attrib(name, 0) < 0 else +1
+                face_record.set_dxf_attrib(name, (index + 1) * sign)
+            self.faces.append(face_record)
+
+    def add(self, vertex: 'DXFVertex') -> int:
+        def key(point):
+            return tuple((round(coord, self.precision) for coord in point))
+
+        location = key(vertex.dxf.location)
+        try:
+            return self.index_mapping[location]
+        except KeyError:  # internal exception
+            index = len(self.vertices)
+            self.index_mapping[location] = index
+            self.vertices.append(vertex)
+            return index

+ 335 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/polyline.py

@@ -0,0 +1,335 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, List, cast, Sequence, Union
+from ezdxf.lldxf.const import DXFValueError, DXFIndexError
+from ezdxf.lldxf import const
+
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, XType
+from .facemixins import PolyfaceMixin, PolymeshMixin
+from .trace import QuadrilateralMixin
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, TagValue, Vertex
+
+_POLYLINE_TPL = """0
+POLYLINE
+5
+0
+8
+0
+66
+1
+70
+0
+10
+0.0
+20
+0.0
+30
+0.0
+"""
+
+
+class Polyline(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_POLYLINE_TPL)
+    DXFATTRIBS = make_attribs({
+        'elevation': DXFAttr(10, xtype=XType.any_point),
+        'flags': DXFAttr(70, default=0),
+        'default_start_width': DXFAttr(40, default=0.0),
+        'default_end_width': DXFAttr(41, default=0.0),
+        'm_count': DXFAttr(71, default=0),
+        'n_count': DXFAttr(72, default=0),
+        'm_smooth_density': DXFAttr(73, default=0),
+        'n_smooth_density': DXFAttr(74, default=0),
+        'smooth_type': DXFAttr(75, default=0),
+    })
+    # polyline flags (70)
+    CLOSED = 1
+    MESH_CLOSED_M_DIRECTION = CLOSED
+    CURVE_FIT_VERTICES_ADDED = 2
+    SPLINE_FIT_VERTICES_ADDED = 4
+    POLYLINE_3D = 8
+    POLYMESH = 16
+    MESH_CLOSED_N_DIRECTION = 32
+    POLYFACE = 64
+    GENERATE_LINETYPE_PATTERN = 128
+    # polymesh smooth type (75)
+    NO_SMOOTH = 0
+    QUADRATIC_BSPLINE = 5
+    CUBIC_BSPLINE = 6
+    BEZIER_SURFACE = 8
+    ANY3D = POLYLINE_3D | POLYMESH | POLYFACE
+
+    def post_new_hook(self) -> None:
+        seqend = self._new_entity('SEQEND', {})
+        self.tags.link = seqend.dxf.handle
+
+    def set_dxf_attrib(self, key: str, value: 'TagValue') -> None:
+        super(Polyline, self).set_dxf_attrib(key, value)
+        if key == 'layer':  # if layer of POLYLINE changed, also change layer of VERTEX entities
+            self._set_vertices_layer(value)
+
+    def _set_vertices_layer(self, layer_name: str) -> None:
+        for vertex in self.vertices():
+            vertex.dxf.layer = layer_name
+
+    def get_vertex_flags(self) -> int:
+        return const.VERTEX_FLAGS[self.get_mode()]
+
+    def get_mode(self) -> str:
+        if self.is_3d_polyline:
+            return 'AcDb3dPolyline'
+        elif self.is_polygon_mesh:
+            return 'AcDbPolygonMesh'
+        elif self.is_poly_face_mesh:
+            return 'AcDbPolyFaceMesh'
+        else:
+            return 'AcDb2dPolyline'
+
+    @property
+    def is_2d_polyline(self) -> bool:
+        return self.dxf.flags & self.ANY3D == 0
+
+    @property
+    def is_3d_polyline(self) -> bool:
+        return bool(self.dxf.flags & self.POLYLINE_3D)
+
+    @property
+    def is_polygon_mesh(self) -> bool:
+        return bool(self.dxf.flags & self.POLYMESH)
+
+    @property
+    def is_poly_face_mesh(self) -> bool:
+        return bool(self.dxf.flags & self.POLYFACE)
+
+    @property
+    def is_closed(self) -> bool:
+        return bool(self.dxf.flags & self.CLOSED)
+
+    @property
+    def is_m_closed(self) -> bool:
+        return bool(self.dxf.flags & self.MESH_CLOSED_M_DIRECTION)
+
+    @property
+    def is_n_closed(self) -> bool:
+        return bool(self.dxf.flags & self.MESH_CLOSED_N_DIRECTION)
+
+    def m_close(self) -> None:
+        self.dxf.flags = self.dxf.flags | self.MESH_CLOSED_M_DIRECTION
+
+    def n_close(self) -> None:
+        self.dxf.flags = self.dxf.flags | self.MESH_CLOSED_N_DIRECTION
+
+    def close(self, m_close, n_close=False) -> None:
+        if m_close:
+            self.m_close()
+        if n_close:
+            self.n_close()
+
+    def __len__(self) -> int:
+        count = 0
+        db = self.entitydb
+        tags = db[self.tags.link]
+        while tags.link is not None:
+            count += 1
+            tags = db[tags.link]
+        return count
+
+    def __getitem__(self, pos) -> 'DXFVertex':
+        count = 0
+        db = self.entitydb
+        tags = db[self.tags.link]
+        while tags.link is not None:
+            if count == pos:
+                return self.dxffactory.wrap_entity(tags)  # type: ignore
+            count += 1
+            tags = db[tags.link]
+        raise DXFIndexError("vertex index out of range")
+
+    def vertices(self) -> Iterable['DXFVertex']:
+        return (entity for entity in self.linked_entities() if entity.dxftype() == 'VERTEX')  # type: ignore
+
+    def points(self) -> Iterable['Vertex']:
+        return (vertex.dxf.location for vertex in self.vertices())
+
+    def append_vertices(self, points: Iterable['Vertex'], dxfattribs: dict = None) -> None:
+        dxfattribs = dxfattribs or {}
+        points = list(points)
+        if len(points) > 0:
+            last_vertex = self._get_last_vertex()
+            for new_vertex in self._points_to_dxf_vertices(points, dxfattribs):
+                self._insert_after(last_vertex, new_vertex)
+                last_vertex = new_vertex
+
+    @staticmethod
+    def _insert_after(prev_vertex: 'DXFVertex', new_vertex: 'DXFVertex') -> None:
+        succ = prev_vertex.tags.link
+        prev_vertex.tags.link = new_vertex.dxf.handle
+        new_vertex.tags.link = succ
+
+    def _get_last_vertex(self) -> 'DXFVertex':
+        db = self.entitydb
+        tags = self.tags
+        handle = self.dxf.handle
+        while tags.link is not None:  # while not SEQEND
+            prev_handle = handle
+            handle = tags.link
+            tags = db[handle]
+        return self.dxffactory.wrap_handle(prev_handle)  # type: DXFVertex
+
+    def insert_vertices(self, pos: int, points: Iterable['Vertex'], dxfattribs: dict = None) -> None:
+        """ Insert *points* at position *pos*.
+
+        :param pos: insertion position
+        :param points: list of (x, y, z)-tuples
+        :param dxfattribs: dict of DXF attributes
+
+        """
+        dxfattribs = dxfattribs or {}
+        if pos > 0:
+            insert_vertex = self.__getitem__(pos - 1)
+        else:
+            insert_vertex = self
+        for new_vertex in self._points_to_dxf_vertices(points, dxfattribs):
+            self._insert_after(insert_vertex, new_vertex)
+            insert_vertex = new_vertex
+
+    def _append_vertices(self, vertices: Iterable['DXFVertex']) -> None:
+        """ Append DXFVertex objects.
+
+        :param vertices: iterable of DXFVertex objects
+        """
+        last_vertex = self._get_last_vertex()
+        for vertex in vertices:
+            self._insert_after(last_vertex, vertex)
+            last_vertex = vertex
+
+    def _points_to_dxf_vertices(self, points: Iterable['Vertex'], dxfattribs: dict) -> List['DXFVertex']:
+        """ Converts point (x,y, z)-tuples into DXFVertex objects.
+
+        :param points: list of (x, y,z)-tuples
+        :param dxfattribs: dict of DXF attributes
+        """
+        dxfattribs['flags'] = dxfattribs.get('flags', 0) | self.get_vertex_flags()
+        dxfattribs['layer'] = self.get_dxf_attrib('layer', '0')  # all vertices on the same layer as the POLYLINE entity
+        vertices = []  # type: List[DXFVertex]
+        for point in points:
+            dxfattribs['location'] = point
+            vertices.append(cast('DXFVertex', self._new_entity('VERTEX', dxfattribs)))
+        return vertices
+
+    def delete_vertices(self, pos: int, count=1) -> None:
+        db = self.entitydb
+        prev_vertex = self.__getitem__(pos - 1).tags if pos > 0 else self.tags
+        vertex = db[prev_vertex.link]
+        while vertex.dxftype() == 'VERTEX':
+            db.delete_handle(prev_vertex.link)  # remove from database
+            prev_vertex.link = vertex.link  # remove vertex from list
+            count -= 1
+            if count == 0:
+                return
+            vertex = db[prev_vertex.link]
+        raise DXFValueError("invalid count")
+
+    def _unlink_all_vertices(self) -> None:
+        # but don't delete it from database
+        last_vertex = self._get_last_vertex()
+        self.tags.link = last_vertex.tags.link  # link POLYLINE -> SEQEND
+
+    def cast(self) -> Union['Polyline', 'Polymesh', 'Polyface']:
+        mode = self.get_mode()
+        if mode == 'AcDbPolyFaceMesh':
+            return Polyface.convert(self)
+        elif mode == 'AcDbPolygonMesh':
+            return Polymesh.convert(self)
+        else:
+            return self
+
+    def destroy(self) -> None:
+        db = self.entitydb
+        handle = self.tags.link
+        while handle is not None:
+            tags = db[handle]
+            db.delete_handle(handle)
+            handle = tags.link
+        self.tags.link = None
+
+
+class Polyface(Polyline, PolyfaceMixin):
+    @staticmethod
+    def convert(polyline: Polyline) -> 'Polyface':
+        return Polyface(polyline.tags, polyline.drawing)
+
+
+class Polymesh(Polyline, PolymeshMixin):
+    @staticmethod
+    def convert(polyline: Polyline) -> 'Polymesh':
+        return Polymesh(polyline.tags, polyline.drawing)
+
+
+_VERTEX_TPL = """0
+VERTEX
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+70
+0
+"""
+
+
+class DXFVertex(GraphicEntity, QuadrilateralMixin):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_VERTEX_TPL)
+    DXFATTRIBS = make_attribs({
+        'location': DXFAttr(10, xtype=XType.any_point),
+        'start_width': DXFAttr(40, default=0.),
+        'end_width': DXFAttr(41, default=0.),
+        'bulge': DXFAttr(42, default=0.),
+        'flags': DXFAttr(70, default=0),
+        'tangent': DXFAttr(50),
+        'vtx0': DXFAttr(71),
+        'vtx1': DXFAttr(72),
+        'vtx2': DXFAttr(73),
+        'vtx3': DXFAttr(74),
+    })
+    EXTRA_VERTEX_CREATED = 1  # Extra vertex created by curve-fitting
+    CURVE_FIT_TANGENT = 2  # Curve-fit tangent defined for this vertex.
+    # A curve-fit tangent direction of 0 may be omitted from the DXF output, but is
+    # significant if this bit is set.
+    # 4 = unused, never set in dxf files
+    SPLINE_VERTEX_CREATED = 8  # Spline vertex created by spline-fitting
+    SPLINE_FRAME_CONTROL_POINT = 16
+    POLYLINE_3D_VERTEX = 32
+    POLYGON_MESH_VERTEX = 64
+    POLYFACE_MESH_VERTEX = 128
+    FACE_FLAGS = POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
+    VTX3D = POLYLINE_3D_VERTEX + POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
+
+    @property
+    def is_2d_polyline_vertex(self) -> bool:
+        return self.dxf.flags & self.VTX3D == 0
+
+    @property
+    def is_3d_polyline_vertex(self) -> bool:
+        return self.dxf.flags & self.POLYLINE_3D_VERTEX
+
+    @property
+    def is_polygon_mesh_vertex(self) -> bool:
+        return self.dxf.flags & self.POLYGON_MESH_VERTEX
+
+    @property
+    def is_poly_face_mesh_vertex(self) -> bool:
+        return self.dxf.flags & self.FACE_FLAGS == self.FACE_FLAGS
+
+    @property
+    def is_face_record(self) -> bool:
+        return (self.dxf.flags & self.FACE_FLAGS) == self.POLYFACE_MESH_VERTEX

+ 859 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/tableentries.py

@@ -0,0 +1,859 @@
+# Created: 16.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Sequence, Tuple, Iterable
+from ezdxf.dxfentity import DXFEntity
+from ezdxf.lldxf.tags import DXFTag
+from ezdxf.lldxf import const
+from ezdxf.lldxf.extendedtags import ExtendedTags
+from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
+from ezdxf.lldxf.validator import is_valid_layer_name
+from ezdxf.lldxf.const import DXFInvalidLayerName, DXFValueError, DXFKeyError, DXFVersionError, DIMJUST, DIMTAD
+from ezdxf.render.arrows import ARROWS
+from ezdxf.math.ucs import UCS as UserCoordinateSystem
+import logging
+
+logger = logging.getLogger('ezdxf')
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import DXFFactoryType
+
+_LAYERTEMPLATE = """0
+LAYER
+5
+0
+2
+LAYERNAME
+70
+0
+62
+7
+6
+CONTINUOUS
+"""
+
+
+# noinspection PyAugmentAssignment,PyUnresolvedReferences
+class Layer(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_LAYERTEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'color': DXFAttr(62),  # dxf color index, if < 0 layer is off
+        'linetype': DXFAttr(6),
+    }))
+    FROZEN = 0b00000001
+    THAW = 0b11111110
+    LOCK = 0b00000100
+    UNLOCK = 0b11111011
+
+    def post_new_hook(self) -> None:
+        if not is_valid_layer_name(self.dxf.name):
+            raise DXFInvalidLayerName("Invalid layer name '{}'".format(self.dxf.name))
+
+    def is_frozen(self) -> bool:
+        return self.dxf.flags & Layer.FROZEN > 0
+
+    def freeze(self) -> None:
+        self.dxf.flags = self.dxf.flags | Layer.FROZEN
+
+    def thaw(self) -> None:
+        self.dxf.flags = self.dxf.flags & Layer.THAW
+
+    def is_locked(self) -> bool:
+        return self.dxf.flags & Layer.LOCK > 0
+
+    def lock(self) -> None:
+        self.dxf.flags = self.dxf.flags | Layer.LOCK
+
+    def unlock(self) -> None:
+        self.dxf.flags = self.dxf.flags & Layer.UNLOCK
+
+    def is_off(self) -> bool:
+        return self.dxf.color < 0
+
+    def is_on(self) -> bool:
+        return not self.is_off()
+
+    def on(self) -> None:
+        self.dxf.color = abs(self.dxf.color)
+
+    def off(self) -> None:
+        self.dxf.color = -abs(self.dxf.color)
+
+    def get_color(self) -> int:
+        return abs(self.dxf.color)
+
+    def set_color(self, color: int) -> None:
+        color = abs(color) if self.is_on() else -abs(color)
+        self.dxf.color = color
+
+
+_STYLETEMPLATE = """0
+STYLE
+5
+0
+2
+STYLENAME
+70
+0
+40
+0.0
+41
+1.0
+50
+0.0
+71
+0
+42
+1.0
+3
+OpenSans-Regular.ttf
+4
+
+"""
+
+
+class Style(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_STYLETEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'height': DXFAttr(40),  # fixed height, 0 if not fixed
+        'width': DXFAttr(41),  # width factor
+        'oblique': DXFAttr(50),  # oblique angle in degree, 0 = vertical
+        'generation_flags': DXFAttr(71),  # 2 = backward, 4 = mirrored in Y
+        'last_height': DXFAttr(42),  # last height used
+        'font': DXFAttr(3),  # primary font file name
+        'bigfont': DXFAttr(4),  # big font name, blank if none
+    }))
+
+
+_LTYPETEMPLATE = """0
+LTYPE
+5
+0
+2
+LTYPENAME
+70
+0
+3
+LTYPEDESCRIPTION
+72
+65
+"""
+
+
+class Linetype(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_LTYPETEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'description': DXFAttr(3),
+        'length': DXFAttr(40),
+        'items': DXFAttr(73),
+    }))
+
+    @classmethod
+    def new(cls, handle: str, dxfattribs: dict = None, dxffactory: 'DXFFactoryType' = None) -> DXFEntity:
+        if dxfattribs is not None:
+            pattern = dxfattribs.pop('pattern', [0.0])
+            length = dxfattribs.pop('length', 0.)
+        else:
+            pattern = [0.0]
+            length = 0.
+        entity = super(Linetype, cls).new(handle, dxfattribs, dxffactory)
+        entity._setup_pattern(pattern, length)
+        return entity
+
+    def _setup_pattern(self, pattern: Sequence[float], length: float) -> None:
+        # length parameter is required for complex line types
+        # pattern: [2.0, 1.25, -0.25, 0.25, -0.25] - 1. element is total pattern length
+        # pattern elements: >0 line, <0 gap, =0 point
+        self.tags.noclass.append(DXFTag(73, len(pattern) - 1))
+        self.tags.noclass.append(DXFTag(40, float(pattern[0])))
+        self.tags.noclass.extend((DXFTag(49, float(p)) for p in pattern[1:]))
+
+
+_VPORTTEMPLATE = """0
+VPORT
+5
+0
+2
+VPORTNAME
+70
+0
+10
+0.0
+20
+0.0
+11
+1.0
+21
+1.0
+12
+70.0
+22
+50.0
+13
+0.0
+23
+0.0
+14
+0.5
+24
+0.5
+15
+0.5
+25
+0.5
+16
+0.0
+26
+0.0
+36
+1.0
+17
+0.0
+27
+0.0
+37
+0.0
+40
+70.
+41
+1.34
+42
+50.0
+43
+0.0
+44
+0.0
+50
+0.0
+51
+0.0
+71
+0
+72
+1000
+73
+1
+74
+3
+75
+0
+76
+0
+77
+0
+78
+0
+"""
+
+
+class VPort(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_VPORTTEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'lower_left': DXFAttr(10, xtype=XType.point2d),
+        'upper_right': DXFAttr(11, xtype=XType.point2d),
+        'center_point': DXFAttr(12, xtype=XType.point2d),
+        'snap_base': DXFAttr(13, xtype=XType.point2d),
+        'snap_spacing': DXFAttr(14, xtype=XType.point2d),
+        'grid_spacing': DXFAttr(15, xtype=XType.point2d),
+        'direction_point': DXFAttr(16, xtype=XType.point3d),
+        'target_point': DXFAttr(17, xtype=XType.point3d),
+        'height': DXFAttr(40),
+        'aspect_ratio': DXFAttr(41),
+        'lens_length': DXFAttr(42),
+        'front_clipping': DXFAttr(43),
+        'back_clipping': DXFAttr(44),
+        'snap_rotation': DXFAttr(50),
+        'view_twist': DXFAttr(51),
+        'status': DXFAttr(68),
+        # group code 69: 'id' is never saved in DXF, and DXF R13 and later has no group code 69
+        'view_mode': DXFAttr(71),
+        'circle_zoom': DXFAttr(72),
+        'fast_zoom': DXFAttr(73),
+        'ucs_icon': DXFAttr(74),
+        'snap_on': DXFAttr(75),
+        'grid_on': DXFAttr(76),
+        'snap_style': DXFAttr(77),
+        'snap_isopair': DXFAttr(78),
+    }))
+
+
+_UCSTEMPLATE = """0
+UCS
+5
+0
+2
+UCSNAME
+70
+0
+10
+0.0
+20
+0.0
+30
+0.0
+11
+1.0
+21
+0.0
+31
+0.0
+12
+0.0
+22
+1.0
+32
+0.0
+"""
+
+
+class UCS(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_UCSTEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'origin': DXFAttr(10, xtype=XType.point3d),
+        'xaxis': DXFAttr(11, xtype=XType.point3d),
+        'yaxis': DXFAttr(12, xtype=XType.point3d),
+    }))
+
+    def ucs(self) -> UserCoordinateSystem:
+        return UserCoordinateSystem(
+            origin=self.get_dxf_attrib('origin', default=(0, 0, 0)),
+            ux=self.get_dxf_attrib('xaxis', default=(1, 0, 0)),
+            uy=self.get_dxf_attrib('yaxis', default=(0, 1, 0)),
+        )
+
+
+_APPIDTEMPLATE = """0
+APPID
+5
+0
+2
+APPNAME
+70
+0
+"""
+
+
+class AppID(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_APPIDTEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+    }))
+
+
+_VIEWTEMPLATE = """0
+VIEW
+5
+0
+2
+VIEWNAME
+70
+0
+10
+0.0
+20
+0.0
+11
+1.0
+21
+1.0
+31
+1.0
+12
+0.0
+22
+0.0
+32
+0.0
+40
+70.
+41
+1.0
+42
+50.0
+43
+0.0
+44
+0.0
+50
+0.0
+71
+0
+"""
+
+
+class View(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_VIEWTEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(5),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'height': DXFAttr(40),
+        'width': DXFAttr(41),
+        'center_point': DXFAttr(10, xtype=XType.point2d),
+        'direction_point': DXFAttr(11, xtype=XType.point3d),
+        'target_point': DXFAttr(12, xtype=XType.point3d),
+        'lens_length': DXFAttr(42),
+        'front_clipping': DXFAttr(43),
+        'back_clipping': DXFAttr(44),
+        'view_twist': DXFAttr(50),
+        'view_mode': DXFAttr(71),
+    }))
+
+
+_DIMSTYLETEMPLATE = """0
+DIMSTYLE
+105
+0
+2
+DIMSTYLENAME
+70
+0
+3
+
+4
+
+5
+
+6
+
+7
+
+40
+1.0
+41
+1.0
+42
+0.0
+43
+3.75
+44
+0.0
+45
+0.0
+46
+0.0
+47
+0.0
+48
+0.0
+140
+2.5
+141
+2.5
+142
+0.0
+143
+25.4
+144
+1.0
+145
+0.0
+146
+1.0
+147
+0.625
+71
+0
+72
+0
+73
+1
+74
+1
+75
+0
+76
+0
+77
+0
+78
+0
+170
+0
+171
+2
+172
+0
+173
+0
+174
+0
+175
+0
+176
+0
+177
+0
+178
+0
+"""
+
+
+def dim_filter(name: str) -> bool:
+    return name.startswith('dim')
+
+
+class DimStyle(DXFEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_DIMSTYLETEMPLATE)
+    DXFATTRIBS = DXFAttributes(DefSubclass(None, {
+        'handle': DXFAttr(105),
+        'name': DXFAttr(2),
+        'flags': DXFAttr(70),
+        'dimpost': DXFAttr(3),
+        'dimapost': DXFAttr(4),
+        'dimblk': DXFAttr(5),
+        'dimblk1': DXFAttr(6),
+        'dimblk2': DXFAttr(7),
+        'dimscale': DXFAttr(40),
+        'dimasz': DXFAttr(41),
+        'dimexo': DXFAttr(42),
+        'dimdli': DXFAttr(43),
+        'dimexe': DXFAttr(44),
+        'dimrnd': DXFAttr(45),
+        'dimdle': DXFAttr(46),
+        'dimtp': DXFAttr(47),
+        'dimtm': DXFAttr(48),
+        'dimtxt': DXFAttr(140),
+        'dimcen': DXFAttr(141),
+        'dimtsz': DXFAttr(142),
+        'dimaltf': DXFAttr(143, default=1.),
+        'dimlfac': DXFAttr(144),
+        'dimtvp': DXFAttr(145),
+        'dimtfac': DXFAttr(146),
+        'dimgap': DXFAttr(147),
+        'dimtol': DXFAttr(71),
+        'dimlim': DXFAttr(72),
+        'dimtih': DXFAttr(73),
+        'dimtoh': DXFAttr(74),
+        'dimse1': DXFAttr(75),
+        'dimse2': DXFAttr(76),
+        'dimtad': DXFAttr(77),  # 0 center, 1 above, 4 below dimline
+        'dimzin': DXFAttr(78),
+        'dimalt': DXFAttr(170),
+        'dimaltd': DXFAttr(171),
+        'dimtofl': DXFAttr(172),
+        'dimsah': DXFAttr(173),
+        'dimtix': DXFAttr(174),
+        'dimsoxd': DXFAttr(175),
+        'dimclrd': DXFAttr(176),
+        'dimclre': DXFAttr(177),
+        'dimclrt': DXFAttr(178),
+    }))
+    CODE_TO_DXF_ATTRIB = dict(DXFATTRIBS.build_group_code_items(dim_filter))
+
+    def dim_attribs(self) -> Iterable[Tuple[str, DXFAttr]]:
+        return ((name, attrib) for name, attrib in self.DXFATTRIBS.items() if name.startswith('dim'))
+
+    def print_dim_attribs(self) -> None:
+        for name, attrib in self.dim_attribs():
+            code = attrib.code
+            value = self.get_dxf_attrib(name, None)
+            if value is not None:
+                print("{name} ({code}) = {value}".format(name=name, value=value, code=code))
+
+    def copy_to_header(self, dwg):
+        attribs = self.dxfattribs()
+        header = dwg.header
+        header['$DIMSTYLE'] = self.dxf.name
+        for name, value in attribs.items():
+            if name.startswith('dim'):
+                header_var = '$' + name.upper()
+                try:
+                    header[header_var] = value
+                except DXFKeyError:
+                    logger.debug('Unsupported header variable: {}.'.format(header_var))
+
+    def set_arrows(self, blk: str = '', blk1: str = '', blk2: str = '') -> None:
+        """
+        Set arrows by block names or AutoCAD standard arrow names, set dimtsz = 0 which disables tick.
+
+        Args:
+            blk: block/arrow name for both arrows, if dimsah == 0
+            blk1: block/arrow name for first arrow, if dimsah == 1
+            blk2: block/arrow name for second arrow, if dimsah == 1
+
+        """
+        self.set_dxf_attrib('dimblk', blk)
+        self.set_dxf_attrib('dimblk1', blk1)
+        self.set_dxf_attrib('dimblk2', blk2)
+        self.set_dxf_attrib('dimtsz', 0)  # use blocks
+
+        # only existing BLOCK definitions allowed
+        if self.drawing:
+            blocks = self.drawing.blocks
+            for b in (blk, blk1, blk2):
+                if ARROWS.is_acad_arrow(b):  # not real blocks
+                    continue
+                if b and b not in blocks:
+                    raise DXFValueError('BLOCK "{}" does not exist.'.format(blk))
+
+    def set_tick(self, size: float = 1) -> None:
+        """
+        Use oblique stroke as tick, disables arrows.
+
+        Args:
+            size: arrow size in daring units
+
+        """
+        self.set_dxf_attrib('dimtsz', size)
+
+    def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None:
+        """
+        Set measurement text alignment, `halign` defines the horizontal alignment (requires DXFR2000+),
+        `valign` defines the vertical  alignment, `above1` and `above2` means above extension line 1 or 2 and aligned
+        with extension line.
+
+        Args:
+            halign: `left`, `right`, `center`, `above1`, `above2`, requires DXF R2000+
+            valign: `above`, `center`, `below`
+            vshift: vertical text shift, if `valign` is `center`; >0 shift upward, <0 shift downwards
+
+        """
+        if valign:
+            valign = valign.lower()
+            self.set_dxf_attrib('dimtad', DIMTAD[valign])
+            if valign == 'center' and vshift is not None:
+                self.set_dxf_attrib('dimtvp', vshift)
+        try:
+            if halign:
+                self.set_dxf_attrib('dimjust', DIMJUST[halign.lower()])
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMJUST require DXF R2000+')
+
+    def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None,
+                        leading_zeros: bool = True, trailing_zeros: bool = True):
+        """
+        Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places.
+
+        Args:
+            prefix: Dimension text prefix text as string
+            postfix: Dimension text postfix text as string
+            rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to 0.25, all
+                 distances round to the nearest 0.25 unit. If you set DIMRND to 1.0, all distances round to the nearest
+                 integer.
+            dec: Sets the number of decimal places displayed for the primary units of a dimension. requires DXF R2000+
+            sep: "." or "," as decimal separator requires DXF R2000+
+            leading_zeros: suppress leading zeros for decimal dimensions if False
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False
+
+        """
+        if prefix or postfix:
+            self.dxf.dimpost = prefix + '<>' + postfix
+        if rnd is not None:
+            self.dxf.dimrnd = rnd
+
+        # works only with decimal dimensions not inch and feet, US user set dimzin directly
+        if leading_zeros is not None or trailing_zeros is not None:
+            dimzin = 0
+            if leading_zeros is False:
+                dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+            if trailing_zeros is False:
+                dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+            self.dxf.dimzin = dimzin
+        try:
+            if dec is not None:
+                self.dxf.dimdec = dec
+            if sep is not None:
+                self.dxf.dimdsep = ord(sep)
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMDSEP and DIMDEC require DXF R2000+')
+
+    def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None,
+                           extension: float = None, disable1: bool = None, disable2: bool = None):
+        """
+        Set dimension line properties
+
+        Args:
+            color: color index
+            linetype: linetype as string, requires DXF R2007+
+            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm, requires DXF R2000+
+            extension: extension length
+            disable1: True to suppress first part of dimension line, requires DXF R2000+
+            disable2: True to suppress second part of dimension line, requires DXF R2000+
+
+        """
+        if color is not None:
+            self.dxf.dimclrd = color
+        if extension is not None:
+            self.dxf.dimdle = extension
+        try:
+            if lineweight is not None:
+                self.dxf.dimlwd = lineweight
+            if disable1 is not None:
+                self.dxf.dimsd1 = disable1
+            if disable2 is not None:
+                self.dxf.dimsd2 = disable2
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMLWD, DIMSD1 and DIMSD2 requires DXF R2000+')
+        try:
+            if linetype is not None:
+                self.dxf.dimltype = linetype
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMLTYPE requires DXF R2007+')
+
+    def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None,
+                           offset: float = None, fixed_length: float = None):
+        """
+        Set common extension line attributes.
+
+        Args:
+            color: color index
+            lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
+            extension: extension length above dimension line
+            offset: offset from measurement point
+            fixed_length: set fixed length extension line, length below the dimension line
+
+        """
+        if color is not None:
+            self.dxf.dimclre = color
+        if extension is not None:
+            self.dxf.dimexe = extension
+        if offset is not None:
+            self.dxf.dimexo = offset
+        try:
+            if lineweight is not None:
+                self.dxf.dimlwe = lineweight
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMLWE requires DXF R2000+')
+        try:
+            if fixed_length is not None:
+                self.dxf.dimfxlon = 1
+                self.dxf.dimfxl = fixed_length
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMFXL requires DXF R2007+')
+
+    def set_extline1(self, linetype: str = None, disable=False):
+        """
+        Set extension line 1 attributes.
+
+        Args:
+            linetype: linetype for extension line 1, requires DXF R2007+
+            disable: disable extension line 1 if True
+
+        """
+        if disable:
+            self.dxf.dimse1 = 1
+        try:
+            if linetype is not None:
+                self.dxf.dimltex1 = linetype
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMLTEX1 requires DXF R2007+')
+
+    def set_extline2(self, linetype: str = None, disable=False):
+        """
+        Set extension line 2 attributes.
+
+        Args:
+            linetype: linetype for extension line 2, requires DXF R2007+
+            disable: disable extension line 2 if True
+
+        """
+        if disable:
+            self.dxf.dimse2 = 1
+        try:
+            if linetype is not None:
+                self.dxf.dimltex2 = linetype
+        except const.DXFAttributeError:
+            raise DXFVersionError('DIMLTEX2 requires DXF R2007+')
+
+    def set_tolerance(self, upper: float, lower: float = None, hfactor: float = 1.0,
+                      align: str = None, dec: int = None, leading_zeros: bool = None,
+                      trailing_zeros: bool = None) -> None:
+        """
+        Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and
+        trailing zero suppression.
+
+        Args:
+            upper: upper tolerance value
+            lower: lower tolerance value, if None same as upper
+            hfactor: tolerance text height factor in relation to the dimension text height
+            align: tolerance text alignment "TOP", "MIDDLE", "BOTTOM", required DXF R2000+
+            dec: Sets the number of decimal places displayed, required DXF R2000+
+            leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+
+
+        """
+        # exclusive tolerances
+        self.dxf.dimtol = 1
+        self.dxf.dimlim = 0
+        self.dxf.dimtp = float(upper)
+        if lower is not None:
+            self.dxf.dimtm = float(lower)
+        else:
+            self.dxf.dimtm = float(upper)
+        if hfactor is not None:
+            self.dxf.dimtfac = float(hfactor)
+
+        if self.dxfversion > 'AC1009':
+            # works only with decimal dimensions not inch and feet, US user set dimzin directly
+            if leading_zeros is not None or trailing_zeros is not None:
+                dimtzin = 0
+                if leading_zeros is False:
+                    dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+                if trailing_zeros is False:
+                    dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+                self.dxf.dimtzin = dimtzin
+
+            if align is not None:
+                self.dxf.dimtolj = const.MTEXT_INLINE_ALIGN[align.upper()]
+            if dec is not None:
+                self.dxf.dimtdec = int(dec)
+
+    def set_limits(self, upper: float, lower: float, hfactor: float = 1.0,
+                   dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None:
+        """
+        Set limits text format, upper and lower limit values, text height factor, number of decimal places or
+        leading and trailing zero suppression.
+
+        Args:
+            upper: upper limit value added to measurement value
+            lower: lower lower value subtracted from measurement value
+            hfactor: limit text height factor in relation to the dimension text height
+            dec: Sets the number of decimal places displayed, required DXF R2000+
+            leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+
+            trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+
+
+        """
+        # exclusive limits
+        self.dxf.dimlim = 1
+        self.dxf.dimtol = 0
+        self.dxf.dimtp = float(upper)
+        self.dxf.dimtm = float(lower)
+        self.dxf.dimtfac = float(hfactor)
+
+        if self.dxfversion > 'AC1009':
+            # works only with decimal dimensions not inch and feet, US user set dimzin directly
+            if leading_zeros is not None or trailing_zeros is not None:
+                dimtzin = 0
+                if leading_zeros is False:
+                    dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
+                if trailing_zeros is False:
+                    dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
+                self.dxf.dimtzin = dimtzin
+            self.dxf.dimtolj = 0  # set bottom as default
+            if dec is not None:
+                self.dxf.dimtdec = int(dec)

+ 119 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/text.py

@@ -0,0 +1,119 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Tuple, Union
+from ezdxf.lldxf.const import DXFValueError
+from ezdxf.lldxf import const
+
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, XType
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+_TEXT_TPL = """0
+TEXT
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+1
+TEXTCONTENT
+50
+0.0
+51
+0.0
+7
+STANDARD
+41
+1.0
+71
+0
+72
+0
+73
+0
+11
+0.0
+21
+0.0
+31
+0.0
+"""
+
+
+class Text(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_TEXT_TPL)
+    DXFATTRIBS = make_attribs({
+        'insert': DXFAttr(10, xtype=XType.any_point),
+        'height': DXFAttr(40),
+        'text': DXFAttr(1),
+        'rotation': DXFAttr(50, default=0.0),  # in degrees (circle = 360deg)
+        'oblique': DXFAttr(51, default=0.0),  # in degrees, vertical = 0deg
+        'style': DXFAttr(7, default='STANDARD'),  # text style
+        'width': DXFAttr(41, default=1.0),  # width FACTOR!
+        'text_generation_flag': DXFAttr(71, default=0),  # 2 = backward (mirr-x), 4 = upside down (mirr-y)
+        'halign': DXFAttr(72, default=0),  # horizontal justification
+        'valign': DXFAttr(73, default=0),  # vertical justification
+        'align_point': DXFAttr(11, xtype=XType.any_point),
+    })
+    # horizontal align values
+    LEFT = 0
+    CENTER = 1
+    RIGHT = 2
+    # vertical align values
+    BASELINE = 0
+    BOTTOM = 1
+    MIDDLE = 2
+    TOP = 3
+    # text generation flags
+    MIRROR_X = 2
+    MIRROR_Y = 4
+    BACKWARD = MIRROR_X
+    UPSIDE_DOWN = MIRROR_Y
+
+    def set_pos(self, p1: 'Vertex', p2: 'Vertex' = None, align: str = None) -> 'Text':
+        if align is None:
+            align = self.get_align()
+        align = align.upper()
+        self.set_align(align)
+        self.set_dxf_attrib('insert', p1)
+        if align in ('ALIGNED', 'FIT'):
+            if p2 is None:
+                raise DXFValueError("Alignment '{}' requires a second alignment point.".format(align))
+        else:
+            p2 = p1
+        self.set_dxf_attrib('align_point', p2)
+        return self
+
+    def get_pos(self) -> Tuple[str, 'Vertex', Union['Vertex', None]]:
+        p1 = self.dxf.insert
+        p2 = self.get_dxf_attrib('align_point', (0., 0., 0.))
+        align = self.get_align()
+        if align == 'LEFT':
+            return align, p1, None
+        if align in ('FIT', 'ALIGN'):
+            return align, p1, p2
+        return align, p2, None
+
+    def set_align(self, align: str='LEFT') -> 'Text':
+        align = align.upper()
+        halign, valign = const.TEXT_ALIGN_FLAGS[align]
+        self.set_dxf_attrib('halign', halign)
+        self.set_dxf_attrib('valign', valign)
+        return self
+
+    def get_align(self) -> str:
+        halign = self.get_dxf_attrib('halign', 0)
+        valign = self.get_dxf_attrib('valign', 0)
+        if halign > 2:
+            valign = 0
+        return const.TEXT_ALIGNMENT_BY_FLAGS.get((halign, valign), 'LEFT')

+ 78 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/trace.py

@@ -0,0 +1,78 @@
+# Created: 25.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import cast
+from ezdxf.lldxf.const import VERTEXNAMES
+
+from .graphics import GraphicEntity, ExtendedTags, make_attribs, DXFAttr, XType
+
+
+class QuadrilateralMixin:
+    __slots__ = ()
+
+    def __getitem__(self, num):
+        return cast(GraphicEntity, self).get_dxf_attrib(VERTEXNAMES[num])
+
+    def __setitem__(self, num, value):
+        return cast(GraphicEntity, self).set_dxf_attrib(VERTEXNAMES[num], value)
+
+
+_TRACE_TPL = """0
+TRACE
+5
+0
+8
+0
+10
+0.0
+20
+0.0
+30
+0.0
+11
+0.0
+21
+0.0
+31
+0.0
+12
+0.0
+22
+0.0
+32
+0.0
+13
+0.0
+23
+0.0
+33
+0.0
+"""
+
+
+class Trace(GraphicEntity, QuadrilateralMixin):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_TRACE_TPL)
+    DXFATTRIBS = make_attribs({
+        'vtx0': DXFAttr(10, xtype=XType.any_point),
+        'vtx1': DXFAttr(11, xtype=XType.any_point),
+        'vtx2': DXFAttr(12, xtype=XType.any_point),
+        'vtx3': DXFAttr(13, xtype=XType.any_point),
+    })
+
+
+class Solid(Trace):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_TRACE_TPL.replace('TRACE', 'SOLID'))
+
+
+class Face(Trace):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_TRACE_TPL.replace('TRACE', '3DFACE'))
+    DXFATTRIBS = make_attribs({
+        'vtx0': DXFAttr(10, xtype=XType.point3d),
+        'vtx1': DXFAttr(11, xtype=XType.point3d),
+        'vtx2': DXFAttr(12, xtype=XType.point3d),
+        'vtx3': DXFAttr(13, xtype=XType.point3d),
+        'invisible_edge': DXFAttr(70, default=0),
+    })

+ 257 - 0
venv/lib/python3.7/site-packages/ezdxf/legacy/viewport.py

@@ -0,0 +1,257 @@
+# Created: 10.10.2015
+# Copyright (c) 2015-2018, Manfred Moitzi
+# License: MIT License
+from contextlib import contextmanager
+
+from ezdxf.lldxf.extendedtags import ExtendedTags
+from ezdxf.lldxf.attributes import DXFAttr, XType
+from ezdxf.lldxf.tags import DXFTag, Tags
+from ezdxf.lldxf.const import DXFStructureError, DXFValueError
+
+from .graphics import make_attribs, GraphicEntity
+
+_VPORT_TPL = """0
+VIEWPORT
+5
+0
+10
+0.0
+20
+0.0
+30
+0.0
+40
+1.0
+41
+1.0
+68
+1
+1001
+ACAD
+1000
+MVIEW
+1002
+{
+1070
+16
+1010
+0.0
+1020
+0.0
+1030
+0.0
+1010
+0.0
+1020
+0.0
+1030
+0.0
+1040
+0.0
+1040
+1.0
+1040
+0.0
+1040
+0.0
+1040
+50.0
+1040
+0.0
+1040
+0.0
+1070
+0
+1070
+100
+1070
+1
+1070
+3
+1070
+0
+1070
+0
+1070
+0
+1070
+0
+1040
+0.0
+1040
+0.0
+1040
+0.0
+1040
+0.1
+1040
+0.1
+1040
+0.1
+1040
+0.1
+1070
+0
+1002
+{
+1002
+}
+1002
+}
+"""
+
+
+class Viewport(GraphicEntity):
+    __slots__ = ()
+    TEMPLATE = ExtendedTags.from_text(_VPORT_TPL)
+    DXFATTRIBS = make_attribs({
+        'center': DXFAttr(10, xtype=XType.any_point),  # center point of entity in paper space coordinates)
+        'width': DXFAttr(40),  # width in paper space units
+        'height': DXFAttr(41),  # height in paper space units
+        'status': DXFAttr(68),
+        'id': DXFAttr(69),
+    })
+
+    viewport_id = 2  # notes to id:
+    # The id of the first viewport has to be 1, which is the definition of
+    # paper space. For the following viewports it seems only important, that
+    # the id is greater than 1.
+
+    @contextmanager
+    def edit_data(self) -> 'ViewportData':
+        viewport_data = self.get_viewport_data()
+        yield viewport_data
+        self.set_viewport_data(viewport_data)
+
+    def get_viewport_data(self) -> 'ViewportData':
+        try:
+            extended_dxf_data = self.tags.get_xdata('ACAD')
+        except DXFValueError:
+            DXFStructureError("Invalid viewport entity - missing data")
+        else:
+            return ViewportData.from_tags(extended_dxf_data)
+
+    def set_viewport_data(self, viewport_data: 'ViewportData') -> None:
+        dxftags = viewport_data.dxftags()
+        pos = None
+        for index, xdata in enumerate(self.tags.xdata):
+            if xdata[0].value == 'ACAD' and xdata[1].value == 'MVIEW':
+                pos = index
+        if pos is None:
+            self.tags.xdata.insert(0, dxftags)  # insert viewport data as first extended data
+        else:
+            self.tags.xdata[pos] = dxftags
+
+    def get_next_viewport_id(self) -> int:
+        current_id = Viewport.viewport_id
+        Viewport.viewport_id += 1
+        return current_id
+
+
+class ViewportData:
+    """ Helper class for Viewport().
+
+    This class defines the extended dxf tags, which can not be treated as DXFAttr()
+    like the 'ordinary' dxf tags, because:
+        - tags defined as extended DXF codes
+        - the group codes of this tags are not unique
+        - this tags must occur in a particular order, the order of their appearing,
+          defines their meaning.
+
+    """
+
+    def __init__(self):
+        # view_target_point & view_direction_vector defines the view direction
+        # only important for 3D model views
+        self.view_target_point = (0., 0., 0.)
+        self.view_direction_vector = (0., 0., 1.)
+        self.view_twist_angle = 0.  # in radians!!!
+        self.view_height = 1.  # height of model space area
+        self.view_center_point = (0., 0.)  # point in model space, which is displayed in the viewport center.
+        self.perspective_lens_length = 50.
+        self.front_clip_plane_z_value = 0.
+        self.back_clip_plane_z_value = 0.
+        self.view_mode = 0
+        self.circle_zoom = 100
+        self.fast_zoom = 1
+        self.ucs_icon = 3
+        self.snap = 0
+        self.grid = 0
+        self.snap_style = 0
+        self.snap_isopair = 0
+        self.snap_angle = 0.
+        self.snap_base_point = (0., 0.)
+        self.snap_spacing = (0.1, 0.1)
+        self.grid_spacing = (0.1, 0.1)
+        self.hidden_plot = 0
+        self.frozen_layers = []  # add layer names as strings
+
+    def dxftags(self) -> Tags:
+        tags = [
+            DXFTag(1001, 'ACAD'),
+            DXFTag(1000, 'MVIEW'),
+            DXFTag(1002, '{', ),
+            DXFTag(1070, 16),  # extended data version, always 16 for R11/12
+            DXFTag(1010, self.view_target_point),
+            DXFTag(1010, self.view_direction_vector),
+            DXFTag(1040, self.view_twist_angle),
+            DXFTag(1040, self.view_height),
+            DXFTag(1040, self.view_center_point[0]),
+            DXFTag(1040, self.view_center_point[1],),
+            DXFTag(1040, self.perspective_lens_length),
+            DXFTag(1040, self.front_clip_plane_z_value),
+            DXFTag(1040, self.back_clip_plane_z_value),
+            DXFTag(1070, self.view_mode),
+            DXFTag(1070, self.circle_zoom),
+            DXFTag(1070, self.fast_zoom),
+            DXFTag(1070, self.ucs_icon),
+            DXFTag(1070, self.snap),
+            DXFTag(1070, self.grid),
+            DXFTag(1070, self.snap_style),
+            DXFTag(1070, self.snap_isopair),
+            DXFTag(1040, self.snap_angle),
+            DXFTag(1040, self.snap_base_point[0]),
+            DXFTag(1040, self.snap_base_point[1]),
+            DXFTag(1040, self.snap_spacing[0]),
+            DXFTag(1040, self.snap_spacing[1]),
+            DXFTag(1040, self.grid_spacing[0]),
+            DXFTag(1040, self.grid_spacing[1]),
+            DXFTag(1070, self.hidden_plot),
+            DXFTag(1002, '{'),  # start frozen layer list
+        ]
+        tags.extend(DXFTag(1003, layer_name) for layer_name in self.frozen_layers)
+        tags.extend([
+            DXFTag(1002, '}'),  # end of frozen layer list
+            DXFTag(1002, '}'),  # end of viewport data
+        ])
+        return Tags(tags)
+
+    @classmethod
+    def from_tags(cls, tags: Tags) -> 'ViewportData':
+        vp_data = cls()
+        try:
+            vp_data.view_target_point = tags[4].value
+            vp_data.view_direction_vector = tags[5].value
+            vp_data.view_twist_angle = tags[6].value
+            vp_data.view_height = tags[7].value
+            vp_data.view_center_point = tags[8].value, tags[9].value
+            vp_data.perspective_lens_length = tags[10].value
+            vp_data.front_clip_plane_z_value = tags[11].value
+            vp_data.back_clip_plane_z_value = tags[12].value
+            vp_data.view_mode = tags[13].value
+            vp_data.circle_zoom = tags[14].value
+            vp_data.fast_zoom = tags[15].value
+            vp_data.ucs_icon = tags[16].value
+            vp_data.snap = tags[17].value
+            vp_data.grid = tags[18].value
+            vp_data.snap_style = tags[19].value
+            vp_data.snap_isopair = tags[20].value
+            vp_data.snap_angle = tags[21].value
+            vp_data.snap_base_point = tags[22].value, tags[23].value
+            vp_data.snap_spacing = tags[24].value, tags[25].value
+            vp_data.grid_spacing = tags[26].value, tags[27].value
+            vp_data.hidden_plot = tags[28].value
+        except IndexError:  # internal exception
+            raise DXFStructureError("Invalid viewport entity - missing data")
+        vp_data.frozen_layers = [frozen_layer_name.value for frozen_layer_name in tags[30:-2]]
+        return vp_data

+ 4 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/__init__.py

@@ -0,0 +1,4 @@
+# Purpose: low level DXF modules
+# Created: 16.07.2015
+# Copyright (C) 2015, Manfred Moitzi
+# License: MIT License

+ 249 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/attributes.py

@@ -0,0 +1,249 @@
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from collections import namedtuple
+from enum import Enum
+from typing import Any, Tuple, Iterable, List, Dict, Union, ItemsView, KeysView, TYPE_CHECKING
+
+from .const import DXFAttributeError, DXFValueError, DXFInternalEzdxfError, DXFStructureError
+from .types import dxftag, DXFVertex
+from .tags import Tags
+from .extendedtags import ExtendedTags
+
+if TYPE_CHECKING:  # import forward declarations
+    from ezdxf.eztypes import DXFEntity, TagValue
+
+DefSubclass = namedtuple('DefSubclass', 'name attribs')
+VIRTUAL_TAG = -666
+
+
+class XType(Enum):
+    """ Extended Attribute Types
+    """
+    point2d = 1  # 2D points only
+    point3d = 2  # 3D points only
+    any_point = 3  # 2D or 3D points
+    callback = 4  #
+
+
+class DXFAttr:
+    """
+    Defines a DXF attribute, accessible by DXFEntity.dxf.name.
+
+    Extended Attribute Types
+    ------------------------
+
+    - XType.point2d:  2D points only
+    - XType.point3d:  3D point only
+    - XType.any_point:  mixed 2D/3D point
+    - XType.callback: Calls get_value(entity) to get the value of DXF attribute 'name', and calls
+      set_value(entity, value) to set value of DXF attribute 'name'.
+
+    For example definitions see TestDXFEntity.test_callback() in file test_dxfentity.py
+
+    """
+
+    def __init__(self,
+                 code: int,
+                 subclass: int = 0,
+                 xtype: XType = None,
+                 default=None,
+                 dxfversion: str = None,
+                 getter: str = None,  # name of getter method
+                 setter: str = None,  # name of setter method
+                 ):
+        self.name = ''  # type: str  # set by DXFAttributes._add_subclass_attribs()
+        self.code = code  # DXF group code
+        self.subclass = subclass  # subclass index
+        self.xtype = xtype  # Point2D, Point3D, Point2D/3D, Callback
+        self.default = default  # type: TagValue # DXF default value
+
+        # If dxfversion is None - this attribute is valid for all supported DXF versions, set dxfversion to a specific
+        # DXF version like 'AC1018' and this attribute can only be set by DXF version 'AC1018' or later.
+        self.dxfversion = dxfversion
+        self.getter = getter  # DXF entity getter method name for callback attributes
+        self.setter = setter  # DXF entity setter method name for callback attributes
+
+    def get_callback_value(self, entity: 'DXFEntity') -> 'TagValue':
+        """
+        Executes a callback function in 'entity' to get a DXF value.
+
+        Callback function is defined by self.getter as string.
+
+        Args:
+            entity: DXF entity
+
+        Returns: DXF attribute value
+        """
+        try:
+            return getattr(entity, self.getter)()
+        except AttributeError:
+            raise DXFAttributeError('DXF attribute {}: invalid getter {}.'.format(self.name, self.getter))
+        except TypeError:  # None
+            DXFAttributeError('DXF attribute {} has no getter.'.format(self.name))
+
+    def set_callback_value(self, entity: 'DXFEntity', value: 'TagValue') -> None:
+        """
+        Executes a callback function in 'entity' to set a DXF value.
+
+        Callback function is defined by self.setter as string.
+
+        Args:
+            entity: DXF entity
+            value: DXF attribute value
+
+        """
+        try:
+            getattr(entity, self.setter)(value)
+        except AttributeError:
+            raise DXFAttributeError('DXF attribute {}: invalid setter {}.'.format(self.name, self.setter))
+        except TypeError:  # None
+            raise DXFAttributeError('DXF attribute {} has no setter.'.format(self.name))
+
+    def get_attrib(self, entity: 'DXFEntity', key: str, default: Any = DXFValueError) -> 'TagValue':
+        """
+        Return value of DXF attribute 'key'.
+
+        Args:
+            entity: DXF entity
+            key: attribute name
+            default: default value or DXFValueError for raising an exception if attribute does not exist
+
+        Returns: value of DXF attribute
+
+        """
+        if self.xtype is XType.callback:
+            return self.get_callback_value(entity)
+        try:  # No check if attribute is valid for DXF version of drawing, if it is there you get it
+            return self._get_dxf_attrib(entity.tags)
+        except DXFValueError:
+            if default is DXFValueError:
+                # no DXF default values if DXF version is incorrect
+                if self.dxfversion is not None and entity.drawing.dxfversion < self.dxfversion:
+                    msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'."
+                    raise DXFValueError(msg.format(key, entity.drawing.dxfversion, self.dxfversion))
+                result = self.default  # default value defined by DXF specs
+                if result is not None:
+                    return result
+                else:
+                    raise DXFValueError("DXFAttrib '%s' does not exist." % key)
+            else:
+                return default
+
+    def _get_dxf_attrib(self, tags: ExtendedTags) -> 'TagValue':
+        subclass_tags = self._get_dxf_attrib_subclass_tags(tags, self.subclass)
+        if self.xtype is not None:
+            return self._get_extended_type(subclass_tags)
+        else:
+            return subclass_tags.get_first_value(self.code)
+
+    def _get_extended_type(self, tags: Tags) -> Tuple[float, ...]:
+        value = tags.get_first_value(self.code)
+        if len(value) == 3:
+            if self.xtype is XType.point2d:
+                raise DXFStructureError("expected 2D point but found 3D point")
+        elif self.xtype is XType.point3d:  # len(value) == 2
+            raise DXFStructureError("expected 3D point but found 2D point")
+        return value
+
+    def _get_dxf_attrib_subclass_tags(self, tags: ExtendedTags, subclass_key: Union[int, str]) -> Tags:
+        try:  # fast access subclass by index as int, no subclass is subclass index 0
+            return tags.subclasses[subclass_key]
+        except IndexError:
+            raise DXFInternalEzdxfError('Subclass index error in {entity} subclass={index}.'.format(
+                entity=str(self),
+                index=subclass_key,
+            ))
+        except TypeError:  # slow access subclass by name as string
+            # raises DXFKeyError if subclass does not exist
+            return tags.get_subclass(subclass_key)
+
+    def set_attrib(self, entity: 'DXFEntity', key: str, value: 'TagValue') -> None:
+        """
+        Set DXF attribute 'key' to value.
+
+        Args:
+            entity: DXF entity
+            key: attribute name
+            value: attribute value
+
+        """
+        if self.dxfversion is not None:
+            if entity.drawing.dxfversion < self.dxfversion:
+                msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'."
+                raise DXFAttributeError(msg.format(key, entity.drawing.dxfversion, self.dxfversion))
+
+        if self.xtype is XType.callback:
+            self.set_callback_value(entity, value)
+            return
+
+        subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass)
+        if self.xtype is not None:
+            self._set_extended_type(subclass_tags, value)
+        else:
+            subclass_tags.set_first(dxftag(self.code, value))
+
+    def _set_extended_type(self, tags: Tags, value: Iterable) -> None:
+        value = tuple(value)
+        vlen = len(value)
+        if vlen == 3:
+            if self.xtype is XType.point2d:
+                raise DXFValueError('2 axis required')
+        elif vlen == 2:
+            if self.xtype is XType.point3d:
+                raise DXFValueError('3 axis required')
+        else:
+            raise DXFValueError('2 or 3 axis required')
+        tags.set_first(DXFVertex(self.code, value))
+
+    def del_attrib(self, entity: 'DXFEntity') -> None:
+        """
+        Remove tag of DXF attribute in 'entity'.
+
+        Args:
+            entity: DXF entity
+
+        """
+        subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass)
+        subclass_tags.remove_tags(codes=(self.code,))
+
+
+class DXFAttributes:
+    def __init__(self, *subclassdefs: DefSubclass):
+        self._subclasses = []  # type: List[DefSubclass]
+        self._attribs = {}  # type: Dict[str, DXFAttr]
+        for subclass in subclassdefs:
+            self.add_subclass(subclass)
+
+    def add_subclass(self, subclass: DefSubclass) -> None:
+        subclass_index = len(self._subclasses)
+        self._subclasses.append(subclass)
+        self._add_subclass_attribs(subclass, subclass_index)
+
+    def _add_subclass_attribs(self, subclass: DefSubclass, subclass_index: int) -> None:
+        for name, dxfattrib in subclass.attribs.items():
+            dxfattrib.name = name
+            dxfattrib.subclass = subclass_index
+            self._attribs[name] = dxfattrib
+
+    def __getitem__(self, name: str) -> DXFAttr:
+        return self._attribs[name]
+
+    def __contains__(self, name: str) -> bool:
+        return name in self._attribs
+
+    def get(self, key: str, default: Any = None) -> Any:
+        return self._attribs.get(key, default)
+
+    def keys(self) -> KeysView[str]:
+        return self._attribs.keys()
+
+    def items(self) -> ItemsView[str, DXFAttr]:
+        return self._attribs.items()
+
+    def subclasses(self) -> Iterable[DefSubclass]:
+        return iter(self._subclasses)
+
+    def build_group_code_items(self, func=lambda x: True):
+        for name, attrib in self.items():
+            if attrib.code > 0 and func(name):  # skip internal tags
+                yield attrib.code, name

+ 445 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/const.py

@@ -0,0 +1,445 @@
+# Purpose: constant values
+# Created: 10.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+
+acad_release = {
+    'AC1009': 'R12',
+    'AC1012': 'R13',
+    'AC1014': 'R14',
+    'AC1015': 'R2000',
+    'AC1018': 'R2004',
+    'AC1021': 'R2007',
+    'AC1024': 'R2010',
+    'AC1027': 'R2013',
+    'AC1032': 'R2018',
+}
+
+versions_supported_by_new = ['AC1009', 'AC1015', 'AC1018', 'AC1021', 'AC1024', 'AC1027', 'AC1032']
+LATEST_DXF_VERSION = versions_supported_by_new[-1]
+
+acad_release_to_dxf_version = {
+    acad: dxf for dxf, acad in acad_release.items()
+}
+
+
+class DXFError(Exception):  # root exception
+    pass
+
+
+class DXFStructureError(DXFError):
+    pass
+
+
+class DXFAppDataError(DXFStructureError):
+    pass
+
+
+class DXFXDataError(DXFStructureError):
+    pass
+
+
+class DXFVersionError(DXFError):
+    pass
+
+
+class DXFInternalEzdxfError(DXFError):
+    pass
+
+
+class DXFValueError(DXFError, ValueError):
+    pass
+
+
+class DXFKeyError(DXFError, KeyError):
+    pass
+
+
+class DXFAttributeError(DXFError, AttributeError):
+    pass
+
+
+class DXFIndexError(DXFError, IndexError):
+    pass
+
+
+class DXFTypeError(DXFError, TypeError):
+    pass
+
+
+class DXFTableEntryError(DXFValueError):
+    pass
+
+
+class DXFEncodingError(DXFError):
+    pass
+
+
+class DXFDecodingError(DXFError):
+    pass
+
+
+class DXFInvalidLayerName(DXFValueError):
+    pass
+
+
+class DXFBlockInUseError(DXFValueError):
+    pass
+
+
+class DXFUndefinedBlockError(DXFKeyError):
+    pass
+
+
+APP_DATA_MARKER = 102
+SUBCLASS_MARKER = 100
+XDATA_MARKER = 1001
+COMMENT_MARKER = 999
+STRUCTURE_MARKER = 0
+HEADER_VAR_MARKER = 9
+
+# Special tag codes for internal purpose
+# -1 to -5 id reserved by AutoCAD for internal use, but this tags will never be saved to file.
+# Same approach here, the following tags have to be converted/transformed into normal tags before
+# saved to file.
+COMPRESSED_TAGS = -10
+
+BYBLOCK = 0
+BYLAYER = 256
+BYOBJECT = 257
+
+LINEWEIGHT_BYLAYER = -1
+LINEWEIGHT_BYBLOCK = -2
+LINEWEIGHT_DEFAULT = -3
+
+# Entity: Polyline, Polymesh
+# 70 flags
+POLYLINE_CLOSED = 1
+POLYLINE_MESH_CLOSED_M_DIRECTION = POLYLINE_CLOSED
+POLYLINE_CURVE_FIT_VERTICES_ADDED = 2
+POLYLINE_SPLINE_FIT_VERTICES_ADDED = 4
+POLYLINE_3D_POLYLINE = 8
+POLYLINE_3D_POLYMESH = 16
+POLYLINE_MESH_CLOSED_N_DIRECTION = 32
+POLYLINE_POLYFACE = 64
+POLYLINE_GENERATE_LINETYPE_PATTERN = 128
+
+# Entity: Polymesh
+# 75 surface smooth type
+POLYMESH_NO_SMOOTH = 0
+POLYMESH_QUADRATIC_BSPLINE = 5
+POLYMESH_CUBIC_BSPLINE = 6
+POLYMESH_BEZIER_SURFACE = 8
+
+# Entity: Vertex
+# 70 flags
+VERTEXNAMES = ('vtx0', 'vtx1', 'vtx2', 'vtx3')
+VTX_EXTRA_VERTEX_CREATED = 1  # Extra vertex created by curve-fitting
+VTX_CURVE_FIT_TANGENT = 2  # Curve-fit tangent defined for this vertex.
+# A curve-fit tangent direction of 0 may be omitted from the DXF output, but is
+# significant if this bit is set.
+# 4 = unused, never set in dxf files
+VTX_SPLINE_VERTEX_CREATED = 8  # Spline vertex created by spline-fitting
+VTX_SPLINE_FRAME_CONTROL_POINT = 16
+VTX_3D_POLYLINE_VERTEX = 32
+VTX_3D_POLYGON_MESH_VERTEX = 64
+VTX_3D_POLYFACE_MESH_VERTEX = 128
+
+VERTEX_FLAGS = {
+    'AcDb2dPolyline': 0,
+    'AcDb3dPolyline': VTX_3D_POLYLINE_VERTEX,
+    'AcDbPolygonMesh': VTX_3D_POLYGON_MESH_VERTEX,
+    'AcDbPolyFaceMesh': VTX_3D_POLYGON_MESH_VERTEX | VTX_3D_POLYFACE_MESH_VERTEX,
+}
+POLYLINE_FLAGS = {
+    'AcDb2dPolyline': 0,
+    'AcDb3dPolyline': POLYLINE_3D_POLYLINE,
+    'AcDbPolygonMesh': POLYLINE_3D_POLYMESH,
+    'AcDbPolyFaceMesh': POLYLINE_POLYFACE,
+}
+
+# block-type flags (bit coded values, may be combined):
+# Entity: BLOCK
+# 70 flags
+
+# This is an anonymous block generated by hatching, associative dimensioning, other internal operations, or an
+# application
+BLK_ANONYMOUS = 1
+
+# This block has non-constant attribute definitions (this bit is not set if the block has any attribute definitions that
+# are constant, or has no attribute definitions at all)
+BLK_NON_CONSTANT_ATTRIBUTES = 2
+
+BLK_XREF = 4  # This block is an external reference (xref)
+BLK_XREF_OVERLAY = 8  # This block is an xref overlay
+BLK_EXTERNAL = 16  # This block is externally dependent
+BLK_RESOLVED = 32  # This is a resolved external reference, or dependent of an external reference (ignored on input)
+BLK_REFERENCED = 64  # This definition is a referenced external reference (ignored on input)
+
+LWPOLYLINE_CLOSED = 1
+LWPOLYLINE_PLINEGEN = 128
+
+TEXT_ALIGN_FLAGS = {
+    'LEFT': (0, 0),
+    'CENTER': (1, 0),
+    'RIGHT': (2, 0),
+    'ALIGNED': (3, 0),
+    'MIDDLE': (4, 0),
+    'FIT': (5, 0),
+    'BOTTOM_LEFT': (0, 1),
+    'BOTTOM_CENTER': (1, 1),
+    'BOTTOM_RIGHT': (2, 1),
+    'MIDDLE_LEFT': (0, 2),
+    'MIDDLE_CENTER': (1, 2),
+    'MIDDLE_RIGHT': (2, 2),
+    'TOP_LEFT': (0, 3),
+    'TOP_CENTER': (1, 3),
+    'TOP_RIGHT': (2, 3),
+}
+TEXT_ALIGNMENT_BY_FLAGS = dict((flags, name) for name, flags in TEXT_ALIGN_FLAGS.items())
+LEFT = 0
+CENTER = 1
+RIGHT = 2
+BOTTOM = 1
+MIDDLE = 2
+TOP = 3
+MIRROR_X = 2
+MIRROR_Y = 4
+
+MTEXT_TOP_LEFT = 1
+MTEXT_TOP_CENTER = 2
+MTEXT_TOP_RIGHT = 3
+MTEXT_MIDDLE_LEFT = 4
+MTEXT_MIDDLE_CENTER = 5
+MTEXT_MIDDLE_RIGHT = 6
+MTEXT_BOTTOM_LEFT = 7
+MTEXT_BOTTOM_CENTER = 8
+MTEXT_BOTTOM_RIGHT = 9
+
+MTEXT_ALIGN_FLAGS = {
+    'TOP_LEFT': 1,
+    'TOP_CENTER': 2,
+    'TOP_RIGHT': 3,
+    'MIDDLE_LEFT': 4,
+    'MIDDLE_CENTER': 5,
+    'MIDDLE_RIGHT': 6,
+    'BOTTOM_LEFT': 7,
+    'BOTTOM_CENTER': 8,
+    'BOTTOM_RIGHT': 9,
+}
+
+MTEXT_LEFT_TO_RIGHT = 1
+MTEXT_TOP_TO_BOTTOM = 3
+MTEXT_BY_STYLE = 5
+
+MTEXT_AT_LEAST = 1
+MTEXT_EXACT = 2
+
+MTEXT_COLOR_INDEX = {
+    'red': 1,
+    'yellow': 2,
+    'green': 3,
+    'cyan': 4,
+    'blue': 5,
+    'magenta': 6,
+    'white': 7,
+}
+
+MTEXT_BG_OFF = 0
+MTEXT_BG_COLOR = 1
+MTEXT_BG_WINDOW_COLOR = 2
+MTEXT_BG_CANVAS_COLOR = 3
+
+MTEXT_INLINE_ALIGN = {
+    'BOTTOM': 0,
+    'MIDDLE': 1,
+    'TOP': 2,
+}
+
+CLOSED_SPLINE = 1
+PERIODIC_SPLINE = 2
+RATIONAL_SPLINE = 4
+PLANAR_SPLINE = 8
+LINEAR_SPLINE = 16
+
+# Hatch constants
+HATCH_TYPE_USER_DEFINED = 0
+HATCH_TYPE_PREDEFINED = 1
+HATCH_TYPE_CUSTOM = 2
+
+HATCH_STYLE_NORMAL = 0
+HATCH_STYLE_OUTERMOST = 1
+HATCH_STYLE_IGNORE = 2
+
+BOUNDARY_PATH_DEFAULT = 0
+BOUNDARY_PATH_EXTERNAL = 1
+BOUNDARY_PATH_POLYLINE = 2
+BOUNDARY_PATH_DERIVED = 4
+BOUNDARY_PATH_TEXTBOX = 8
+BOUNDARY_PATH_OUTERMOST = 16
+
+GRADIENT_TYPES = frozenset([
+    'LINEAR',
+    'CYLINDER',
+    'INVCYLINDER',
+    'SPHERICAL',
+    'INVSPHERICAL',
+    'HEMISPHERICAL',
+    'INVHEMISPHERICAL',
+    'CURVED',
+    'INVCURVED'
+])
+
+# Viewport Status Flags (VSF) group code=90
+VSF_PERSPECTIVE_MODE = 0x1  # enabled if set
+VSF_FRONT_CLIPPING = 0x2  # enabled if set
+VSF_BACK_CLIPPING = 0x4  # enabled if set
+VSF_USC_FOLLOW = 0x8  # enabled if set
+VSF_FRONT_CLIPPING_NOT_AT_EYE = 0x10  # enabled if set
+VSF_UCS_ICON_VISIBILITY = 0x20  # enabled if set
+VSF_UCS_ICON_AT_ORIGIN = 0x40  # enabled if set
+VSF_FAST_ZOOM = 0x80  # enabled if set
+VSF_SNAP_MODE = 0x100  # enabled if set
+VSF_GRID_MODE = 0x200  # enabled if set
+VSF_ISOMETRIC_SNAP_STYLE = 0x400  # enabled if set
+VSF_HIDE_PLOT_MODE = 0x800  # enabled if set
+VSF_KISOPAIR_TOP = 0x1000  # If set and kIsoPairRight is not set, then isopair top is enabled. If both kIsoPairTop and kIsoPairRight are set, then isopair left is enabled
+VSF_KISOPAIR_RIGHT = 0x2000  # If set and kIsoPairTop is not set, then isopair right is enabled
+VSF_VIEWPORT_ZOOM_LOCKING = 0x4000  # enabled if set
+VSF_CURRENTLY_ALWAYS_ENABLED = 0x8000  # always set without a meaning :)
+VSF_NON_RECTANGULAR_CLIPPING = 0x10000  # enabled if set
+VSF_TURN_VIEWPORT_OFF = 0x20000
+
+# Viewport Render Mode (VRM) group code=281
+VRM_2D_OPTIMIZED = 0
+VRM_WIREFRAME = 1
+VRM_HIDDEN_LINE = 2
+VRM_FLAT_SHADED = 3
+VRM_GOURAUD_SHADED = 4
+VRM_FLAT_SHADED_WITH_WIREFRAME = 5
+VRM_GOURAUD_SHADED_WITH_WIREFRAME = 6
+
+IMAGE_SHOW = 1
+IMAGE_SHOW_WHEN_NOT_ALIGNED = 2
+IMAGE_USE_CLIPPING_BOUNDARY = 4
+IMAGE_TRANSPARENCY_IS_ON = 8
+
+UNDERLAY_CLIPPING = 1
+UNDERLAY_ON = 2
+UNDERLAY_MONOCHROME = 4
+UNDERLAY_ADJUST_FOR_BG = 8
+
+DIM_LINEAR = 0
+DIM_ALIGNED = 1
+DIM_ANGULAR = 2
+DIM_DIAMETER = 3
+DIM_RADIUS = 4
+DIM_ANGULAR_3P = 5
+DIM_ORDINATE = 6
+DIM_BLOCK_EXCLUSIVE = 32
+DIM_ORDINATE_TYPE = 64
+DIM_USER_LOCATION_OVERRIDE = 128
+
+DIMZIN_SUPPRESS_ZERO_FEET_AND_PRECISELY_ZERO_INCHES = 0
+DIMZIN_INCLUDES_ZERO_FEET_AND_PRECISELY_ZERO_INCHES = 1
+DIMZIN_INCLUDES_ZERO_FEET_AND_SUPPRESSES_ZERO_INCHES = 2
+DIMZIN_INCLUDES_ZERO_INCHES_AND_SUPPRESSES_ZERO_FEET = 3
+DIMZIN_SUPPRESSES_LEADING_ZEROS = 4  # only decimal dimensions
+DIMZIN_SUPPRESSES_TRAILING_ZEROS = 8  # only decimal dimensions
+
+# ATTRIB & ATTDEF flags
+ATTRIB_INVISIBLE = 1  # Attribute is invisible (does not appear)
+ATTRIB_CONST = 2  # This is a constant attribute
+ATTRIB_VERIFY = 4  # Verification is required on input of this attribute
+ATTRIB_IS_PRESET = 8  # no prompt during insertion
+
+
+class Error:
+    MISSING_REQUIRED_ROOT_DICT_ENTRY = 1
+    DUPLICATE_TABLE_ENTRY_NAME = 2
+    POINTER_TARGET_NOT_EXISTS = 3
+    TABLE_NOT_FOUND = 4
+    UNDEFINED_LINETYPE = 100
+    UNDEFINED_DIMENSION_STYLE = 101
+    UNDEFINED_TEXT_STYLE = 102
+    INVALID_LAYER_NAME = 200
+    INVALID_COLOR_INDEX = 201
+    INVALID_OWNER_HANDLE = 202
+    INVALID_GROUP_CODE_IN_CLASS_DEFINITION = 203
+
+
+INVALID_LAYER_NAME_CHARACTERS = frozenset(['<', '>', '/', '\\', '"', ':', ';', '?', '*', '|', '=', '`'])
+
+STD_SCALES = {
+    1: (1. / 128., 12.),
+    2: (1. / 64., 12.),
+    3: (1. / 32., 12.),
+    4: (1. / 16., 12.),
+    5: (3. / 32., 12.),
+    6: (1. / 8., 12.),
+    7: (3. / 16., 12.),
+    8: (1. / 4., 12.),
+    9: (3. / 8., 12.),
+    10: (1. / 2., 12.),
+    11: (3. / 4., 12.),
+    12: (1., 12.),
+    13: (3., 12.),
+    14: (6., 12.),
+    15: (12., 12.),
+    16: (1., 1.),
+    17: (1., 2.),
+    18: (1., 4.),
+    19: (1., 8.),
+    20: (1., 10.),
+    21: (1., 16.),
+    22: (1., 20.),
+    23: (1., 30.),
+    24: (1., 40.),
+    25: (1., 50.),
+    26: (1., 100.),
+    27: (2., 1.),
+    28: (4., 1.),
+    29: (8., 1.),
+    30: (10., 1.),
+    31: (100., 1.),
+    32: (1000., 1.),
+}
+
+RASTER_UNITS = {
+    'mm': 1,
+    'cm': 2,
+    'm': 3,
+    'km': 4,
+    'in': 5,
+    'ft': 6,
+    'yd': 7,
+    'mi': 8,
+}
+
+LAYOUT_NAMES = ('*paper_space', '$paper_space', '*model_space', '$model_space')
+
+
+class SortEntities:
+    DISABLE = 0
+    SELECTION = 1  # 1 = Sorts for object selection
+    SNAP = 2  # 2 = Sorts for object snap
+    REDRAW = 4  # 4 = Sorts for redraws; obsolete
+    MSLIDE = 8  # 8 = Sorts for MSLIDE command slide creation; obsolete
+    REGEN = 16  # 16 = Sorts for REGEN commands
+    PLOT = 32  # 32 = Sorts for plotting
+    POSTSCRIPT = 64  # 64 = Sorts for PostScript output; obsolete
+
+
+DIMJUST = {
+    'center': 0,
+    'left': 1,
+    'right': 2,
+    'above1': 3,
+    'above2': 4,
+}
+
+DIMTAD = {
+    'above': 1,
+    'center': 0,
+    'below': 4,
+}

+ 30 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/encoding.py

@@ -0,0 +1,30 @@
+# Purpose: low level DXF data encoding/decoding module
+# Created: 26.03.2016
+# Copyright (c) 2016-2018, Manfred Moitzi
+# License: MIT License
+from .const import DXFEncodingError
+
+
+def dxf_backslash_replace(exc: Exception):
+    if isinstance(exc, (UnicodeEncodeError, UnicodeTranslateError)):
+        s = ""
+        for c in exc.object[exc.start:exc.end]:
+            if ord(c) <= 0xff:
+                s += "\\x%02x" % ord(c)
+            elif ord(c) <= 0xffff:
+                s += "\\U+%04x" % ord(c)
+            else:
+                s += "\\U+%08x" % ord(c)
+        return s, exc.end
+    else:
+        raise TypeError("can't handle %s" % exc.__name__)
+
+
+def encode(unicode: str, encoding: str = 'cp1252', ignore_error: bool = False):
+    try:
+        return bytes(unicode, encoding)
+    except UnicodeEncodeError:  # can not use the given encoding
+        if ignore_error:  # encode string with the default unicode encoding
+            return bytes(unicode, 'utf-8')
+        else:
+            raise DXFEncodingError("Can not encode string '{}' with given encoding '{}'".format(unicode, encoding))

+ 366 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/extendedtags.py

@@ -0,0 +1,366 @@
+# Created: 30.04.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, Optional, Callable, List
+from itertools import chain
+
+from .types import tuples_to_tags
+from .tags import Tags, DXFTag, NONE_TAG
+from .const import DXFStructureError, DXFValueError, DXFKeyError
+from .types import APP_DATA_MARKER, SUBCLASS_MARKER, XDATA_MARKER
+from .types import is_app_data_marker, is_embedded_object_marker
+from .tagger import internal_tag_compiler
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import IterableTags
+
+
+class ExtendedTags:
+    """
+    Manage Subclasses, AppData and Extended Data
+
+    """
+    __slots__ = ('subclasses', 'appdata', 'xdata', 'link', 'embedded_objects')
+
+    def __init__(self, iterable: Iterable[DXFTag] = None):
+        if isinstance(iterable, str):
+            raise DXFValueError("use ExtendedTags.from_text() to create tags from a string.")
+
+        self.appdata = list()  # type: List[Tags] # code == 102, keys are "{<arbitrary name>", values are Tags()
+        self.subclasses = list()  # type: List[Tags] # code == 100, keys are "subclassname", values are Tags()
+        self.xdata = list()  # type: List[Tags] # code >= 1000, keys are "APPNAME", values are Tags()
+        self.link = None  # type: Optional[str] # link (as handle) to following entities like INSERT -> ATTRIB and POLYLINE -> VERTEX
+
+        # store embedded objects as list, but embedded objects are rare, so storing an empty list for every DXF entity
+        # is waste of memory
+        self.embedded_objects = None  # type: Optional[List[Tags]]
+        if iterable is not None:
+            self._setup(iterable)
+
+    def __copy__(self) -> 'ExtendedTags':
+        """
+        Shallow copy - linked entities are not duplicated!
+
+        ExtendedTags() knows nothing about the entity database, and has no access to, so it is not possible for
+        ExtendedTags() to do a deep copy, by also copying linked entities (VERTEX, ATTRIB, SEQEND).
+        To do a deep copy you have to go one level up and use DXFEntity.copy()
+
+        """
+
+        def copy(tag_lists):
+            return [tags.clone() for tags in tag_lists]
+
+        clone = self.__class__()
+        clone.appdata = copy(self.appdata)
+        clone.subclasses = copy(self.subclasses)
+        clone.xdata = copy(self.xdata)
+        if self.embedded_objects is not None:
+            clone.embedded_objects = copy(self.embedded_objects)
+        clone.link = self.link  # important for dxf importer!
+        return clone
+
+    clone = __copy__
+
+    def __getitem__(self, index) -> Tags:
+        return self.noclass[index]
+
+    @property
+    def noclass(self) -> Tags:
+        return self.subclasses[0]
+
+    def get_handle(self) -> str:
+        return self.noclass.get_handle()
+
+    def dxftype(self) -> str:
+        return self.noclass[0].value
+
+    def replace_handle(self, handle: str) -> None:
+        self.noclass.replace_handle(handle)
+
+    def _setup(self, iterable: Iterable[DXFTag]) -> None:
+        tagstream = iter(iterable)
+
+        def collect_subclass(starttag: Optional[DXFTag]) -> DXFTag:
+            """
+            A subclass can contain appdata, but not XDATA, ends with
+            SUBCLASS_MARKER, XDATA_MARKER or EMBEDDED_OBJ_MARKER.
+
+            """
+            # All subclasses begin with (100, subclass name)
+            # EXCEPT DIMASSOC has one subclass starting with: (1, AcDbOsnapPointRef). Well done, Autodesk!
+            # This special subclass is ignored by ezdxf, content is included in the preceding subclass: (100, AcDbDimAssoc)
+            #
+            # TEXT contains 2x the (100, AcDbText). Also well done, Autodesk! Therefore it is not possible to use an
+            # (ordered) dict where subclass name is key, but usual use case is access by index.
+
+            data = Tags() if starttag is None else Tags([starttag])
+            try:
+                while True:
+                    tag = next(tagstream)
+                    if is_app_data_marker(tag):
+                        app_data_pos = len(self.appdata)
+                        data.append(DXFTag(tag.code, app_data_pos))
+                        collect_app_data(tag)
+                    elif tag.code in (SUBCLASS_MARKER, XDATA_MARKER) or is_embedded_object_marker(tag):
+                        self.subclasses.append(data)
+                        return tag
+                    else:
+                        data.append(tag)
+            except StopIteration:
+                pass
+            self.subclasses.append(data)
+            return NONE_TAG
+
+        def collect_app_data(starttag: DXFTag) -> None:
+            """
+            Appdata, cannot contain XDATA or subclasses.
+
+            """
+            data = Tags([starttag])
+            closing_strings = ('}', starttag.value[1:] + '}')  # alternative closing tag 'APPID}'
+            while True:
+                try:
+                    tag = next(tagstream)
+                except StopIteration:
+                    raise DXFStructureError("Missing closing (102, '}') tag for appdata structure.")
+                data.append(tag)
+                if (tag.code == APP_DATA_MARKER) and (tag.value in closing_strings):
+                    break
+                    # every other (102, ) tag is treated as usual tag
+            self.appdata.append(data)
+
+        def collect_xdata(starttag: DXFTag) -> DXFTag:
+            """
+            XDATA is always at the end of the entity and can not contain appdata or subclasses
+
+            NEW: 09.08.2018
+
+            Since AutoCAD 2018, DXF entities can contain embedded objects, this objects appear at the end of an entity,
+            after XDATA (if XDATA exists).
+
+            """
+            data = Tags([starttag])
+            try:
+                while True:
+                    tag = next(tagstream)
+                    if tag.code == XDATA_MARKER:
+                        self.xdata.append(data)
+                        return tag
+                    else:
+                        data.append(tag)
+            except StopIteration:
+                pass
+            self.xdata.append(data)
+            return NONE_TAG
+
+        def collect_embedded_object(starttag: DXFTag) -> DXFTag:
+            """
+            Since AutoCAD 2018, DXF entities can contain embedded objects, this objects appear at the end of an entity,
+            also after XDATA, and start with the (101, 'Embedded Object') tag.
+
+            All embedded object data is collected in a simple Tags() object, no subclass app data or XDATA processing is
+            done. ezdxf does not use or modify the embedded object data, the data is just stored and written out as it
+            is.
+
+            self.embedded_objects = [1. embedded object as Tags(), 2. embedded object as Tags(), ...]
+
+            """
+            if self.embedded_objects is None:
+                self.embedded_objects = list()
+            data = Tags([starttag])
+            try:
+                while True:
+                    tag = next(tagstream)
+                    if is_embedded_object_marker(tag):
+                        # another embedded object found, don't know if an DXF entity can contain more than one embedded
+                        # objects
+                        self.embedded_objects.append(data)
+                        return tag
+                    else:
+                        data.append(tag)
+            except StopIteration:
+                pass
+            self.embedded_objects.append(data)
+            return NONE_TAG
+
+        tag = collect_subclass(None)  # preceding tags without a subclass
+        while tag.code == SUBCLASS_MARKER:
+            tag = collect_subclass(tag)
+
+        if not is_embedded_object_marker(tag):
+            # XDATA can not appear after an embedded object
+            while tag.code == XDATA_MARKER:
+                tag = collect_xdata(tag)
+
+        while is_embedded_object_marker(tag):
+            tag = collect_embedded_object(tag)
+
+        if tag is not NONE_TAG:
+            raise DXFStructureError("Unexpected tag '%r' at end of entity." % tag)
+
+    def __iter__(self) -> Iterable[DXFTag]:
+        for subclass in self.subclasses:
+            for tag in subclass:
+                if tag.code == APP_DATA_MARKER and isinstance(tag.value, int):
+                    yield from self.appdata[tag.value]
+                else:
+                    yield tag
+
+        yield from chain.from_iterable(self.xdata)
+
+        if self.embedded_objects is not None:
+            yield from chain.from_iterable(self.embedded_objects)
+
+    def get_subclass(self, name: str, pos: int = 0) -> Tags:
+        for index, subclass in enumerate(self.subclasses):
+            try:
+                if (index >= pos) and (subclass[0].value == name):
+                    return subclass
+            except IndexError:
+                pass  # subclass[0]: ignore empty subclasses
+
+        raise DXFKeyError("Subclass '%s' does not exist." % name)
+
+    def has_xdata(self, appid: str) -> bool:
+        return any(xdata[0].value == appid for xdata in self.xdata)
+
+    def get_xdata(self, appid: str) -> Tags:
+        for xdata in self.xdata:
+            if xdata[0].value == appid:
+                return xdata
+        raise DXFValueError("No extended data for APPID '%s'" % appid)
+
+    def set_xdata(self, appid: str, tags: 'IterableTags') -> None:
+        xdata = self.get_xdata(appid)
+        xdata[1:] = tuples_to_tags(tags)
+
+    def new_xdata(self, appid: str, tags: 'IterableTags' = None) -> Tags:
+        """
+        Append a new xdata block.
+
+        Assumes that no xdata block with the same appid already exists::
+
+            try:
+                xdata = tags.get_xdata('EZDXF')
+            except ValueError:
+                xdata = tags.new_xdata('EZDXF')
+        """
+        xtags = Tags([DXFTag(XDATA_MARKER, appid)])
+        if tags is not None:
+            xtags.extend(tuples_to_tags(tags))
+        self.xdata.append(xtags)
+        return xtags
+
+    def has_app_data(self, appid: str) -> bool:
+        return any(appdata[0].value == appid for appdata in self.appdata)
+
+    def get_app_data(self, appid: str) -> Tags:
+        """
+        Get app data including first and last marker tag.
+
+        """
+        for appdata in self.appdata:
+            if appdata[0].value == appid:
+                return appdata
+        raise DXFValueError("Application defined group '%s' does not exist." % appid)
+
+    def get_app_data_content(self, appid: str) -> Tags:
+        """
+        Get app data without first and last marker tag.
+
+        """
+        return Tags(self.get_app_data(appid)[1:-1])
+
+    def set_app_data_content(self, appid: str, tags: 'IterableTags') -> None:
+        app_data = self.get_app_data(appid)
+        app_data[1:-1] = tuples_to_tags(tags)
+
+    def new_app_data(self, appid: str, tags: 'IterableTags' = None, subclass_name: str = None) -> Tags:
+        """
+        Append a new app data block to subclass *subclass_name*.
+
+        Assumes that no app data block with the same appid already exists::
+
+            try:
+                app_data = tags.get_app_data('{ACAD_REACTORS', tags)
+            except ValueError:
+                app_data = tags.new_app_data('{ACAD_REACTORS', tags)
+
+        """
+        if not appid.startswith('{'):
+            raise DXFValueError("App data id has to start with '{'.")
+
+        app_tags = Tags([
+            DXFTag(APP_DATA_MARKER, appid),
+            DXFTag(APP_DATA_MARKER, '}'),
+        ])
+        if tags is not None:
+            app_tags[1:1] = tuples_to_tags(tags)
+
+        if subclass_name is None:
+            subclass = self.noclass
+        else:
+            subclass = self.get_subclass(subclass_name, 1)  # raises KeyError, if not exists
+        app_data_pos = len(self.appdata)
+        subclass.append(DXFTag(APP_DATA_MARKER, app_data_pos))
+        self.appdata.append(app_tags)
+        return app_tags
+
+    @classmethod
+    def from_text(cls, text: str) -> 'ExtendedTags':
+        return cls(internal_tag_compiler(text))
+
+
+LINKED_ENTITIES = {
+    'INSERT': 'ATTRIB',
+    'POLYLINE': 'VERTEX'
+}
+
+
+def get_xtags_linker() -> Callable[[ExtendedTags], bool]:
+    prev = None  # type: Optional[ExtendedTags]
+    expected = ""
+
+    def xtags_linker(tags: ExtendedTags) -> bool:
+        nonlocal prev, expected
+        handle = tags.get_handle()
+
+        def attribs_follow() -> bool:
+            try:
+                ref_tags = tags.get_subclass('AcDbBlockReference')
+            except DXFKeyError:
+                return False
+            else:
+                return bool(ref_tags.get_first_value(66, 0))
+
+        dxftype = tags.dxftype()  # type: str
+        are_linked_tags = False  # INSERT & POLYLINE are not linked tags, they are stored in the entity space
+        if prev is not None:
+            are_linked_tags = True  # VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in the entity space
+            if dxftype == 'SEQEND':
+                prev.link = handle
+                prev = None
+            # check for valid DXF structure just VERTEX follows POLYLINE and just ATTRIB follows INSERT
+            elif dxftype == expected:
+                prev.link = handle
+                prev = tags
+            else:
+                raise DXFStructureError("expected DXF entity {} or SEQEND".format(dxftype))
+        elif dxftype in ('INSERT', 'POLYLINE'):  # only these two DXF types have this special linked structure
+            if dxftype == 'INSERT' and not attribs_follow():
+                # INSERT must not have following ATTRIBS, ATTRIB can be a stand alone entity:
+                #   INSERT with no ATTRIBS, attribs_follow == 0
+                #   ATTRIB as stand alone entity
+                #   ....
+                #   INSERT with ATTRIBS, attribs_follow == 1
+                #   ATTRIB as connected entity
+                #   SEQEND
+                #
+                # Therefore a ATTRIB following an INSERT doesn't mean that these entities are connected.
+                pass
+            else:
+                prev = tags
+                expected = LINKED_ENTITIES[dxftype]
+        return are_linked_tags  # caller should know, if *tags* should be stored in the entity space or not
+
+    return xtags_linker

+ 19 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/hdrvars.py

@@ -0,0 +1,19 @@
+# Purpose: header variables factory
+# Created: 20.11.2010
+# Copyright (c) 2010-2018, Manfred Moitzi
+# License: MIT License
+from typing import Sequence, Union
+from .types import DXFVertex, DXFTag, cast_tag_value
+
+
+def SingleValue(value: Union[str, float], code: int = 1) -> DXFTag:
+    return DXFTag(code, cast_tag_value(code, value))
+
+
+def Point2D(value: Sequence[float]) -> DXFVertex:
+
+    return DXFVertex(10, (value[0], value[1]))
+
+
+def Point3D(value: Sequence[float]) -> DXFVertex:
+    return DXFVertex(10, (value[0], value[1], value[2]))

+ 169 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/loader.py

@@ -0,0 +1,169 @@
+# Purpose: DXF structure loader and validator
+# Created: 25.01.2018
+# Copyright (c) 2018, Manfred Moitzi
+# License: MIT License
+import logging
+from typing import Callable, Dict, Iterable, List, Union, TYPE_CHECKING
+
+from .const import DXFStructureError
+from .tags import group_tags, DXFTag, Tags
+from .extendedtags import ExtendedTags
+from .validator import entity_structure_validator
+
+from ezdxf.options import options
+
+if TYPE_CHECKING:  # import forward declarations
+    from ezdxf.eztypes import EntityDB
+
+logger = logging.getLogger('ezdxf')
+
+TagProcessor = Callable[[ExtendedTags], ExtendedTags]
+modern_post_load_tag_processors = {}  # type: Dict[str, TagProcessor]
+legacy_post_load_tag_processors = {}  # type: Dict[str, TagProcessor]
+
+
+SectionDict = Dict[str, List[Union[Tags, ExtendedTags]]]
+
+
+def is_registered(entity: str, legacy: bool = False):
+    # just for testing
+    processors = legacy_post_load_tag_processors if legacy else modern_post_load_tag_processors
+    return entity in processors
+
+
+def register(entity: str, legacy: bool = False) -> Callable:
+    """
+    Register (decorator) functions to process from DXF file loaded tags.
+
+    Args:
+        entity: DXF type like 'LINE' or 'VERTEX'
+        legacy: use for legacy tag structure (DXF version <= AC1009) or modern tag structures
+
+    """
+    logger.debug('Register post load tag processor for DXF type: {}; legacy: {}'.format(entity, legacy))
+
+    def decorator(processor: TagProcessor) -> TagProcessor:
+        """
+
+        Args:
+            processor: function with one parameter 'tags'
+
+        Returns: processor
+
+        """
+        processors = legacy_post_load_tag_processors if legacy else modern_post_load_tag_processors
+        processors[entity] = processor
+        return processor
+
+    return decorator
+
+
+def load_dxf_structure(tagger: Iterable[DXFTag], ignore_missing_eof: bool = False) -> SectionDict:
+    """
+    Divide input tag stream from tagger into DXF structure entities. Each DXF structure entity starts with a DXF
+    structure (0, ...) tag, and ends before the next DXF structure tag.
+
+    Generated structure:
+
+    each entity is a Tags() object
+
+    {
+        'HEADER': [entity],                # 1. section, HEADER section contains only the SECTION head tag
+        'CLASSES': [entity, entity, ...],  # 2. section
+        'TABLES': [entity, entity, ...],   # 3. section
+        ...
+        'OBJECTS': [entity, entity, ...],
+    }
+
+    {
+        'HEADER': [(0, 'SECTION'), (2, 'HEADER'), .... ],  # HEADER section contains only the SECTION head tag
+        'CLASSES': [[(0, 'SECTION'), (2, 'CLASSES')], [(0, 'CLASS'), ...], [(0, 'CLASS'), ...]],
+        'TABLES': [[(0, 'SECTION'), (2, 'TABLES')], [(0, 'TABLE'), (2, 'VPORT')], [(0, 'VPORT'), ...], ... , [(0, 'ENDTAB')]],
+        ...
+        'OBJECTS': [[(0, 'SECTION'), (2, 'OBJECTS')], ...]
+    }
+
+    Args:
+        tagger: generates DXFTag() entities from input data
+        ignore_missing_eof: raises DXFStructureError() if False and EOF tag is not present, set to True only in tests
+
+    Returns:
+        dict of sections, each section is a list of DXF structure entities as Tags() objects
+
+    """
+
+    def inside_section() -> bool:
+        if len(section):
+            return section[0][0] == (0, 'SECTION')  # first entity, first tag
+        return False
+
+    def outside_section() -> bool:
+        if len(section):
+            return section[0][0] != (0, 'SECTION')  # first entity, first tag
+        return True
+
+    sections = {}  # type: SectionDict
+    section = []  # type: List[Tags]
+    eof = False
+    for entity in group_tags(tagger):
+        tag = entity[0]
+        if tag == (0, 'SECTION'):
+            if inside_section():
+                raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
+            if len(section):
+                logger.warning('DXF Structure Warning: found tags outside a SECTION, ignored by ezdxf.')
+            section = [entity]
+        elif tag == (0, 'ENDSEC'):  # not collected
+            if outside_section():
+                raise DXFStructureError("DXFStructureError: found ENDSEC tag without previous SECTION tag.")
+            section_header = section[0]
+            if len(section_header) < 2 or section_header[1].code != 2:
+                raise DXFStructureError(
+                    'DXFStructureError: missing required section NAME tag (2, name) at start of section.')
+            name_tag = section_header[1]
+            sections[name_tag.value] = section
+            section = []  # collect tags outside of sections, but ignore it
+        elif tag == (0, 'EOF'):  # not collected
+            if eof:
+                logger.warning('DXF Structure Warning: found more than one EOF tags.')
+            eof = True
+        else:
+            section.append(entity)
+    if inside_section():
+        raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
+    if not eof and not ignore_missing_eof:
+        raise DXFStructureError('DXFStructureError: missing EOF tag.')
+    return sections
+
+
+DATABASE_EXCLUDE = frozenset(['SECTION', 'ENDSEC', 'EOF', 'TABLE', 'ENDTAB', 'CLASS', 'ACDSRECORD', 'ACDSSCHEMA'])
+
+
+def load_dxf_entities_into_database(database: 'EntityDB', dxf_entities: List[Tags]) -> Iterable[ExtendedTags]:
+    check_tag_structure = options.check_entity_tag_structures
+    for entity in dxf_entities:
+        if len(entity) == 0:
+            raise DXFStructureError('Invalid empty DXF entity.')
+        code, dxftype = entity[0]
+        if code != 0:
+            raise DXFStructureError('Invalid first tag in DXF entity, group code={} .'.format(code))
+        if dxftype not in DATABASE_EXCLUDE:
+            if check_tag_structure:
+                entity = entity_structure_validator(entity)
+            entity = ExtendedTags(entity)
+            database.add_tags(entity)
+        yield entity
+
+
+def fill_database(database: 'EntityDB', sections: SectionDict, dxfversion: str = 'AC1009') -> None:
+    post_processors = legacy_post_load_tag_processors if dxfversion <= 'AC1009' else modern_post_load_tag_processors
+    for name in ['TABLES', 'ENTITIES', 'BLOCKS', 'OBJECTS']:
+        if name in sections:
+            section = sections[name]
+            # entities stored in the database are converted from Tags() to ExtendedTags()
+            for index, entity in enumerate(load_dxf_entities_into_database(database, section)):
+                # entities not stored in database are still Tags() e.g. CLASS
+                processor = post_processors.get(entity.dxftype())
+                if processor:
+                    processor(entity)
+                section[index] = entity

+ 241 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/packedtags.py

@@ -0,0 +1,241 @@
+# created: 19.04.2018
+# Copyright (c) 2018 Manfred Moitzi
+# License: MIT License
+from array import array
+from abc import abstractmethod
+from collections import OrderedDict
+from typing import Iterable, Tuple, Sequence, Mapping, Union
+
+from .types import DXFTag, DXFVertex
+from .const import DXFTypeError, DXFIndexError, DXFValueError
+
+from .tags import Tags
+from ezdxf.tools import take2
+from ezdxf.tools.indexing import Index
+
+
+class PackedTags:
+    __slots__ = ()
+
+    @abstractmethod
+    def dxftags(self) -> Iterable[DXFTag]:
+        """
+        Yield packed tags as unpacked DXFTags().
+
+        """
+        pass
+
+    @abstractmethod
+    def clone(self) -> 'PackedTags':
+        """
+        Returns cloned tags (deep copy).
+
+        """
+        pass
+
+    def dxfstr(self) -> str:
+        """
+        Returns the DXF strings constructed from dxftags().
+
+        """
+        return ''.join(tag.dxfstr() for tag in self.dxftags())
+
+
+class TagList(PackedTags):
+    code = -100  # compatible with DXFTag.code
+    VALUE_CODE = 330
+    __slots__ = ('value',)
+
+    def __init__(self, data=None):
+        self.value = list(data or [])  # compatible with DXFTag.value
+
+    def dxftags(self) -> Iterable[DXFTag]:
+        for value in self.value:
+            yield DXFTag(self.VALUE_CODE, value)
+
+    def clone(self) -> 'TagList':
+        return self.__class__(data=self.value)
+
+    def replace_tags(self, tags: Tags):
+        return replace_tags(tags, codes=(self.VALUE_CODE,), packed_data=self)
+
+    @classmethod
+    def from_tags(cls, tags: Tags) -> 'TagList':
+        return cls(data=(tag.value for tag in tags if tag.code == cls.VALUE_CODE))
+
+    def clear(self) -> None:
+        del self.value[:]
+
+
+class TagArray(TagList):
+    code = -101  # compatible with DXFTag.code
+    VALUE_CODE = 60
+    DTYPE = 'i'
+
+    def __init__(self, data: Iterable = None):
+        self.value = array(self.DTYPE, data or [])  # compatible with DXFTag.value
+
+    def set_values(self, values: Iterable) -> None:
+        self.value[:] = array(self.DTYPE, values)
+
+
+class TagDict(PackedTags):
+    __slots__ = ('value',)
+    code = -102  # compatible with DXFTag.code
+    KEY_CODE = 3
+    VALUE_CODE = 350
+    SEARCH_CODES = (3, 350, 360)  # some DICTIONARY have 360 handles
+
+    def __init__(self, data: Union[Mapping, Iterable[Tuple]] = None):
+        self.value = OrderedDict(data or {})  # compatible with DXFTag.value
+
+    def dxftags(self) -> Iterable[DXFTag]:
+        for key, value in self.value.items():
+            yield DXFTag(self.KEY_CODE, key)
+            yield DXFTag(self.VALUE_CODE, value)
+
+    def clone(self) -> 'TagDict':
+        return self.__class__(data=self.value)
+
+    def replace_tags(self, tags: Tags):
+        return replace_tags(tags, codes=self.SEARCH_CODES, packed_data=self)
+
+    @classmethod
+    def from_tags(cls, tags: Tags) -> 'TagDict':
+        return cls(data=(t for t in take2(tag.value for tag in tags if tag.code in set(cls.SEARCH_CODES))))
+
+
+class VertexArray(PackedTags):
+    code = -10  # compatible with DXFTag.code
+    VERTEX_CODE = 10
+    VERTEX_SIZE = 3  # set to 2 for 2d points
+    __slots__ = ('value',)
+
+    def __init__(self, data: Iterable = None):
+        self.value = array('d', data or [])  # compatible with DXFTag.value
+
+    def __len__(self) -> int:
+        return len(self.value) // self.VERTEX_SIZE
+
+    def __getitem__(self, index: int):
+        if isinstance(index, slice):
+            return list(self._get_points(self._slicing(index)))
+        else:
+            return self._get_point(self._index(index))
+
+    def __setitem__(self, index: int, point: Sequence[float]) -> None:
+        if isinstance(index, slice):
+            raise DXFTypeError('slicing not supported')
+        else:
+            self._set_point(self._index(index), point)
+
+    def __delitem__(self, index: int) -> None:
+        if isinstance(index, slice):
+            self._del_points(self._slicing(index))
+        else:
+            self._del_point(self._index(index))
+
+    def insert(self, pos: int, point: Sequence[float]):
+        """
+        Insert point in front of point at index pos.
+
+        Args:
+            pos: insert position
+            point: point as tuple
+
+        """
+        size = self.VERTEX_SIZE
+        if len(point) != size:
+            raise DXFValueError('point requires exact {} components.'.format(size))
+
+        pos = self._index(pos) * size
+        _insert = self.value.insert
+        for value in reversed(point):
+            _insert(pos, value)
+
+    def clone(self) -> 'VertexArray':
+        return self.__class__(data=self.value)
+
+    @classmethod
+    def from_tags(cls, tags: Iterable[DXFTag]) -> 'VertexArray':
+        """
+        Setup point array from extended tags.
+
+        Args:
+            tags: Tags() object
+
+        """
+        vertices = array('d')
+        for tag in tags:
+            if tag.code == cls.VERTEX_CODE:
+                vertices.extend(tag.value)
+        return cls(data=vertices)
+
+    def _index(self, item) -> int:
+        return Index(self).index(item, error=DXFIndexError)
+
+    def _slicing(self, index) -> Iterable[int]:
+        return Index(self).slicing(index)
+
+    def _get_point(self, index: int) -> Sequence[float]:
+        size = self.VERTEX_SIZE
+        index = index * size
+        return tuple(self.value[index:index + size])
+
+    def _get_points(self, indices) -> Iterable:
+        for index in indices:
+            yield self._get_point(index)
+
+    def _set_point(self, index: int, point: Sequence[float]):
+        size = self.VERTEX_SIZE
+        if len(point) != size:
+            raise DXFValueError('point requires exact {} components.'.format(size))
+        if isinstance(point, (tuple, list)):
+            point = array('d', point)
+        index = index * size
+        self.value[index:index + size] = point
+
+    def _del_point(self, index: int) -> None:
+        size = self.VERTEX_SIZE
+        pos = index * size
+        del self.value[pos:pos + size]
+
+    def _del_points(self, indices: Iterable[int]) -> None:
+        del_flags = set(indices)
+        size = self.VERTEX_SIZE
+        survivors = array('d', (v for i, v in enumerate(self.value) if (i // size) not in del_flags))
+        self.value = survivors
+
+    def dxftags(self) -> Iterable[DXFVertex]:
+        for point in self:
+            yield DXFVertex(self.VERTEX_CODE, point)
+
+    def append(self, point: Sequence[float]) -> None:
+        if len(point) != self.VERTEX_SIZE:
+            raise DXFValueError('point requires exact {} components.'.format(self.VERTEX_SIZE))
+        self.value.extend(point)
+
+    def extend(self, points: Iterable[Sequence[float]]) -> None:
+        for point in points:
+            self.append(point)
+
+    def clear(self) -> None:
+        del self.value[:]
+
+
+def replace_tags(tags: Tags, codes: Sequence[int], packed_data: PackedTags):
+    """
+    Replace single DXF tags by packed data object.
+
+    Args:
+        tags: Tags() object
+        codes: codes to replace as tuple
+        packed_data: packed data object
+
+    """
+    try:
+        pos = tags.tag_index(codes[0])
+    except ValueError:
+        pos = len(tags)
+    tags.remove_tags(codes=codes)
+    tags.insert(pos, packed_data)

+ 558 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/repair.py

@@ -0,0 +1,558 @@
+# Purpose: repair/setup required DXF structures in existing DXF files (created by other DXF libs)
+# Created: 05.03.2016
+# Copyright (C) 2016, Manfred Moitzi
+# License: MIT License
+# --------------------------------------------------- #
+# Welcome to the place, where it gets dirty and ugly! #
+# --------------------------------------------------- #
+from typing import TYPE_CHECKING, Iterable, Optional, List
+from functools import partial
+import logging
+
+from .extendedtags import ExtendedTags
+from .tags import DXFTag, Tags
+from .const import DXFInternalEzdxfError, DXFValueError, DXFKeyError, SUBCLASS_MARKER
+logger = logging.getLogger('ezdxf')
+
+if TYPE_CHECKING:  # import forward declarations
+    from ezdxf.eztypes import Drawing
+
+
+def setup_layouts(dwg: 'Drawing'):
+    layout_dict = dwg.rootdict.get_required_dict('ACAD_LAYOUT')
+    if 'Model' not in layout_dict:  # do it only if model space is not defined
+        setup_model_space(dwg)
+        setup_paper_space(dwg)
+
+
+def setup_model_space(dwg: 'Drawing'):
+    setup_layout_space(dwg, 'Model', '*Model_Space', _MODEL_SPACE_LAYOUT_TPL)
+
+
+def setup_paper_space(dwg: 'Drawing'):
+    setup_layout_space(dwg, 'Layout1', '*Paper_Space', _PAPER_SPACE_LAYOUT_TPL)
+
+
+def setup_layout_space(dwg: 'Drawing', layout_name: str, block_name: str, tag_string: str):
+    # This is just necessary for existing DXF drawings without properly setup management structures.
+    # Layout structure is not initialized at this runtime phase
+    logger.info('creating LAYOUT structure for: {}'.format(layout_name))
+
+    def get_block_record_by_alt_names(names: Iterable[str]):
+        for name in names:
+            try:
+                brecord = dwg.block_records.get(name)
+            except DXFValueError:
+                pass
+            else:
+                return brecord
+        raise DXFKeyError
+
+    layout_dict = dwg.rootdict.get_required_dict('ACAD_LAYOUT')
+    if layout_name in layout_dict:
+        return
+    try:
+        block_record = get_block_record_by_alt_names((block_name, block_name.upper()))
+    except DXFKeyError:
+        raise NotImplementedError("'%s' block record setup not implemented, send an email to "
+                                  "<ezdxf@mozman.at> with your DXF file." % block_name)
+    real_block_name = block_record.dxf.name  # can be *Model_Space or *MODEL_SPACE
+    block_record_handle = block_record.dxf.handle
+    logger.debug('found {}: {}'.format(str(block_record), real_block_name))
+
+    try:
+        block_layout = dwg.blocks.get(real_block_name)
+        logger.debug('found {}: {}'.format(str(block_layout.block), real_block_name))
+    except DXFKeyError:
+        logger.debug('expected BLOCK: {} not found.'.format(real_block_name))
+        raise NotImplementedError("'%s' block setup not implemented, send an email to "
+                                  "<ezdxf@mozman.at> with your DXF file." % real_block_name)
+    else:
+        block_layout.set_block_record_handle(block_record_handle)   # grant valid linking
+
+    layout_handle = create_layout_tags(dwg, block_record_handle, owner=layout_dict.dxf.handle, tag_string=tag_string)
+    logger.debug('creating entry in ACAD_LAYOUT dictionary for {}'.format(layout_name))
+    layout_dict[layout_name] = layout_handle  # insert layout into the layout management table
+    block_record.dxf.layout = layout_handle  # link model space block record to layout
+
+    # rename block to standard format (*Model_Space or *Paper_Space)
+    if real_block_name != block_name:
+        logger.debug('renaming BLOCK form {} to {}'.format(block_name, real_block_name))
+        dwg.blocks.rename_block(real_block_name, block_name)
+
+
+def create_layout_tags(dwg: 'Drawing', block_record_handle: str, owner: str, tag_string: str):
+    # Problem: ezdxf was not designed to handle the absence of model/paper space LAYOUT entities
+    # Layout structure is not initialized at this runtime phase
+    logger.debug('creating LAYOUT entity for BLOCK_RECORD(#{})'.format(block_record_handle))
+    object_section = dwg.objects
+    entitydb = dwg.entitydb
+
+    tags = ExtendedTags.from_text(tag_string)
+    layout_handle = entitydb.get_unique_handle()  # create new unique handle
+    tags.replace_handle(layout_handle)  # set entity handle
+
+    entitydb.add_tags(tags)  # add layout entity to entity database
+    object_section.add_handle(layout_handle)  # add layout entity to objects section
+
+    tags.noclass.set_first(DXFTag(330, owner))  # set owner tag
+    acdblayout = tags.get_subclass('AcDbLayout')
+    acdblayout.set_first(DXFTag(330, block_record_handle))  # link to block record
+    return layout_handle
+
+
+def upgrade_to_ac1015(dwg: 'Drawing'):
+    """
+    Upgrade DXF versions AC1012 and AC1014 to AC1015.
+    """
+    def upgrade_layout_table():
+        if 'ACAD_LAYOUT' in dwg.rootdict:
+            setup_model_space(dwg)  # setup layout entity and link to proper block and block_record entities
+            setup_paper_space(dwg)  # setup layout entity and link to proper block and block_record entities
+        else:
+            raise DXFInternalEzdxfError("Table ACAD_LAYOUT should already exist in root dict.")
+
+    def upgrade_layer_table():
+        logger.debug('upgrading LAYERS table')
+        try:
+            plot_style_name_handle = dwg.rootdict.get('ACAD_PLOTSTYLENAME')  # DXFDictionaryWithDefault
+        except DXFKeyError:
+            raise DXFInternalEzdxfError("Table ACAD_PLOTSTYLENAME should already exist in root dict.")
+        set_plot_style_name_in_layers(plot_style_name_handle)
+
+        try:  # do not plot DEFPOINTS layer or AutoCAD is yelling
+            defpoints_layer = dwg.layers.get('DEFPOINTS')
+        except DXFValueError:
+            pass
+        else:
+            defpoints_layer.dxf.plot = 0
+
+    def set_plot_style_name_in_layers(plot_style_name_handle):
+        logger.debug('setting layers "plot_style_name" attribute')
+        for layer in dwg.layers:
+            layer.dxf.plot_style_name = plot_style_name_handle
+
+    def upgrade_dim_style_table():
+        logger.debug('upgrading DIMSTYLES table')
+        dim_styles = dwg.dimstyles
+        header = dim_styles._table_header
+        dim_style_table = Tags([
+            DXFTag(100, 'AcDbDimStyleTable'),
+            DXFTag(71, len(dim_styles))
+        ])
+        for entry in dim_styles:
+            dim_style_table.append(DXFTag(340, entry.dxf.handle))
+        header.subclasses.append(dim_style_table)
+
+    def upgrade_objects():
+        logger.debug('upgrading ACDBPLACEHOLDER entities in the OBJECTS section')
+        upgrade_acdbplaceholder(dwg.objects.query('ACDBPLACEHOLDER'))
+
+    def upgrade_acdbplaceholder(entities):
+        for entity in entities:
+            entity.tags.subclasses = entity.tags.subclasses[0:1]  # remove subclass AcDbPlaceHolder
+
+    # calling order is important!
+    logger.info('Upgrading drawing to DXF R2000.')
+    upgrade_layout_table()
+    upgrade_layer_table()
+    upgrade_dim_style_table()
+    upgrade_objects()
+
+    logger.debug('Setting DXF version to AC1015.')
+    dwg.dxfversion = 'AC1015'
+    dwg.header['$ACADVER'] = 'AC1015'
+
+
+def upgrade_to_ac1009(dwg: 'Drawing'):
+    """
+    Upgrade DXF versions prior to AC1009 (R12) to AC1009.
+    """
+    logger.info('Upgrading drawing to DXF R12.')
+    logger.debug('Setting DXF version to AC1009.')
+    dwg.dxfversion = 'AC1009'
+    dwg.header['$ACADVER'] = 'AC1009'
+    # as far I know, nothing else to do
+
+
+def cleanup_r12(dwg: 'Drawing'):
+    """
+    Remove unsupported sections and tables, repair tag structure.
+
+    Args:
+        dwg: Drawing() object
+
+    """
+    logger.info('Cleanup DXF R12 drawing.')
+    if dwg.dxfversion > 'AC1009':
+        return
+    for section_name in ('CLASSES', 'OBJECTS', 'THUMBNAILIMAGE', 'ACDSDATA'):  # unsupported sections for DXF R12
+        if section_name in dwg.sections:
+            logger.debug('Deleting {} section.'.format(section_name))
+            dwg.sections.delete_section(section_name)
+    if 'BLOCK_RECORDS' in dwg.sections.tables:
+        logger.debug('Deleting BLOCK_RECORDS table.')
+        del dwg.sections.tables['BLOCK_RECORDS']
+
+
+def filter_subclass_marker(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
+    """
+    Filter subclass marker from malformed DXF R12 files. (like from Leica Disto Units)
+
+    Subclass markers in R12 files, creates subclasses in ExtendedTags(), which does not work with the DXF R12 attribute
+    definitions. Other unsupported tags are not problematic, they are just ignored.
+
+    Args:
+        tagger: low level tagger
+
+    """
+    found = 0
+    for tag in tagger:
+        if tag.code == SUBCLASS_MARKER and tag.value.startswith('AcDb'):
+            found += 1
+        else:
+            yield tag
+    if found:
+        logger.debug('Filtered {} SUBCLASS marker from DXF R12 tag stream.'.format(found))
+
+
+def tag_reorder_layer(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
+    """
+    Reorder coordinates of legacy DXF Entities, for now only LINE.
+
+    Args:
+        tagger: low level tagger
+
+    """
+    logger.debug('Reordering coordinate tags for LINE entity.')
+    collector = None  # type: Optional[List]
+    for tag in tagger:
+        if tag.code == 0:
+            if collector is not None:  # stop collecting if inside of an supported entity
+                entity = collector[0].value
+                yield from COORDINATE_FIXING_TOOLBOX[entity](collector)
+                collector = None
+
+            if tag.value in COORDINATE_FIXING_TOOLBOX:
+                collector = [tag]
+                tag = None  # do not yield collected tag yet
+        else:  # tag.code != 0
+            if collector is not None:
+                collector.append(tag)
+                tag = None  # do not yield collected tag yet
+        if tag is not None:
+            yield tag
+
+
+def fix_coordinate_order(tags, codes=(10, 11)):
+    def extend_codes():
+        for code in codes:
+            yield code  # x tag
+            yield code + 10  # y tag
+            yield code + 20  # z tag
+
+    def get_coords(code):
+        # if x or y coordinate is missing, it is a DXFStructureError
+        # but here is not the location to validate the DXF structure
+        try:
+            yield coordinates[code]
+        except KeyError:
+            pass
+        try:
+            yield coordinates[code + 10]
+        except KeyError:
+            pass
+        try:
+            yield coordinates[code + 20]
+        except KeyError:
+            pass
+
+    coordinate_codes = frozenset(extend_codes())
+    coordinates = {}
+    remaining_tags = []
+    insert_pos = None
+    for tag in tags:
+        # separate tags
+        if tag.code in coordinate_codes:
+            coordinates[tag.code] = tag
+            if insert_pos is None:
+                insert_pos = tags.index(tag)
+        else:
+            remaining_tags.append(tag)
+
+    if len(coordinates) == 0:
+        # no coordinates found, this is probably a DXFStructureError,
+        # but here is not the location to validate the DXF structure,
+        # just do nothing.
+        return tags
+
+    ordered_coords = []
+    for code in codes:
+        ordered_coords.extend(get_coords(code))
+    remaining_tags[insert_pos:insert_pos] = ordered_coords
+    return remaining_tags
+
+
+COORDINATE_FIXING_TOOLBOX = {
+    'LINE': partial(fix_coordinate_order, codes=(10, 11)),
+}
+
+
+def fix_classes(dwg):
+    def remove_group_code_91():
+        logger.debug('Deleting group code 91 tags from CLASS entities for DXF Versions prior AC1018.')
+        for cls in dwg.sections.classes:
+            xtags = cls.tags
+            xtags.noclass.remove_tags((91,))
+
+    if dwg.dxfversion <= 'AC1009':  # DXF R12 and prior has no CLASSES
+        return
+
+    if dwg.dxfversion < 'AC1018':
+        # remove group code 91, which is not supported prior to AC1018
+        remove_group_code_91()
+
+
+_MODEL_SPACE_LAYOUT_TPL = """  0
+LAYOUT
+  5
+0
+330
+0
+100
+AcDbPlotSettings
+  1
+
+  2
+DWFx ePlot (XPS Compatible).pc3
+  4
+ANSI_A_(8.50_x_11.00_Inches)
+  6
+
+ 40
+5.8
+ 41
+17.8
+ 42
+5.8
+ 43
+17.8
+ 44
+215.9
+ 45
+279.4
+ 46
+0.0
+ 47
+0.0
+ 48
+0.0
+ 49
+0.0
+140
+0.0
+141
+0.0
+142
+1.0
+143
+14.53
+ 70
+11952
+ 72
+0
+ 73
+1
+ 74
+0
+  7
+
+ 75
+0
+147
+0.069
+148
+114.98
+149
+300.29
+100
+AcDbLayout
+  1
+Model
+ 70
+1
+ 71
+0
+ 10
+0.0
+ 20
+0.0
+ 11
+12.0
+ 21
+9.0
+ 12
+0.0
+ 22
+0.0
+ 32
+0.0
+ 14
+0.0
+ 24
+0.0
+ 34
+0.0
+ 15
+0.0
+ 25
+0.0
+ 35
+0.0
+146
+0.0
+ 13
+0.0
+ 23
+0.0
+ 33
+0.0
+ 16
+1.0
+ 26
+0.0
+ 36
+0.0
+ 17
+0.0
+ 27
+1.0
+ 37
+0.0
+ 76
+0
+330
+0
+"""
+
+_PAPER_SPACE_LAYOUT_TPL = """  0
+LAYOUT
+  5
+DEAD
+330
+DEAD
+100
+AcDbPlotSettings
+  1
+
+  2
+DWFx ePlot (XPS Compatible).pc3
+  4
+ANSI_A_(8.50_x_11.00_Inches)
+  6
+
+ 40
+5.8
+ 41
+17.8
+ 42
+5.8
+ 43
+17.8
+ 44
+215.9
+ 45
+279.4
+ 46
+0.0
+ 47
+0.0
+ 48
+0.0
+ 49
+0.0
+140
+0.0
+141
+0.0
+142
+1.0
+143
+1.0
+ 70
+688
+ 72
+0
+ 73
+1
+ 74
+5
+  7
+acad.ctb
+ 75
+16
+147
+1.0
+148
+0.0
+149
+0.0
+100
+AcDbLayout
+  1
+Layout1
+ 70
+1
+ 71
+1
+ 10
+-0.7
+ 20
+-0.23
+ 11
+10.3
+ 21
+8.27
+ 12
+0.0
+ 22
+0.0
+ 32
+0.0
+ 14
+0.63
+ 24
+0.8
+ 34
+0.0
+ 15
+9.0
+ 25
+7.2
+ 35
+0.0
+146
+0.0
+ 13
+0.0
+ 23
+0.0
+ 33
+0.0
+ 16
+1.0
+ 26
+0.0
+ 36
+0.0
+ 17
+0.0
+ 27
+1.0
+ 37
+0.0
+ 76
+0
+330
+DEAD
+"""

+ 164 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/tagger.py

@@ -0,0 +1,164 @@
+# Purpose: untrusted stream tag reader, tag compiler for trusted and untrusted sources
+# Created: 10.04.2016
+# Copyright (c) 2016-2018, Manfred Moitzi
+# License: MIT License
+from typing import Iterable, TextIO, Iterator
+
+from .types import DXFTag, DXFVertex, DXFBinaryTag
+from .const import DXFStructureError
+from .types import POINT_CODES, TYPE_TABLE, BINARAY_DATA
+
+
+def internal_tag_compiler(s: str) -> Iterable[DXFTag]:
+    """
+    Generates DXFTag() from trusted (internal) source - relies on
+    well formed and error free DXF format. Does not skip comment
+    tags 999.
+
+    Args:
+        s: DXF unicode string, lines separated by universal line endings '\n'
+
+    Yields: DXFTag() or inherited
+
+    """
+    assert isinstance(s, str)
+    lines = s.split('\n')
+    # split() creates an extra item, if s ends with '\n',
+    # but lines[-1] can be an empty string!!!
+    if s.endswith('\n'):
+        lines.pop()
+    pos = 0
+    count = len(lines)
+    while pos < count:
+        code = int(lines[pos])
+        value = lines[pos + 1]
+        pos += 2
+        if code in POINT_CODES:
+            # next tag; y coordinate is mandatory - internal_tag_compiler relies on well formed DXF strings
+            y = lines[pos + 1]
+            pos += 2
+            if pos < count:
+                # next tag; z coordinate just for 3d points
+                z_code = int(lines[pos])
+                z = lines[pos + 1]
+            else:  # if string s ends with a 2d point
+                z_code, z = None, 0.
+            if z_code == code + 20:  # 3d point
+                pos += 2
+                point = (float(value), float(y), float(z))
+            else:  # 2d point
+                point = (float(value), float(y))
+            yield DXFVertex(code, point)  # 2d/3d point
+        elif code in BINARAY_DATA:
+            yield DXFBinaryTag.from_string(code, value)
+        else:  # single value tag: int, float or string
+            yield DXFTag(code, TYPE_TABLE.get(code, str)(value))
+
+
+def low_level_tagger(stream: TextIO) -> Iterator[DXFTag]:
+    """
+    Generates DXFTag(code, value) tuples from a stream (untrusted external source) and does not optimize coordinates.
+    Skip comment tags 999. code is always an int and value is always an unicode string without a trailing '\n'.
+    Works with file system streams and StringIO() streams, only required feature is the readline() method.
+
+    Args:
+        stream: text stream
+
+    Yields: DXFTag()
+
+    Raises: DXFStructureError() for invalid group codes.
+
+    """
+    line = 1
+    while True:
+        try:
+            code = stream.readline()
+            value = stream.readline()  # if throws EOFError -> DXFStructureError, but should be handled in higher layers
+        except EOFError:
+            return
+        if code and value:  # StringIO(): empty strings indicates EOF
+            try:
+                code = int(code)
+            except ValueError:
+                raise DXFStructureError('Invalid group code "{}" at line {}.'.format(code, line))
+            else:
+                if code != 999:  # skip comments
+                    yield DXFTag(code, value.rstrip('\n'))
+                line += 2
+        else:
+            return
+
+
+def tag_compiler(tagger: Iterator[DXFTag]) -> Iterable[DXFTag]:
+    """
+    Compiles DXF tag values imported by low_level_tagger() into Python types.
+
+    Raises DXFStructureError() for invalid float values and invalid coordinate values.
+
+    Expects DXF coordinates written in x, y[, z] order, this is not required by the DXF standard, but nearly all CAD
+    applications write DXF coordinates that (sane) way, there are older CAD applications (namely an older QCAD version)
+    that write LINE coordinates in x1, x2, y1, y2 order, which does not work with tag_compiler(). For this cases use
+    tag_reorder_layer() from the repair module to reorder the LINE coordinates::
+
+        tag_compiler(tag_reorder_layer(low_level_tagger(stream)))
+
+    Args:
+        tagger: DXF tag generator/iterator like low_level_tagger() or skip_comments()
+
+    Yields: DXFTag() or inherited
+
+    Raises: DXFStructureError() for invalid dxf values and unexpected coordinate order.
+
+    """
+
+    def error_msg(tag):
+        return 'Invalid tag (code={code}, value="{value}") near line: {line}.'.format(line=line, code=tag.code,
+                                                                                      value=tag.value)
+
+    undo_tag = None
+    line = 0
+    while True:
+        try:
+            if undo_tag is not None:
+                x = undo_tag
+                undo_tag = None
+            else:
+                x = next(tagger)
+                line += 2
+            code = x.code
+            if code in POINT_CODES:
+                y = next(tagger)  # y coordinate is mandatory
+                line += 2
+                if y.code != code + 10:  # like 20 for base x-code 10
+                    raise DXFStructureError("Missing required y coordinate near line: {}.".format(line))
+                z = next(tagger)  # z coordinate just for 3d points
+                line += 2
+                try:
+                    if z.code == code + 20:  # it is a z-coordinate like (30, 0.0) for base x-code 10
+                        point = (float(x.value), float(y.value), float(z.value))
+                    else:
+                        point = (float(x.value), float(y.value))
+                        undo_tag = z
+                except ValueError:  # internal exception
+                    raise DXFStructureError('Invalid floating point values near line: {}.'.format(line))
+                yield DXFVertex(code, point)
+            elif code in BINARAY_DATA:
+                try:
+                    yield DXFBinaryTag.from_string(code, x.value)
+                except ValueError:
+                    raise DXFStructureError('Invalid binary data near line: {}.'.format(line))
+            else:  # just a single tag
+                try:
+                    # fast path!
+                    yield DXFTag(code, TYPE_TABLE.get(code, str)(x.value))
+                except ValueError:  # internal exception
+                    # slow path
+                    if TYPE_TABLE.get(code, str) is int:  # ProE stores int values as floats :((
+                        try:
+                            yield DXFTag(code, int(float(x.value)))
+                        except ValueError:
+                            raise DXFStructureError(error_msg(x))
+                    else:
+                        raise DXFStructureError(error_msg(x))
+        except StopIteration:
+            return

+ 319 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/tags.py

@@ -0,0 +1,319 @@
+# Created: 10.03.2011
+# Copyright (c) 2011-2018, Manfred Moitzi
+# License: MIT License
+from copy import deepcopy
+from typing import Iterable, List, TextIO, Any, TYPE_CHECKING
+
+from .const import acad_release, DXFStructureError, DXFValueError, DXFIndexError, HEADER_VAR_MARKER, STRUCTURE_MARKER
+from .types import NONE_TAG, DXFTag, is_point_code, EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR
+from .tagger import internal_tag_compiler, low_level_tagger
+
+from ezdxf.tools.codepage import toencoding
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import TagValue
+
+COMMENT_CODE = 999
+
+
+class DXFInfo(object):
+    def __init__(self):
+        self.release = 'R12'
+        self.version = 'AC1009'
+        self.encoding = 'cp1252'
+        self.handseed = '0'
+
+    def set_header_var(self, name: str, value: str) -> int:
+        if name == '$ACADVER':
+            self.version = value
+            self.release = acad_release.get(value, 'R12')
+        elif name == '$DWGCODEPAGE':
+            self.encoding = toencoding(value)
+        elif name == '$HANDSEED':
+            self.handseed = value
+        else:
+            return 0
+        return 1
+
+
+def dxf_info(stream: TextIO) -> DXFInfo:
+    info = DXFInfo()
+    tagger = low_level_tagger(stream)  # filters already comments
+    if next(tagger) != (0, 'SECTION'):  # maybe a DXF structure error, handled by later processing
+        return info
+    if next(tagger) != (2, 'HEADER'):  # no leading HEADER section like DXF R12 with only ENTITIES section
+        return info
+    tag = NONE_TAG
+    found = 0
+    while tag != (0, 'ENDSEC'):  # until end of HEADER section
+        tag = next(tagger)
+        if tag.code != HEADER_VAR_MARKER:
+            continue
+        name = tag.value
+        value = next(tagger).value
+        found += info.set_header_var(name, value)
+        if found > 2:  # all expected values collected
+            break
+    return info
+
+
+class Tags(list):
+    """
+    DXFTag() chunk as flat list.
+
+    """
+
+    @classmethod
+    def from_text(cls, text: str) -> 'Tags':
+        return cls(internal_tag_compiler(text))
+
+    def __copy__(self) -> 'Tags':
+        return self.__class__(tag.clone() for tag in self)
+
+    clone = __copy__
+
+    def get_handle(self) -> str:
+        """
+        Get DXF handle. Raises DXFValueError if handle not exists.
+
+        Returns: handle as hex-string like 'FF'
+
+        """
+        try:
+            code, handle = self[1]  # fast path  for most common cases
+        except IndexError:
+            raise DXFValueError('No handle found.')
+
+        if code == 5 or code == 105:
+            return handle
+
+        for code, handle in self:
+            if code in (5, 105):
+                return handle
+        raise DXFValueError('No handle found.')
+
+    def replace_handle(self, new_handle: str) -> None:
+        """
+        Replace existing handle.
+
+        Args:
+            new_handle: new handle as hex string
+
+        """
+        for index, tag in enumerate(self):
+            if tag.code in (5, 105):
+                self[index] = DXFTag(tag.code, new_handle)
+                return
+
+    def dxftype(self) -> str:
+        return self[0].value
+
+    def has_tag(self, code: int) -> bool:
+        """
+        Returns True if a DXF tag with group code == code is present else False.
+
+        Args:
+            code: group code as int
+
+        """
+        return any(True for tag in self if tag.code == code)
+
+    def get_first_value(self, code: int, default: 'TagValue' = DXFValueError) -> 'TagValue':
+        """
+        Returns value of first DXF tag with given group code or default if default != DXFValueError, else raises DXFValueError.
+
+        Args:
+            code: group code as int
+            default: return value for default case or raises DXFValueError
+
+        """
+        for tag in self:
+            if tag.code == code:
+                return tag.value
+        if default is DXFValueError:
+            raise DXFValueError(code)
+        else:
+            return default
+
+    def get_first_tag(self, code: int, default: 'TagValue' = DXFValueError) -> DXFTag:
+        """
+        Returns first DXF tag with given group code or default if default != DXFValueError, else raises DXFValueError.
+
+        Args:
+            code: group code as int
+            default: return value for default case or raises DXFValueError
+
+        """
+        for tag in self:
+            if tag.code == code:
+                return tag
+        if default is DXFValueError:
+            raise DXFValueError(code)
+        else:
+            return default
+
+    def find_all(self, code: int) -> List[DXFTag]:
+        """
+        Returns a list of DXF tag with given group code.
+
+        Args:
+            code: group code as int
+
+        """
+        return [tag for tag in self if tag.code == code]
+
+    def tag_index(self, code: int, start: int = 0, end: int = None) -> int:
+        """
+        Return index of first DXF tag with given group code.
+
+        Args:
+            code: group code as int
+            start: start index as int
+            end: end index as int, if None end index = len(self)
+
+        """
+        if end is None:
+            end = len(self)
+        index = start
+        while index < end:
+            if self[index].code == code:
+                return index
+            index += 1
+        raise DXFValueError(code)
+
+    def update(self, tag: DXFTag) -> None:
+        """
+        Update first existing tag with same group code as tag, raises DXFValueError if tag not exists.
+
+        """
+        index = self.tag_index(tag.code)
+        self[index] = tag
+
+    def set_first(self, tag: DXFTag) -> None:
+        """
+        Update first existing tag with group code tag.code or append tag.
+
+        """
+        try:
+            self.update(tag)
+        except DXFValueError:
+            self.append(tag)
+
+    def remove_tags(self, codes: Iterable[int]) -> None:
+        """
+        Remove all tags inplace with group codes specified in codes.
+
+        Args:
+            codes: iterable of group codes
+
+        Returns: Tags() object
+
+        """
+        self[:] = [tag for tag in self if tag.code not in frozenset(codes)]
+
+    def remove_tags_except(self, codes: Iterable[int]) -> None:
+        """
+        Remove all tags inplace except those with group codes specified in codes.
+
+        Args:
+            codes: iterable of group codes
+
+        """
+        self[:] = [tag for tag in self if tag.code in frozenset(codes)]
+
+    def collect_consecutive_tags(self, codes: Iterable[int], start: int = 0, end: int = None) -> 'Tags':
+        """
+        Collect all consecutive tags with group code in codes, start and end delimits the search range. A tag code not
+        in codes ends the process.
+
+        Args:
+            codes: iterable of group codes
+            start: start index as int
+            end: end index as int, if None end index = len(self)
+
+        Returns: collected tags as Tags() object
+
+        """
+        codes = frozenset(codes)
+        index = int(start)
+        if end is None:
+            end = len(self)
+        bag = self.__class__()
+
+        while index < end:
+            tag = self[index]
+            if tag.code in codes:
+                bag.append(tag)
+                index += 1
+            else:
+                break
+        return bag
+
+    def has_embedded_objects(self) -> bool:
+        for tag in self:
+            if tag.code == EMBEDDED_OBJ_MARKER and tag.value == EMBEDDED_OBJ_STR:
+                return True
+        return False
+
+    @classmethod
+    def strip(cls, tags: 'Tags', codes: Iterable[int]) -> 'Tags':
+        """
+        Strips all tags with group codes in codes from tags.
+
+        Args:
+            tags: iterable of DXFTags() objects
+            codes: iterable of group codes
+
+        """
+        return cls((tag for tag in tags if tag.code not in frozenset(codes)))
+
+
+def text2tags(text: str) -> Tags:
+    return Tags.from_text(text)
+
+
+def group_tags(tags: Iterable[DXFTag], splitcode: int = STRUCTURE_MARKER) -> Iterable[Tags]:
+    """
+    Group of tags starts with a SplitTag and ends before the next SplitTag.
+    A SplitTag is a tag with code == splitcode, like (0, 'SECTION') for splitcode == 0.
+
+    Args:
+        tags: iterable of DXFTag()
+        splitcode int: group code of split tag
+
+    Yields: list of DXFTag()
+
+    """
+
+    def append(tag):  # first do nothing, skip tags in front of the first split tag
+        pass
+
+    group = None
+    for tag in tags:  # has to work with iterators/generators
+        if tag.code == splitcode:
+            if group is not None:
+                yield group
+            group = Tags([tag])
+            append = group.append  # redefine append: add tags to this group
+        else:
+            append(tag)
+    if group is not None:
+        yield group
+
+
+def text_to_multi_tags(text: str, code: int = 303, size: int = 255, line_ending: str = '^J') -> List[DXFTag]:
+    text = ''.join(text).replace('\n', line_ending)
+
+    def chop():
+        start = 0
+        end = size
+        while start < len(text):
+            yield text[start:end]
+            start = end
+            end += size
+
+    return [DXFTag(code, part) for part in chop()]
+
+
+def multi_tags_to_text(tags, line_ending: str = '^J') -> str:
+    return ''.join(tag.value for tag in tags).replace(line_ending, '\n')

+ 47 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/tagwriter.py

@@ -0,0 +1,47 @@
+# Created: 13.01.2018
+# Copyright (c) 2018, Manfred Moitzi
+# License: MIT License
+from typing import Any, TextIO, TYPE_CHECKING, Union
+from .types import TAG_STRING_FORMAT
+from .tags import DXFTag
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Tags, ExtendedTags
+
+
+class TagWriter:
+    """
+    Writes DXF tags into a stream.
+
+    Args:
+        stream: text stream
+        write_handles: if False don't write handles (5, 105), use only for DXF R12 format
+
+    """
+
+    def __init__(self, stream: TextIO, write_handles: bool = True):
+        self._stream = stream
+        self.write_handles = write_handles
+
+    def write_tags(self, tags: Union['Tags', 'ExtendedTags']) -> None:
+        if self.write_handles:
+            for tag in tags:
+                self.write_tag(tag)
+        else:  # don't write handles
+            if tags[0] == (0, 'DIMSTYLE'):
+                handle_code = 105
+            else:
+                handle_code = 5
+            for tag in tags:
+                if tag.code == handle_code:
+                    continue  # skip handles in DXF R12 files, use only for DXF R12 files!!!
+                self.write_tag(tag)
+
+    def write_tag(self, tag: DXFTag) -> None:
+        self._stream.write(tag.dxfstr())
+
+    def write_tag2(self, code: int, value: Any) -> None:
+        self._stream.write(TAG_STRING_FORMAT % (code, value))
+
+    def write_str(self, s: str) -> None:
+        self._stream.write(s)

+ 218 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/types.py

@@ -0,0 +1,218 @@
+# Created: 30.04.2014
+# Copyright (c) 2014-2018, Manfred Moitzi
+# License: MIT License
+from typing import Union, Tuple, Iterable, Callable, Sequence, Any, TYPE_CHECKING
+from array import array
+from itertools import chain
+import reprlib
+
+from ezdxf.tools import encode_hex_code_string_to_bytes, byte_to_hexstr
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import TagValue
+
+TAG_STRING_FORMAT = '%3d\n%s\n'
+POINT_CODES = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 110, 111, 112, 210, 1010, 1011, 1012, 1013, 1014, 1015, 1016,
+               1017, 1018, 1019, }
+
+GENERAL_MARKER = 0
+SUBCLASS_MARKER = 100
+XDATA_MARKER = 1001
+EMBEDDED_OBJ_MARKER = 101
+APP_DATA_MARKER = 102
+EXT_DATA_MARKER = 1001
+GROUP_MARKERS = {GENERAL_MARKER, SUBCLASS_MARKER, EMBEDDED_OBJ_MARKER, APP_DATA_MARKER, EXT_DATA_MARKER}
+BINARY_FLAGS = {70, 90}
+HANDLE_CODES = {5, 105}
+POINTER_CODES = set(chain(range(320, 370), range(390, 400), (480, 481, 1005)))
+HEX_HANDLE_CODES = set(chain(HANDLE_CODES, POINTER_CODES))
+BINARAY_DATA = {310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 1004}
+EMBEDDED_OBJ_STR = 'Embedded Object'
+
+
+class DXFTag:
+    __slots__ = ('code', '_value')
+
+    def __init__(self, code: int, value: 'TagValue'):
+        self.code = code  # type: int
+        self._value = value  # type: TagValue
+
+    def __str__(self) -> str:
+        return str((self.code, self.value))
+
+    def __repr__(self) -> str:
+        return "DXFTag{}".format(str(self))
+
+    @property
+    def value(self) -> 'TagValue':
+        return self._value
+
+    def __getitem__(self, item: int):
+        return (self.code, self.value)[item]
+
+    def __iter__(self) -> Iterable:
+        yield self.code
+        yield self.value
+
+    def __eq__(self, other) -> bool:
+        return (self.code, self.value) == other
+
+    def dxfstr(self) -> str:
+        return TAG_STRING_FORMAT % (self.code, self._value)
+
+    def clone(self) -> 'DXFTag':
+        return self.__class__(self.code, self._value)
+
+
+# Special marker tag
+NONE_TAG = DXFTag(None, None)  # type: ignore
+
+
+def is_app_data_marker(tag: DXFTag) -> bool:
+    return tag.code == APP_DATA_MARKER and tag.value.startswith('{')
+
+
+def is_embedded_object_marker(tag: DXFTag) -> bool:
+    return tag.code == EMBEDDED_OBJ_MARKER and tag.value == EMBEDDED_OBJ_STR
+
+
+class DXFVertex(DXFTag):
+    __slots__ = ()
+
+    def __init__(self, code: int, value: Sequence[float]):
+        super(DXFVertex, self).__init__(code, array('d', value))  # type: ignore # array is like a tuple
+
+    def __str__(self) -> str:
+        return str(self.value)
+
+    def __repr__(self) -> str:
+        return "DXFVertex({}, {})".format(self.code, str(self))
+
+    @property
+    def value(self) -> Tuple:
+        return tuple(self._value)
+
+    def dxftags(self) -> Iterable[Tuple]:
+        c = self.code
+        return ((code, value) for code, value in zip((c, c + 10, c + 20), self.value))
+
+    def dxfstr(self) -> str:
+        return ''.join(TAG_STRING_FORMAT % tag for tag in self.dxftags())
+
+
+class DXFBinaryTag(DXFTag):
+    __slots__ = ()
+
+    def __str__(self) -> str:
+        return "({}, {})".format(self.code, self.tostring())
+
+    def __repr__(self) -> str:
+        return "DXFBinaryTag({}, {})".format(self.code, reprlib.repr(self.tostring()))
+
+    def tostring(self) -> str:  # value to string
+        return ''.join(byte_to_hexstr(b) for b in self.value)
+
+    def dxfstr(self) -> str:
+        return TAG_STRING_FORMAT % (self.code, self.tostring())
+
+    @classmethod
+    def from_string(cls, code: int, value: str):
+        return cls(code, encode_hex_code_string_to_bytes(value))
+
+
+def dxftag(code: int, value: 'TagValue') -> DXFTag:
+    """
+    DXF tag factory function.
+
+    Args:
+        code: group code
+        value: tag value
+
+    Returns: DXFTag() or inherited
+
+    """
+    if code in BINARAY_DATA:
+        return DXFBinaryTag(code, value)
+    elif code in POINT_CODES:
+        return DXFVertex(code, value)
+    else:
+        return DXFTag(code, cast_tag_value(code, value))
+
+
+def tuples_to_tags(iterable: Iterable[Tuple[int, 'TagValue']]) -> Iterable[DXFTag]:
+    for code, value in iterable:
+        if code in POINT_CODES:
+            yield DXFVertex(code, value)
+        elif code in BINARAY_DATA:
+            yield DXFBinaryTag.from_string(code, value)
+        else:
+            yield DXFTag(code, value)
+
+
+def _build_type_table(types):
+    table = {}
+    for caster, codes in types:
+        for code in codes:
+            table[code] = caster
+    return table
+
+
+TYPE_TABLE = _build_type_table([
+    # all group code < 0 are spacial tags for internal use, but not accessible by get_dxf_attrib()
+    (float, range(10, 60)),
+    (int, range(60, 100)),
+    (float, range(110, 150)),
+    (int, range(160, 170)),
+    (int, range(170, 180)),
+    (float, range(210, 240)),
+    (int, range(270, 290)),
+    (int, range(290, 300)),  # bool 1=True 0=False
+    (int, range(370, 390)),
+    (int, range(400, 410)),
+    (int, range(420, 430)),
+    (int, range(440, 460)),
+    (float, range(460, 470)),
+    (float, range(1010, 1060)),
+    (int, range(1060, 1072)),
+])
+
+
+def is_binary_data(code: int) -> bool:
+    return code in BINARAY_DATA
+
+
+def is_pointer_code(code: int) -> bool:
+    return code in POINTER_CODES
+
+
+def is_point_code(code: int) -> bool:
+    return code in POINT_CODES
+
+
+def is_point_tag(tag: Tuple) -> bool:
+    return tag[0] in POINT_CODES
+
+
+def cast_tag_value(code: int, value: 'TagValue') -> 'TagValue':
+    return TYPE_TABLE.get(code, str)(value)
+
+
+def tag_type(code: int) -> Callable:
+    return TYPE_TABLE.get(code, str)
+
+
+def strtag(tag: Union[DXFTag, Tuple[int, Any]]) -> str:
+    return TAG_STRING_FORMAT % tuple(tag)
+
+
+def get_xcode_for(code) -> int:
+    if code in HEX_HANDLE_CODES:
+        return 1005
+    if code in BINARAY_DATA:
+        return 1004
+    type_ = TYPE_TABLE.get(code, str)
+    if type_ is int:
+        return 1070
+    if type_ is float:
+        return 1040
+    return 1000

+ 160 - 0
venv/lib/python3.7/site-packages/ezdxf/lldxf/validator.py

@@ -0,0 +1,160 @@
+# Purpose: validate DXF tag structures
+# Created: 03.01.2018
+# Copyright (C) 2018, Manfred Moitzi
+# License: MIT License
+import logging
+import io
+from typing import TextIO, Iterable, List
+
+from .const import DXFStructureError, DXFError, DXFValueError, DXFAppDataError, DXFXDataError
+from .const import APP_DATA_MARKER, HEADER_VAR_MARKER, XDATA_MARKER
+from .const import INVALID_LAYER_NAME_CHARACTERS
+from .tagger import low_level_tagger
+from .types import is_embedded_object_marker, DXFTag
+
+logger = logging.getLogger('ezdxf')
+
+
+def header_validator(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
+    """
+    Checks the tag structure of the content of the header section.
+
+    Do not feed (0, 'SECTION') (2, 'HEADER') and (0, 'ENDSEC') tags!
+
+    Args:
+        tagger: generator/iterator of low level tags or compiled tags
+
+    Yields:
+        DXFTag()
+
+    Raises:
+        DXFStructureError() -> invalid group codes
+        DXFValueError() -> invalid header variable name
+    """
+    variable_name_tag = True
+    for tag in tagger:
+        if variable_name_tag:
+            if tag.code != HEADER_VAR_MARKER:
+                raise DXFStructureError('Invalid header variable tag ({0.code}, {0.value}).'.format(tag))
+            if not tag.value.startswith('$'):
+                raise DXFValueError('Invalid header variable name "{}", missing leading "$".'.format(tag.value))
+            variable_name_tag = False
+        else:
+            variable_name_tag = True
+        yield tag
+
+
+def entity_structure_validator(tags: List[DXFTag]) -> Iterable[DXFTag]:
+    """
+    Checks for valid DXF entity tag structure.
+
+    - APP DATA can not be nested and every opening tag (102, '{...') needs a closing tag (102, '}')
+    - extended group codes (>=1000) allowed before XDATA section
+    - XDATA section starts with (1001, APPID) and is always at the end of an entity
+    - XDATA section: only group code >= 1000 is allowed
+    - XDATA control strings (1002, '{') and (1002, '}') have to be balanced
+    - embedded objects may follow XDATA
+
+    XRECORD entities will not be checked.
+
+    Args:
+        tags: list of DXFTag()
+
+    Yields:
+        DXFTag()
+
+    Raises:
+        DXFAppDataError() for invalid APP DATA
+        DXFXDataError() for invalid XDATA
+    """
+    assert isinstance(tags, list)
+    dxftype = tags[0].value  # type: str
+    handle = '???'
+    app_data = False
+    xdata = False
+    xdata_list_level = 0
+    app_data_closing_tag = '}'
+    embedded_object = False
+    for tag in tags:
+        if tag.code == 5 and handle == '???':
+            handle = tag.value
+
+        if is_embedded_object_marker(tag):
+            embedded_object = True
+
+        if embedded_object:  # no further validation
+            yield tag
+            continue  # with next tag
+
+        if xdata and not embedded_object:
+            if tag.code < 1000:
+                dxftype = tags[0].value
+                raise DXFXDataError('Invalid XDATA structure in entity {}(#{}), only group code >=1000 allowed in XDATA section'.format(dxftype, handle))
+            if tag.code == 1002:
+                value = tag.value
+                if value == '{':
+                    xdata_list_level += 1
+                elif value == '}':
+                    xdata_list_level -= 1
+                else:
+                    raise DXFXDataError('Invalid XDATA control string (1002, "{}") entity {}(#{}).'.format(value, dxftype, handle))
+                if xdata_list_level < 0:  # more closing than opening tags
+                    raise DXFXDataError('Invalid XDATA structure in entity {}(#{}), unbalanced list markers, missing  (1002, "{{").'.format(dxftype, handle))
+
+        if tag.code == APP_DATA_MARKER:
+            value = tag.value
+            if value.startswith('{'):
+                if app_data:  # already in app data mode
+                    raise DXFAppDataError('Invalid APP DATA structure in entity {}(#{}), APP DATA can not be nested.'.format(dxftype, handle))
+                app_data = True
+                # 'APPID}' is also a valid closing tag
+                app_data_closing_tag = value[1:] + '}'
+            elif value == '}' or value == app_data_closing_tag:
+                if not app_data:
+                    raise DXFAppDataError('Invalid APP DATA structure in entity {}(#{}), found (102, "}}") tag without opening tag.'.format(dxftype, handle))
+                app_data = False
+                app_data_closing_tag = '}'
+            else:
+                if dxftype != 'XRECORD':  # group code 102 as non app data allowed in XRECORD
+                    raise DXFAppDataError('Invalid APP DATA structure tag (102, "{}") in entity {}(#{}).'.format(value, dxftype, handle))
+
+        # XDATA section starts with (1001, APPID) and is always at the end of an entity,
+        # since AutoCAD 2018, embedded objects may follow XDATA
+        if tag.code == XDATA_MARKER and xdata is False:
+            xdata = True
+            if app_data:
+                raise DXFAppDataError('Invalid APP DATA structure in entity {}(#{}), missing closing tag (102, "}}").'.format(dxftype, handle))
+        yield tag
+
+    if app_data:
+        raise DXFAppDataError('Invalid APP DATA structure in entity {}(#{}), missing closing tag (102, "}}").'.format(dxftype, handle))
+
+    if xdata:
+        if xdata_list_level < 0:
+            raise DXFXDataError('Invalid XDATA structure in entity {}(#{}), unbalanced list markers, missing  (1002, "{{").'.format(dxftype, handle))
+        elif xdata_list_level > 0:
+            raise DXFXDataError('Invalid XDATA structure in entity {}(#{}), unbalanced list markers, missing  (1002, "}}").'.format(dxftype, handle))
+
+
+def is_dxf_file(filename: str) -> bool:
+    with io.open(filename, errors='ignore') as fp:
+        return is_dxf_stream(fp)
+
+
+def is_dxf_stream(stream: TextIO) -> bool:
+    try:
+        reader = low_level_tagger(stream)
+        return next(reader) == (0, 'SECTION')
+    except DXFError:
+        return False
+
+
+def is_valid_layer_name(name: str) -> bool:
+    return not bool(INVALID_LAYER_NAME_CHARACTERS.intersection(set(name)))
+
+
+is_valid_name = is_valid_layer_name
+
+
+def is_adsk_special_layer(name: str) -> bool:
+    return name.upper().startswith('*ADSK_')  # special Autodesk layers starts with invalid character *

+ 47 - 0
venv/lib/python3.7/site-packages/ezdxf/math/__init__.py

@@ -0,0 +1,47 @@
+# Purpose: math and construction tools
+# Created: 27.03.2010, 2018 integrated into ezdxf
+# Copyright (c) 2010-2019 Manfred Moitzi
+# License: MIT License
+from .construct2d import is_close_points
+from .vector import Vector, Vec2, X_AXIS, Y_AXIS, Z_AXIS
+from .matrix44 import Matrix44
+from .matrix import Matrix
+from .bspline import bspline_control_frame, bspline_control_frame_approx
+from .bspline import BSpline, BSplineU, BSplineClosed, DBSpline, DBasisU, DBSplineClosed
+from .bezier import Bezier, DBezier
+from .bezier4p import Bezier4P
+from .ucs import OCS, UCS, PassTroughUCS
+from .bulge import bulge_to_arc, bulge_3_points, bulge_center, bulge_radius, arc_to_bulge
+from .arc import ConstructionArc
+from .line import ConstructionRay, ConstructionLine
+from .circle import ConstructionCircle
+from .box import ConstructionBox
+from .shape import Shape2d
+from .bbox import BoundingBox2d, BoundingBox
+from .convexhull import convex_hull_2d
+from .points import closest_point
+
+
+def xround(value: float, rounding: float = 0.) -> float:
+    """
+    Extended rounding.
+
+    `rounding` defines the rounding limit:
+        - 0 = remove fraction
+        - 0.1 = round next to x.1, x.2, ... x.0
+        - 0.25 = round next to x.25, x.50, x.75 or x.00
+        - 0.5 = round next to x.5 or x.0
+        - 1. = round to a multiple of 1: remove fraction
+        - 2. = round to a multiple of 2: xxx2, xxx4, xxx6 ...
+        - 5. = round to a multiple of 5: xxx5 or xxx0
+        - 10. = round to a multiple of 10: xx10, xx20, ...
+
+    Args:
+        value: float value to round
+        rounding: rounding limit
+
+    """
+    if rounding == 0:
+        return round(value)
+    factor = 1. / rounding
+    return round(value * factor) / factor

+ 223 - 0
venv/lib/python3.7/site-packages/ezdxf/math/arc.py

@@ -0,0 +1,223 @@
+# Copyright (c) 2018 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Tuple
+
+from .vector import Vec2
+from .bbox import BoundingBox2d
+from .construct2d import ConstructionTool, enclosing_angles
+from .circle import ConstructionCircle
+from .ucs import OCS, UCS
+import math
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex, GenericLayoutType
+    from ezdxf.eztypes import Arc as DXFArc
+
+QUARTER_ANGLES = [0, math.pi * .5, math.pi, math.pi * 1.5]
+
+
+class ConstructionArc(ConstructionTool):
+    def __init__(self,
+                 center: 'Vertex' = (0, 0),
+                 radius: float = 1,
+                 start_angle: float = 0,
+                 end_angle: float = 360,
+                 is_counter_clockwise: bool = True):
+
+        self.center = Vec2(center)
+        self.radius = radius
+        if is_counter_clockwise:
+            self.start_angle = start_angle
+            self.end_angle = end_angle
+        else:
+            self.start_angle = end_angle
+            self.end_angle = start_angle
+
+    @property
+    def start_point(self):
+        return self.center + Vec2.from_deg_angle(self.start_angle, self.radius)
+
+    @property
+    def end_point(self):
+        return self.center + Vec2.from_deg_angle(self.end_angle, self.radius)
+
+    @property
+    def bounding_box(self) -> 'BoundingBox2d':
+        bbox = BoundingBox2d((self.start_point, self.end_point))
+        bbox.extend(self.main_axis_points())
+        return bbox
+
+    def main_axis_points(self):
+        center = self.center
+        radius = self.radius
+        start = math.radians(self.start_angle)
+        end = math.radians(self.end_angle)
+        for angle in QUARTER_ANGLES:
+            if enclosing_angles(angle, start, end):
+                yield center + Vec2.from_angle(angle, radius)
+
+    def move(self, dx: float, dy: float) -> None:
+        self.center += Vec2((dx, dy))
+
+    @property
+    def start_angle_rad(self) -> float:
+        return math.radians(self.start_angle)
+
+    @property
+    def end_angle_rad(self) -> float:
+        return math.radians(self.end_angle)
+
+    @staticmethod
+    def validate_start_and_end_point(start_point: 'Vertex', end_point: 'Vertex') -> Tuple[Vec2, Vec2]:
+        start_point = Vec2(start_point)
+        end_point = Vec2(end_point)
+        if start_point == end_point:
+            raise ValueError("start- and end point has to be different points.")
+        return start_point, end_point
+
+    @classmethod
+    def from_2p_angle(cls, start_point: 'Vertex', end_point: 'Vertex', angle: float,
+                      ccw: bool = True) -> 'ConstructionArc':
+        """
+        Create arc from two points and enclosing angle. Additional precondition: arc goes by default in counter
+        clockwise orientation from start_point to end_point, can be changed by ccw=False.
+        Z-axis of start_point and end_point has to be 0 if given.
+
+        Args:
+            start_point: start point (x, y) as args accepted by Vec2()
+            end_point: end point (x, y) as args accepted by Vec2()
+            angle: enclosing angle in degrees
+            ccw: counter clockwise direction True/False
+
+        """
+        start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
+        angle = math.radians(angle)
+        if angle == 0:
+            raise ValueError("angle can not be 0.")
+        if ccw is False:
+            start_point, end_point = end_point, start_point
+        alpha2 = angle / 2.
+        distance = end_point.distance(start_point)
+        distance2 = distance / 2.
+        radius = distance2 / math.sin(alpha2)
+        height = distance2 / math.tan(alpha2)
+        mid_point = end_point.lerp(start_point, factor=.5)
+
+        distance_vector = end_point - start_point
+        height_vector = distance_vector.orthogonal().normalize(height)
+        center = mid_point + height_vector
+
+        return ConstructionArc(
+            center=center,
+            radius=radius,
+            start_angle=(start_point - center).angle_deg,
+            end_angle=(end_point - center).angle_deg,
+            is_counter_clockwise=True,
+        )
+
+    @classmethod
+    def from_2p_radius(cls, start_point: 'Vertex', end_point: 'Vertex', radius: float, ccw: bool = True,
+                       center_is_left: bool = True) -> 'ConstructionArc':
+        """
+        Create arc from two points and arc radius. Additional precondition: arc goes by default in counter clockwise
+        orientation from start_point to end_point can be changed by ccw=False.
+        Z-axis of start_point and end_point has to be 0 if given.
+
+        The parameter *center_is_left* defines if the center of the arc is left or right of the line *start point* ->
+        *end point*. Parameter ccw=False swaps start- and end point, which inverts the meaning of center_is_left.
+
+        Args:
+            start_point: start point (x, y) as args accepted by Vec2()
+            end_point: end point (x, y) as args accepted by Vec2()
+            radius: arc radius
+            ccw: counter clockwise direction True/False
+            center_is_left: center point of arc is left of line SP->EP if True, else on the right side of this line
+
+        """
+        start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
+        radius = float(radius)
+        if radius <= 0:
+            raise ValueError("radius has to be > 0.")
+        if ccw is False:
+            start_point, end_point = end_point, start_point
+
+        mid_point = end_point.lerp(start_point, factor=.5)
+        distance = end_point.distance(start_point)
+        distance2 = distance / 2.
+        height = math.sqrt(radius ** 2 - distance2 ** 2)
+        center = mid_point + (end_point - start_point).orthogonal(ccw=center_is_left).normalize(height)
+
+        return ConstructionArc(
+            center=center,
+            radius=radius,
+            start_angle=(start_point - center).angle_deg,
+            end_angle=(end_point - center).angle_deg,
+            is_counter_clockwise=True,
+        )
+
+    @classmethod
+    def from_3p(cls, start_point: 'Vertex', end_point: 'Vertex', def_point: 'Vertex',
+                ccw: bool = True) -> 'ConstructionArc':
+        """
+        Create arc from three points. Additional precondition: arc goes in counter clockwise
+        orientation from start_point to end_point. Z-axis of start_point, end_point and def_point has to be 0 if given.
+
+        Args:
+            start_point: start point (x, y)  as args accepted by Vec2()
+            end_point: end point (x, y) as args accepted by Vec2()
+            def_point: additional definition point as (x, y)  as args accepted by Vec2()
+            ccw: counter clockwise direction True/False
+
+        """
+        start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
+        def_point = Vec2(def_point)
+        if def_point == start_point or def_point == end_point:
+            raise ValueError("def point has to be different to start- and end point")
+
+        circle = ConstructionCircle.from_3p(start_point, end_point, def_point)
+        center = Vec2(circle.center)
+        return ConstructionArc(
+            center=center,
+            radius=circle.radius,
+            start_angle=(start_point - center).angle_deg,
+            end_angle=(end_point - center).angle_deg,
+            is_counter_clockwise=ccw,
+        )
+
+    def add_to_layout(self, layout: 'GenericLayoutType', ucs: UCS = None, dxfattribs: dict = None) -> 'DXFArc':
+        """
+        Add arc as DXF entity to a layout.
+
+        Supports 3D arcs by using an UCS. An ConstructionArc is always defined in the xy-plane, but by using an arbitrary UCS, the
+        arc can be placed in 3D space, automatically OCS transformation included.
+
+        Args:
+            layout: destination layout (model space, paper space or block)
+            ucs: arc properties transformation from ucs to ocs
+            dxfattribs: usual DXF attributes supported by ARC
+
+        Returns: DXF ConstructionArc() object
+
+        """
+
+        if ucs is not None:
+            if dxfattribs is None:
+                dxfattribs = {}
+            dxfattribs['extrusion'] = ucs.uz
+            ocs = OCS(ucs.uz)
+            wcs_center = ucs.to_wcs(self.center)
+            ocs_center = ocs.from_wcs(wcs_center)
+            arc = self.__class__(radius=self.radius)
+            arc.center = ocs_center
+            arc.start_angle = ucs.to_ocs_angle_deg(self.start_angle)
+            arc.end_angle = ucs.to_ocs_angle_deg(self.end_angle)
+        else:
+            arc = self
+
+        return layout.add_arc(
+            center=arc.center,
+            radius=arc.radius,
+            start_angle=arc.start_angle,
+            end_angle=arc.end_angle,
+            dxfattribs=dxfattribs,
+        )

+ 88 - 0
venv/lib/python3.7/site-packages/ezdxf/math/bbox.py

@@ -0,0 +1,88 @@
+# Created: 27.01.2019
+# Copyright (c) 2019, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, Tuple
+from .vector import Vector, Vec2
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+class BoundingBox:
+    def __init__(self, vertices: Iterable['Vertex']):
+        self.extmin, self.extmax = extends(vertices)
+
+    def inside(self, vertex: 'Vertex') -> bool:
+        x, y, z = Vector(vertex).xyz
+        xmin, ymin, zmin = self.extmin.xyz
+        xmax, ymax, zmax = self.extmax.xyz
+        return (xmin <= x <= xmax) and (ymin <= y <= ymax) and (zmin <= z <= zmax)
+
+    def extend(self, vertices: Iterable['Vertex']) -> None:
+        v = [self.extmin, self.extmax]
+        v.extend(vertices)
+        self.extmin, self.extmax = extends(v)
+
+
+def extends(vertices: Iterable['Vertex']) -> Tuple[Vector, Vector]:
+    minx, miny, minz = None, None, None
+    maxx, maxy, maxz = None, None, None
+    for v in vertices:
+        v = Vector(v)
+        if minx is None:
+            minx, miny, minz = v.xyz
+            maxx, maxy, maxz = v.xyz
+        else:
+            x, y, z = v.xyz
+            if x < minx:
+                minx = x
+            elif x > maxx:
+                maxx = x
+            if y < miny:
+                miny = y
+            elif y > maxy:
+                maxy = y
+            if z < minz:
+                minz = z
+            elif z > maxz:
+                maxz = z
+    return Vector(minx, miny, minz), Vector(maxx, maxy, maxz)
+
+
+class BoundingBox2d:
+    def __init__(self, vertices: Iterable['Vertex']):
+        self.extmin, self.extmax = extends2d(vertices)
+
+    def inside(self, vertex: 'Vertex') -> bool:
+        v = Vec2(vertex)
+        min_ = self.extmin
+        max_ = self.extmax
+        return (min_.x <= v.x <= max_.x) and (min_.y <= v.y <= max_.y)
+
+    def extend(self, vertices: Iterable['Vertex']) -> None:
+        v = [self.extmin, self.extmax]
+        v.extend(vertices)
+        self.extmin, self.extmax = extends(v)
+
+
+def extends2d(vertices: Iterable['Vertex']) -> Tuple[Vec2, Vec2]:
+    minx, miny = None, None
+    maxx, maxy = None, None
+    for v in vertices:
+        v = Vec2(v)
+        x, y = v.x, v.y
+        if minx is None:
+            minx = x
+            maxx = x
+            miny = y
+            maxy = y
+        else:
+            if x < minx:
+                minx = x
+            elif x > maxx:
+                maxx = x
+            if y < miny:
+                miny = y
+            elif y > maxy:
+                maxy = y
+    return Vec2((minx, miny)), Vec2((maxx, maxy))

+ 194 - 0
venv/lib/python3.7/site-packages/ezdxf/math/bezier.py

@@ -0,0 +1,194 @@
+# Purpose: general bezier curve
+# Created: 26.03.2010
+# Copyright (c) 2010-2018 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, List, Iterable, Tuple, Dict
+from ezdxf.math.vector import Vector
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+"""
+
+Bezier curves
+=============
+
+https://www.cl.cam.ac.uk/teaching/2000/AGraphHCI/SMEG/node3.html
+
+A Bezier curve is a weighted sum of n+1 control points,  P0, P1, ..., Pn, where the weights are the Bernstein 
+polynomials. 
+
+The Bezier curve of order n+1 (degree n) has n+1 control points. These are the first three orders of Bezier curve 
+definitions. 
+
+(75) linear P(t) = (1-t)*P0 + t*P1
+(76) quadratic P(t) = (1-t)^2*P0 + 2*(t-1)*t*P1 + t^2*P2
+(77) cubic P(t) = (1-t)^3*P0 + 3*(t-1)^2*t*P1 + 3*(t-1)*t^2*P2 + t^3*P3
+
+Ways of thinking about Bezier curves
+------------------------------------
+
+There are several useful ways in which you can think about Bezier curves. Here are the ones that I use.
+
+Linear interpolation
+~~~~~~~~~~~~~~~~~~~~
+
+Equation (75) is obviously a linear interpolation between two points. Equation (76) can be rewritten as a linear 
+interpolation between linear interpolations between points.
+
+Weighted average
+~~~~~~~~~~~~~~~~
+
+A Bezier curve can be seen as a weighted average of all of its control points. Because all of the weights are 
+positive, and because the weights sum to one, the Bezier curve is guaranteed to lie within the convex hull of its 
+control points.
+    
+Refinement of the control polygon
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A Bezier curve can be seen as some sort of refinement of the polygon made by connecting its control points in order. 
+The Bezier curve starts and ends at the two end points and its shape is determined by the relative positions of the 
+n-1 other control points, although it will generally not pass through these other control points. The tangent vectors 
+at the start and end of the curve pass through the end point and the immediately adjacent point.
+
+Continuity
+----------
+
+You should note that each Bezier curve is independent of any other Bezier curve. If we wish two Bezier curves to join 
+with any type of continuity, then we must explicitly position the control points of the second curve so that they bear 
+the appropriate relationship with the control points in the first curve.
+
+Any Bezier curve is infinitely differentiable within itself, and is therefore continuous to any degree.
+
+"""
+
+
+class Bezier:
+    """
+    A general Bezier curve.
+
+    Accepts 2d points as definition points, but output ist always 3d (z-axis is 0).
+
+    """
+
+    def __init__(self, defpoints: Iterable['Vertex']):
+        self._defpoints = [Vector(p) for p in defpoints]  # type: List[Vector]
+
+    @property
+    def control_points(self) -> List[Vector]:
+        return self._defpoints
+
+    def approximate(self, segments: int = 20) -> Iterable[Vector]:
+        step = 1.0 / float(segments)
+        for point_index in range(segments + 1):
+            yield self.point(point_index * step)
+
+    def point(self, t: float) -> Vector:
+        """
+        Returns point at BezierCurve(t) as tuple (x, y, z)
+
+        Args:
+            t: parameter in range [0, 1]
+
+        Returns: Vector(x, y, z)
+
+        """
+        if t < 0. or t > 1.:
+            raise ValueError('parameter t in range [0, 1]')
+        if (1.0 - t) < 5e-6:
+            t = 1.0
+        point = [0., 0., 0.]
+        defpoints = self._defpoints
+        len_defpoints = len(defpoints)
+
+        for axis in (0, 1, 2):
+            for i in range(len_defpoints):
+                bsf = bernstein_basis(len_defpoints - 1, i, t)
+                point[axis] += bsf * defpoints[i][axis]
+        return Vector(point)
+
+
+class DBezier(Bezier):
+    """
+    Calculate the Points and Derivative of a Bezier curve.
+
+    """
+
+    def point(self, t: float) -> Tuple[Vector, Vector, Vector]:
+        """
+        Returns (point, derivative1, derivative2) at BezierCurve(t)
+
+        Args:
+            t: parameter in range [0, 1]
+
+        Returns: (point, derivative1, derivative2)
+            point -- Vector(x, y, z)
+            derivative1 -- Vector(x, y, z)
+            derivative2 -- Vector(x, y, z)
+
+        """
+        if t < 0. or t > 1.:
+            raise ValueError('parameter t in range [0, 1]')
+
+        if (1.0 - t) < 5e-6:
+            t = 1.0
+        defpoints = self._defpoints
+        npts = len(defpoints)
+        npts0 = npts - 1
+        point = [0., 0., 0.]
+        d1 = [0., 0., 0.]
+        d2 = [0., 0., 0.]
+        for axis in (0, 1, 2):
+            if t == 0.0:
+                d1[axis] = npts0 * (defpoints[1][axis] - defpoints[0][axis])
+                d2[axis] = npts0 * (npts0 - 1) * (defpoints[0][axis] - 2. * defpoints[1][axis] + defpoints[2][axis])
+            for i in range(len(defpoints)):
+                tempbasis = bernstein_basis(npts0, i, t)
+                point[axis] += tempbasis * defpoints[i][axis]
+                if 0.0 < t < 1.0:
+                    d1[axis] += ((i - npts0 * t) / (t * (1. - t))) * tempbasis * defpoints[i][axis]
+                    temp1 = (i - npts0 * t) ** 2
+                    temp2 = temp1 - npts0 * t ** 2 - i * (1. - 2. * t)
+                    d2[axis] += (temp2 / (t ** 2 * (1. - t) ** 2)) * tempbasis * defpoints[i][axis]
+                if t == 1.0:
+                    d1[axis] = npts0 * (defpoints[npts0][axis] - defpoints[npts0 - 1][axis])
+                    d2[axis] = npts0 * (npts0 - 1) * (defpoints[npts0][axis] - 2 * defpoints[npts0 - 1][axis] +
+                                                      defpoints[npts0 - 2][axis])
+        return Vector(point), Vector(d1), Vector(d2)
+
+
+def bernstein_basis(n: int, i: int, t: float) -> float:
+    # handle the special cases to avoid domain problem with pow
+    if t == 0.0 and i == 0:
+        ti = 1.0
+    else:
+        ti = pow(t, i)
+    if n == i and t == 1.0:
+        tni = 1.0
+    else:
+        tni = pow((1.0 - t), (n - i))
+    Ni = factorial(n) / (factorial(i) * factorial(n - i))
+    return Ni * ti * tni
+
+
+class _Factorial:
+    _values = {0: 1.0}  # type: Dict[int, float]
+
+    def __init__(self, maxvalue: int = 33):
+        value = 1.
+        for i in range(1, maxvalue):
+            value *= i
+            self._values[i] = value
+
+    def get(self, value: int) -> float:
+        value = int(value)
+        try:
+            return self._values[value]
+        except KeyError:
+            result = self.get(value - 1) * value
+            self._values[value] = result
+            return result
+
+
+_factorial = _Factorial()
+factorial = _factorial.get

+ 159 - 0
venv/lib/python3.7/site-packages/ezdxf/math/bezier4p.py

@@ -0,0 +1,159 @@
+# Purpose: Bezier Curve optimized for 4 control points
+# Created: 26.03.2010
+# Copyright (c) 2010-2018 Manfred Moitzi
+# License: MIT License
+from typing import List, TYPE_CHECKING, Iterable
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+def check_if_in_valid_range(t: float):
+    if not (0 <= t <= 1.):
+        raise ValueError("t not in range [0 to 1]")
+
+
+class Bezier4P:
+    """
+    Implements an optimized Cubic Bezier Curve with 4 control points.
+
+    Special behavior: 2d in -> 2d out and 3d in -> 3d out!
+
+    """
+
+    def __init__(self, control_points: List['Vertex']):
+        if len(control_points) == 4:
+            is3d = any(len(p) > 2 for p in control_points)
+            self.math = D3D if is3d else D2D
+            self._cpoints = [self.math.tovector(vector) for vector in control_points]
+        else:
+            raise ValueError("Four control points required.")
+
+    @property
+    def control_points(self) -> List['Vertex']:
+        return self._cpoints
+
+    def tangent(self, t: float) -> 'Vertex':
+        """
+        Calculate tangent at parameter t [0, 1].
+
+        Args:
+            t: curve position in the range [0, 1]
+
+        Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.
+
+        """
+        check_if_in_valid_range(t)
+        return self._get_curve_tangent(t)
+
+    def point(self, t: float) -> 'Vertex':
+        """
+        Calculate curve point at parameter t [0, 1].
+
+        Args:
+            t: curve position in the range [0, 1]
+
+        Returns: (x, y, z) tuple
+
+        """
+        check_if_in_valid_range(t)
+        return self._get_curve_point(t)
+
+    def approximate(self, segments: int) -> Iterable['Vertex']:
+        delta_t = 1. / segments
+        yield self._cpoints[0]
+        for segment in range(1, segments):
+            yield self.point(delta_t * segment)
+        yield self._cpoints[3]
+
+    def _get_curve_point(self, t: float) -> 'Vertex':
+        """
+        Calculate curve point at parameter t [0, 1].
+
+        Returns: (x, y, z) tuple
+
+        """
+        b1, b2, b3, b4 = self._cpoints
+        one_minus_t = 1. - t
+        m = self.math
+        point = m.vmul_scalar(b1, one_minus_t ** 3)
+        point = m.vadd(point, m.vmul_scalar(b2, 3. * one_minus_t ** 2 * t))
+        point = m.vadd(point, m.vmul_scalar(b3, 3. * one_minus_t * t ** 2))
+        point = m.vadd(point, m.vmul_scalar(b4, t ** 3))
+        return tuple(point)
+
+    def _get_curve_tangent(self, t: float) -> 'Vertex':
+        """
+        Calculate tangent at parameter t [0, 1]. Implementation optimized for 4 control points.
+
+        Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.
+
+        """
+        b1, b2, b3, b4 = self._cpoints
+        m = self.math
+        tangent = m.vmul_scalar(b1, -3. * (1. - t) ** 2)
+        tangent = m.vadd(tangent, m.vmul_scalar(b2, 3. * (1. - 4. * t + 3. * t ** 2)))
+        tangent = m.vadd(tangent, m.vmul_scalar(b3, 3. * t * (2. - 3. * t)))
+        tangent = m.vadd(tangent, m.vmul_scalar(b4, 3. * t ** 2))
+        return tuple(tangent)
+
+    def approximated_length(self, segments: int = 100) -> float:
+        length = 0.
+        point_gen = self.approximate(segments)
+        prev_point = next(point_gen)
+        distance = self.math.distance
+        for point in point_gen:
+            length += distance(prev_point, point)
+            prev_point = point
+        return length
+
+
+class D2D:
+    @staticmethod
+    def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
+        """ Add vectors """
+        return vector1[0] + vector2[0], vector1[1] + vector2[1]
+
+    @staticmethod
+    def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
+        """ Mul vector with scalar """
+        return vector[0] * scalar, vector[1] * scalar
+
+    @staticmethod
+    def tovector(vector: 'Vertex') -> 'Vertex':
+        """ Return a 2d point """
+        return float(vector[0]), float(vector[1])
+
+    @staticmethod
+    def distance(point1: 'Vertex', point2: 'Vertex') -> float:
+        """ calc distance between two 2d points """
+        return ((point1[0] - point2[0]) ** 2 +
+                (point1[1] - point2[1]) ** 2) ** 0.5
+
+
+class D3D:
+    @staticmethod
+    def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
+        """ Add vectors """
+        return vector1[0] + vector2[0], vector1[1] + vector2[1], vector1[2] + vector2[2]
+
+    @staticmethod
+    def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
+        """ Mul vector with scalar """
+        return vector[0] * scalar, vector[1] * scalar, vector[2] * scalar
+
+    @staticmethod
+    def tovector(vector: 'Vertex') -> 'Vertex':
+        """ Return a 3d point """
+        try:
+            z = float(vector[2])
+        except IndexError:
+            z = 0.
+        return float(vector[0]), float(vector[1]), z
+
+    @staticmethod
+    def distance(point1: 'Vertex', point2: 'Vertex') -> float:
+        """Calc distance between two 3d points """
+        return ((point1[0] - point2[0]) ** 2 +
+                (point1[1] - point2[1]) ** 2 +
+                (point1[2] - point2[2]) ** 2) ** 0.5

+ 194 - 0
venv/lib/python3.7/site-packages/ezdxf/math/box.py

@@ -0,0 +1,194 @@
+from typing import List, Sequence, TYPE_CHECKING, Iterable, Tuple
+import math
+from .vector import Vec2
+from .bbox import BoundingBox2d
+from .line import ConstructionLine
+from .construct2d import left_of_line, ConstructionTool
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+class ConstructionBox(ConstructionTool):
+    def __init__(self, center: 'Vertex' = (0, 0), width: float = 1, height: float = 1, angle: float = 0):
+        self._center = Vec2(center)
+        self._width = abs(width)  # type: float
+        self._height = abs(height)  # type: float
+        self._angle = angle  # type: float  # in degrees
+        self._corners = None  # type: Tuple[Vec2, Vec2, Vec2, Vec2]
+        self._tainted = True
+
+    @classmethod
+    def from_points(cls, p1: 'Vertex', p2: 'Vertex') -> 'ConstructionBox':
+        p1 = Vec2(p1)
+        p2 = Vec2(p2)
+        width = abs(p2.x - p1.x)
+        height = abs(p2.y - p1.y)
+        center = p1.lerp(p2)
+        return cls(center=center, width=width, height=height)
+
+    def update(self) -> None:
+        if not self._tainted:
+            return
+        center = self.center
+        w2 = Vec2.from_deg_angle(self._angle, self._width / 2.)
+        h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.)
+        self._corners = (
+            center - w2 - h2,  # lower left
+            center + w2 - h2,  # lower right
+            center + w2 + h2,  # upper right
+            center - w2 + h2,  # upper left
+        )
+        self._tainted = False
+
+    @property
+    def bounding_box(self) -> BoundingBox2d:
+        return BoundingBox2d(self.corners)
+
+    @property
+    def center(self) -> Vec2:
+        return self._center
+
+    @center.setter
+    def center(self, c: 'Vertex') -> None:
+        self._center = Vec2(c)
+        self._tainted = True
+
+    @property
+    def width(self) -> float:
+        return self._width
+
+    @width.setter
+    def width(self, w: float) -> None:
+        self._width = abs(w)
+        self._tainted = True
+
+    @property
+    def height(self) -> float:
+        return self._height
+
+    @height.setter
+    def height(self, h: float) -> None:
+        self._height = abs(h)
+        self._tainted = True
+
+    @property
+    def incircle_radius(self) -> float:
+        return min(self._width, self._height) / 2.
+
+    @property
+    def circumcircle_radius(self) -> float:
+        return math.hypot(self._width, self._height) / 2.
+
+    @property
+    def angle(self) -> float:
+        return self._angle
+
+    @angle.setter
+    def angle(self, a: float) -> None:
+        self._angle = a
+        self._tainted = True
+
+    @property
+    def corners(self) -> Sequence[Vec2]:
+        self.update()
+        return self._corners
+
+    def __iter__(self) -> Iterable[Vec2]:
+        return iter(self.corners)
+
+    def __getitem__(self, corner) -> Vec2:
+        return self.corners[corner]
+
+    def __repr__(self) -> str:
+        return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(self)
+
+    def move(self, dx: float, dy: float) -> None:
+        self.center += Vec2((dx, dy))
+
+    def expand(self, dw: float, dh: float) -> None:
+        self.width += dw
+        self.height += dh
+
+    def scale(self, sx: float, sy: float) -> None:
+        self.width *= sx
+        self.height *= sy
+
+    def rotate(self, angle: float) -> None:
+        self.angle += angle
+
+    def is_inside(self, point: 'Vertex') -> bool:
+        point = Vec2(point)
+        delta = self.center - point
+        if math.isclose(self.angle, 0.):  # fast path for horizontal rectangles
+            return abs(delta.x) <= (self._width / 2.) and abs(delta.y) <= (self._height / 2.)
+        else:
+            distance = delta.magnitude
+            if distance > self.circumcircle_radius:
+                return False
+            elif distance <= self.incircle_radius:
+                return True
+            else:
+                # inside if point is "left of line" of all border lines.
+                p1, p2, p3, p4 = self.corners
+                return all(
+                    (left_of_line(point, a, b, online=True) for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)])
+                )
+
+    def is_any_corner_inside(self, other: 'ConstructionBox') -> bool:
+        return any(self.is_inside(p) for p in other.corners)
+
+    def is_overlapping(self, other: 'ConstructionBox') -> bool:
+        distance = (self.center - other.center).magnitude
+        max_distance = self.circumcircle_radius + other.circumcircle_radius
+        if distance > max_distance:
+            return False
+        min_distance = self.incircle_radius + other.incircle_radius
+        if distance <= min_distance:
+            return True
+
+        if self.is_any_corner_inside(other):
+            return True
+        if other.is_any_corner_inside(self):
+            return True
+        # no corner inside of any box, maybe crossing boxes?
+        # check intersection of diagonals
+        c1, c2, c3, c4 = self.corners
+        diag1 = ConstructionLine(c1, c3)
+        diag2 = ConstructionLine(c2, c4)
+
+        t1, t2, t3, t4 = other.corners
+        test_diag = ConstructionLine(t1, t3)
+        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
+            return True
+        test_diag = ConstructionLine(t2, t4)
+        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
+            return True
+
+        return False
+
+    def border_lines(self) -> Sequence[ConstructionLine]:
+        p1, p2, p3, p4 = self.corners
+        return (
+            ConstructionLine(p1, p2),
+            ConstructionLine(p2, p3),
+            ConstructionLine(p3, p4),
+            ConstructionLine(p4, p1),
+        )
+
+    def intersect(self, line: ConstructionLine) -> List[Vec2]:
+        """
+        Returns 0, 1 or 2 intersection points between `line` and `TextBox` border lines.
+
+        Args:
+            line: line to intersect with border lines
+
+        Returns: list of intersection points
+
+        """
+        result = set()
+        for border_line in self.border_lines():
+            p = line.intersect(border_line)
+            if p is not None:
+                result.add(p)
+        return sorted(result)

+ 861 - 0
venv/lib/python3.7/site-packages/ezdxf/math/bspline.py

@@ -0,0 +1,861 @@
+# Created: 2012.01.03
+# Copyright (c) 2012-2018 Manfred Moitzi
+# License: MIT License
+"""
+B-Splines
+=========
+
+https://www.cl.cam.ac.uk/teaching/2000/AGraphHCI/SMEG/node4.html
+
+n + 1 ... number of control points P_1, P_2, ..., P_{n+1} or P_0, P_1, ..., P_n
+k ... order of the B-spline, 2 <= k <= n + 1
+degree ... k - 1
+
+B-splines are a more general type of curve than Bezier curves. In a B-spline each control point is associated with a 
+basis function.
+
+(87) P(t) = sum {i=1}^{n+1} N_{i,k}(t) P_i, t_min <= t < t_max
+
+There are n + 1 control points,  P_1, P_2, ..., P_{n+1}. The N_{i,k} basis functions are of order k(degree k-1). 
+k must be at least 2 (linear), and can be no more than n+1 (the number of control points). The important point here is 
+that the order of the curve (linear, quadratic, cubic,...) is therefore not dependent on the number of control points 
+(which it is for Bezier curves, where k must always equal n+1).
+
+Equation (87) defines a piecewise continuous function. A knot vector,  (t_1, t_2, ..., t_{k+(n+1)}), must be specified. 
+This determines the values of t at which the pieces of curve join, like knots joining bits of string. It is necessary 
+that:
+
+(88)  t_i <= t_{i+1}, for all i
+
+The N_{i,k} depend only on the value of k and the values in the knot vector. N is defined recursively as:
+
+(89) N_{i,1}(t)	= 1 for t_i <= t < t_{i+1}; 0 otherwise
+     N_{i,k}(t)	= (t-t_i) / ({t_{i+k-1} - t_i}) * N_{i,k-1}(t) + (t_{i+k}-t) / (t_{i+k} - t_{i+1}) * N_{i+1,k-1}(t)
+
+This is essentially a modified version of the idea of taking linear interpolations of linear interpolations of linear 
+interpolations ... n
+
+
+The Knot Vector
+---------------
+
+The above explanation shows that the knot vector is very important. The knot vector can, by its definition, be any 
+sequence of numbers provided that each one is greater than or equal to the preceding one. Some types of knot vector are 
+more useful than others. Knot vectors are generally placed into one of three categories: uniform, open uniform, and 
+non-uniform.
+
+Uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~
+
+These are knot vectors for which 
+
+(90) t_{i+1} - t_i = constant, for all i
+
+e.g. [1, 2, 3, 4, 5, 6, 7, 8], [0, .25, .5, .75, 1.]
+
+Open Uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+These are uniform knot vectors which have k equal knot values at each end
+ 
+(91) t_i = t_1,  i <= k
+     t_{i+1} - t_i = constant, k <= i < n+2
+     t_i = t_{k+(n+1)}, i >= n + 2
+
+e.g. [0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4] for k=4, 
+     [1, 1, 1, 2, 3, 4, 5, 6, 6, 6] for k=3
+     [.1, .1, .1, .1, .1, .3, .5, .7, .7, .7, .7, .7] for k=5
+
+Non-uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This is the general case, the only constraint being the standard  t_i <= t_{i+1}, for all i (Equations 88). 
+
+e.g. [1, 3, 7, 22, 23, 23, 49, 50, 50]
+     [1, 1, 1, 2, 2, 3, 4, 5, 6, 6, 6, 7, 7, 7]
+     [.2, .7, .7, .7, 1.2, 1.2, 2.9, 3.6]
+
+The shapes of the N_{i,k} basis functions are determined entirely by the relative spacing between the knots.
+ 
+    scaling: t_i' = alpha * t_i, for all i
+    translating t_i'= t_i + delta t, for all i
+    
+The knot vector has no effect on the shapes of the N_{i,k}.
+
+The above gives a description of the various types of knot vector but it doesn't really give you any insight into how 
+the knot vector determines the shape of the curve. The following subsections look at the different types of knot vector 
+in more detail. However, the best way to get to feel for these is to derive and draw the basis functions yourself.
+
+Uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~
+
+For simplicity, let t_i = i (this is allowable given that the scaling or translating the knot vector has no effect on 
+the shapes of the N_{i,k}). The knot vector thus becomes  [1,2,3, ... ,k+(n+1)] and equation (89) simplifies to:
+
+(92) N_{i,1}(t)	= 1 for t_i <= t < t_{i+1}; 0 otherwise
+     N_{i,k}(t)	= (t-i)(k-1) * N_{i,k-1}(t) + (i+k-t)/ (k-1) * N_{i+1,k-1}(t)
+
+Things you can change about a uniform B-spline
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+With a uniform B-spline, you obviously cannot change the basis functions (they are fixed because all the knots are 
+equi-spaced). However you can alter the shape of the curve by modifying a number of things:
+
+Moving control points:
+
+Moving the control points obviously changes the shape of the curve.
+
+Multiple control points:
+
+Sticking two adjacent control points on top of one another causes the curve to pass closer to that point. Stick enough 
+adjacent control points on top of one another and you can make the curve pass through that point.
+
+Order:
+
+Increasing the order k increases the continuity of the curve at the knots, increases the smoothness of the curve, and 
+tends to move the curve farther from its defining polygon.
+
+Joining the ends:
+
+You can join the ends of the curve to make a closed loop. Say you have M points,  P_1, ... P_M. You want a closed 
+B-spline defined by these points. For a given order, k, you will need M+(k-1) control points (repeating the first k-1 
+points):  P_1, ... P_M, P_1, ..., P_{k-1}. Your knot vector will thus have M+2k-1 uniformly spaced knots.
+
+Open Uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The previous section intimated that uniform B-splines can be used to describe closed curves: all you have to do is join 
+the ends as described above. If you do not want a closed curve, and you use a uniform knot vector, you find that you 
+need to specify control points at each end of the curve which the curve doesn't go near.
+
+If you wish your B-spline to start and end at your first and last control points then you need an open uniform knot 
+vector. The only difference between this and the uniform knot vector being that the open uniform version has k equal 
+knots at each end.
+
+An order k open uniform B-spline with n+1=k points is the Bezier curve of order k.
+
+Non-uniform Knot Vector
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Any B-spline whose knot vector is neither uniform nor open uniform is non-uniform. Non-uniform knot vectors allow any 
+spacing of the knots, including multiple knots (adjacent knots with the same value). We need to know how this 
+non-uniform spacing affects the basis functions in order to understand where non-uniform knot vectors could be useful. 
+
+It transpires that there are only three cases of any interest: 
+
+    1. multiple knots (adjacent knots equal)
+    2. adjacent knots more closely spaced than the next knot in the vector
+    3. adjacent knots less closely spaced than the next knot in the vector 
+    
+Obviously, case (3) is simply case (2) turned the other way round.
+
+Multiple knots:
+
+A multiple knot reduces the degree of continuity at that knot value. Across a normal knot the continuity is Ck-2. Each 
+extra knot with the same value reduces continuity at that value by one. This is the only way to reduce the continuity of 
+the curve at the knot values. If there are k-1 (or more) equal knots then you get a discontinuity in the curve.
+
+Close knots:
+
+As two knots' values get closer together, relative to the spacing of the other knots, the curve moves closer to the 
+related control point.
+
+Distant knots:
+
+As two knots' values get further apart, relative to the spacing of the other knots, the curve moves further away from 
+the related control point.
+
+Use of Non-uniform Knot Vectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Standard procedure is to use uniform or open uniform B-splines unless there is a very good reason not to do so. 
+Moving two knots closer together tends to move the curve only slightly and so there is usually little point in doing it. 
+This leads to the conclusion that the main use of non-uniform B-splines is to allow for multiple knots, which adjust the 
+continuity of the curve at the knot values.
+
+However, non-uniform B-splines are the general form of the B-spline because they incorporate open uniform and uniform 
+B-splines as special cases. Thus we will talk about non-uniform B-splines when we mean the general case, incorporating 
+both uniform and open uniform.
+
+What can you do to control the shape of a B-spline?
+
+    - Move the control points.
+    - Add or remove control points.
+    - Use multiple control points.
+    - Change the order, k.
+    - Change the type of knot vector.
+    - Change the relative spacing of the knots.
+    - Use multiple knot values in the knot vector.
+
+What should the defaults be?
+
+If there are no pressing reasons for doing otherwise, your B-spline should be defined as follows:
+
+    - k=4 (cubic)
+    - no multiple control points
+    - uniform (for a closed curve) or open uniform (for an open curve) knot vector.
+
+
+Rational B-splines
+==================
+
+https://www.cl.cam.ac.uk/teaching/2000/AGraphHCI/SMEG/node5.html:
+
+Rational B-splines have all of the properties of non-rational B-splines plus the following two useful features:
+They produce the correct results under projective transformations (while non-rational B-splines only produce the correct
+results under affine transformations).
+
+They can be used to represent lines, conics, non-rational B-splines; and, when generalised to patches, can represents
+planes, quadrics, and tori.
+
+The antonym of rational is non-rational. Non-rational B-splines are a special case of rational B-splines, just as
+uniform B-splines are a special case of non-uniform B-splines. Thus, non-uniform rational B-splines encompass almost
+every other possible 3D shape definition. Non-uniform rational B-spline is a bit of a mouthful and so it is generally
+abbreviated to NURBS.
+
+We have already learnt all about the the B-spline bit of NURBS and about the non-uniform bit. So now all we need to
+know is the meaning of the rational bit and we will fully(?) understand NURBS.
+
+Rational B-splines are defined simply by applying the B-spline equation (Equation 87) to homogeneous coordinates,
+rather than normal 3D coordinates.
+
+"""
+from typing import List, Iterable, Sequence, TYPE_CHECKING, Dict, Tuple, Optional
+from .vector import Vector, distance
+from .matrix import Matrix
+from math import pow, isclose
+from ezdxf.lldxf.const import DXFValueError
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+def knot_open_uniform(n: int, order: int) -> List[float]:
+    """
+    Returns a open uniform knot vector.
+
+    Args:
+        n: count of control points
+        order: spline order
+
+    Returns: list of floats (knot vector)
+
+    """
+    nplusc = n + order
+    nplus2 = n + 2
+    knots = [0.]
+    for i in range(2, nplusc + 1):
+        if (i > order) and (i < nplus2):
+            knots.append(knots[-1] + 1.)
+        else:
+            knots.append(knots[-1])
+    return knots
+
+
+def is_uniform_knots(knots: Sequence[float], places: int = 4) -> bool:
+    deltas = set(round(k2 - k1, ndigits=places) for k1, k2 in zip(knots, knots[1:]))
+    return len(deltas) == 1
+
+
+def knot_uniform(n: int, order: int) -> List[float]:
+    """
+    Returns a uniform knot vector.
+
+    Args:
+        n: count of control points
+        order: spline order
+
+    Returns: list of floats (knot vector)
+
+    """
+    return [float(knot_value) for knot_value in range(0, n + order)]
+
+
+def required_knot_values(count: int, order: int) -> int:
+    # just to show the connections
+    # count = count of control points = n + 1
+    # k = order of spline = degree + 1
+    # 2 <= k <= n + 1
+    # p = degree
+    # order = p + 1
+    k = order
+    n = count - 1
+    p = k - 1
+    if not (2 <= k <= (n + 1)):
+        raise DXFValueError('Invalid count/order combination')
+    # n + p + 2 = count + order
+    return n + p + 2
+
+
+def uniform_t_vector(fit_points: Sequence) -> Iterable[float]:
+    n = float(len(fit_points) - 1)
+    for t in range(len(fit_points)):
+        yield float(t) / n
+
+
+def distance_t_vector(fit_points: Iterable['Vertex']):
+    return centripetal_t_vector(fit_points, power=1)
+
+
+def centripetal_t_vector(fit_points: Iterable['Vertex'], power: float = .5) -> Iterable[float]:
+    distances = [pow(distance(p1, p2), power) for p1, p2 in zip(fit_points, fit_points[1:])]
+    total_length = sum(distances)
+    s = 0.
+    yield s
+    for d in distances:
+        s += d
+        yield s / total_length
+
+
+def bspline_basis(u: float, index: int, degree: int, knots: Sequence[float]) -> float:
+    """
+    B-spline basis function.
+
+    Simple recursive implementation for testing and comparison.
+
+    Args:
+        u: curve parameter in range [0 .. max(knots)]
+        index: index of control point
+        degree: degree of B-spline
+        knots: knots vector
+
+    Returns: basis value N_i,p(u) as float
+
+    """
+    cache = {}  # type: Dict[Tuple[int, int], float]
+    u = float(u)
+
+    def N(i: int, p: int) -> float:
+        try:
+            return cache[(i, p)]
+        except KeyError:
+            if p == 0:
+                retval = 1 if knots[i] <= u < knots[i + 1] else 0.
+            else:
+                dominator = (knots[i + p] - knots[i])
+                f1 = (u - knots[i]) / dominator * N(i, p - 1) if dominator else 0.
+
+                dominator = (knots[i + p + 1] - knots[i + 1])
+                f2 = (knots[i + p + 1] - u) / dominator * N(i + 1, p - 1) if dominator else 0.
+
+                retval = f1 + f2
+            cache[(i, p)] = retval
+            return retval
+
+    return N(int(index), int(degree))
+
+
+def bspline_basis_vector(u: float, count: int, degree: int, knots: Sequence[float]) -> List[float]:
+    """
+    Create basis vector at parameter u.
+
+    Used with the bspline_basis() for testing and comparison.
+
+    Args:
+        u: curve parameter in range [0 .. max(knots)]
+        count: control point count (n + 1)
+        degree: degree of B-spline (order = degree + 1)
+        knots: knot vector
+
+    Returns: basis vector as list fo floats, len(basis) == count
+
+    """
+    assert len(knots) == (count + degree + 1)
+    basis = [bspline_basis(u, index, degree, knots) for index in range(count)]  # type: List[float]
+    if isclose(u, knots[-1]):  # pick up last point ??? why is this necessary ???
+        basis[-1] = 1.
+    return basis
+
+
+def bspline_vertex(u: float, degree: int, control_points: Sequence['Vertex'], knots: Sequence[float]) -> Vector:
+    """
+    Calculate B-spline vertex at parameter u.
+
+    Used with the bspline_basis_vector() for testing and comparison.
+
+    Args:
+        u:  curve parameter in range [0 .. max(knots)]
+        degree: degree of B-spline (order = degree + 1)
+        control_points: control points as list of (x, y[,z]) tuples
+        knots: knot vector as list of floats, len(knots) == (count + order)
+
+    Returns: Vector() object
+
+    """
+    basis_vector = bspline_basis_vector(u, count=len(control_points), degree=degree, knots=knots)
+
+    vertex = Vector()
+    for basis, point in zip(basis_vector, control_points):
+        vertex += Vector(point) * basis
+    return vertex
+
+
+def bspline_control_frame(fit_points: Iterable['Vertex'], degree: int = 3, method: str = 'distance', power: float = .5):
+    """
+    Calculate B-spline control frame, given are the fit points and the degree of the B-spline.
+
+        1. method = 'uniform', creates a uniform t vector, [0 .. 1] equally spaced
+        2. method = 'distance', creates a t vector with values proportional to the fit point distances
+        3. method = 'centripetal', creates a t vector with values proportional to the fit point distances^power
+
+    Args:
+        fit_points: fit points of B-spline
+        degree: degree of B-spline
+        method: calculation method for parameter vector t
+        power: power for centripetal method
+
+    """
+
+    def create_t_vector():
+        if method == 'uniform':
+            return uniform_t_vector(fit_points)  # equally spaced 0 .. 1
+        elif method == 'distance':
+            return distance_t_vector(fit_points)
+        elif method == 'centripetal':
+            return centripetal_t_vector(fit_points, power=power)
+        else:
+            raise DXFValueError('Unknown method: {}'.format(method))
+
+    fit_points = list(fit_points)
+    count = len(fit_points)
+    order = degree + 1
+    if order > count:
+        raise DXFValueError('Need more fit points for degree {}'.format(degree))
+
+    t_vector = list(create_t_vector())
+    knots = list(control_frame_knots(count - 1, degree, t_vector))
+    control_points = global_curve_interpolation(fit_points, degree, t_vector, knots)
+    bspline = BSpline(control_points, order=order, knots=knots)
+    bspline.t_array = t_vector
+    return bspline
+
+
+def bspline_control_frame_approx(fit_points: Iterable['Vertex'], count: int, degree: int = 3, method: str = 'distance',
+                                 power: float = .5):
+    """
+    Approximate B-spline by a reduced count of control points, given are the fit points and the degree of the B-spline.
+
+        1. method = 'uniform', creates a uniform t vector, [0 .. 1] equally spaced
+        2. method = 'distance', creates a t vector with values proportional to the fit point distances
+        3. method = 'centripetal', creates a t vector with values proportional to the fit point distances^power
+
+    Args:
+        fit_points: all fit points of B-spline
+        count: count of designated control points
+        degree: degree of B-spline
+        method: calculation method for parameter vector t
+        power: power for centripetal method
+
+    """
+
+    def create_t_vector():
+        if method == 'uniform':
+            return uniform_t_vector(fit_points)  # equally spaced 0 .. 1
+        elif method == 'distance':
+            return distance_t_vector(fit_points)
+        elif method == 'centripetal':
+            return centripetal_t_vector(fit_points, power=power)
+        else:
+            raise DXFValueError('Unknown method: {}'.format(method))
+
+    fit_points = list(fit_points)
+    order = degree + 1
+    if order > count:
+        raise DXFValueError('More control points for degree {} required.'.format(degree))
+
+    t_vector = list(create_t_vector())
+    knots = list(control_frame_knots(len(fit_points) - 1, degree, t_vector))
+    control_points = global_curve_approximation(fit_points, count, degree, t_vector, knots)
+    bspline = BSpline(control_points, order=order)
+    return bspline
+
+
+def control_frame_knots(n: int, p: int, t_vector: Iterable[float]) -> Iterable[float]:
+    """
+    Generates a 'clamped' knot vector for control frame creation. All knot values in the range [0 .. 1].
+
+    Args:
+        n: count fit points - 1
+        p: degree of spline
+        t_vector: parameter vector, length(t_vector) == n+1
+
+    Yields: n+p+2 knot values as floats
+
+    """
+    order = int(p + 1)
+    if order > (n + 1):
+        raise DXFValueError('Invalid n/p combination')
+
+    t_vector = [float(t) for t in t_vector]
+    for _ in range(order):  # clamped spline has 'order' leading 0s
+        yield t_vector[0]
+    for j in range(1, n - p + 1):
+        yield sum(t_vector[j: j + p]) / p
+    for _ in range(order):  # clamped spline has 'order' appended 1s
+        yield t_vector[-1]
+
+
+def global_curve_interpolation(fit_points: Sequence['Vertex'],
+                               degree: int,
+                               t_vector: Iterable[float],
+                               knots: Iterable[float]) -> List[Vector]:
+    def create_matrix_N():
+        spline = Basis(knots=knots, order=degree + 1, count=len(fit_points))
+        return Matrix([spline.basis(t) for t in t_vector])
+
+    matrix_N = create_matrix_N()
+    control_points = matrix_N.gauss_matrix(fit_points)
+    return Vector.list(control_points.rows())
+
+
+def global_curve_approximation(fit_points: Iterable['Vertex'],
+                               count: int,
+                               degree: int,
+                               t_vector: Iterable[float],
+                               knots: Iterable[float]) -> List[Vector]:
+    """
+    Approximate B-spline by a reduced count of control points, given are the fit points and the degree of the B-spline.
+
+    Args:
+        fit_points: all B-spline fit point
+        count: count of designated control points
+        degree: degree of B-spline
+        t_vector: parameter vector
+        knots: knot vector
+
+    Returns: BSpline() object
+
+    """
+    # source: http://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/INT-APP/CURVE-APP-global.html
+    fit_points = Vector.list(fit_points)  # data points D
+    n = len(fit_points) - 1
+    h = count - 1
+    d0 = fit_points[0]
+    dn = fit_points[n]
+    spline = Basis(knots, order=degree + 1, count=len(fit_points))
+    # matrix_N[0] == row 0
+    matrix_N = [spline.basis(t) for t in t_vector]  # 0 .. n
+
+    def Q(k):
+        ntk = matrix_N[k]
+        return fit_points[k] - d0 * ntk[0] - dn * ntk[h]
+
+    # matrix_Q[0] == row 1
+    matrix_Q = [sum(Q(k) * matrix_N[k][i] for k in range(1, n)) for i in range(1, h)]
+    matrix_N = Matrix([row[1:h] for row in matrix_N[1:-1]])
+    matrix_M = matrix_N.transpose() * matrix_N
+    P = matrix_M.gauss_matrix(matrix_Q)
+    control_points = [d0]
+    control_points.extend(Vector.generate(P.rows()))
+    control_points.append(dn)
+    return control_points
+
+
+class Basis:
+    def __init__(self, knots: Iterable[float], order: int, count: int, weights: Sequence[float] = None):
+        self.knots = list(knots)  # type: List[float]
+        self.order = order  # type: int
+        self.count = count  # type: int
+        self.weights = weights  # type: Optional[Sequence[float]]
+
+    @property
+    def max_t(self) -> float:
+        return self.knots[-1]
+
+    @property
+    def nplusc(self) -> int:
+        return self.count + self.order
+
+    def create_nbasis(self, t: float) -> List[float]:
+        """
+        Calculate the first order basis functions N[i][1]
+
+        Returns: list of basis values
+
+        """
+        return [1. if k1 <= t < k2 else 0. for k1, k2 in zip(self.knots, self.knots[1:])]
+
+    def basis(self, t: float) -> List[float]:
+        knots = self.knots
+        nbasis = self.create_nbasis(t)
+        # calculate the higher order basis functions
+        for k in range(2, self.order + 1):
+            for i in range(self.nplusc - k):
+                d = ((t - knots[i]) * nbasis[i]) / (knots[i + k - 1] - knots[i]) if nbasis[i] != 0. else 0.
+                e = ((knots[i + k] - t) * nbasis[i + 1]) / (knots[i + k] - knots[i + 1]) if nbasis[i + 1] != 0. else 0.
+                nbasis[i] = d + e
+        if isclose(t, self.max_t):  # pick up last point
+            nbasis[self.count - 1] = 1.
+        if self.weights is None:
+            return nbasis[:self.count]
+        else:
+            return self.weighting(nbasis[:self.count])
+
+    def weighting(self, nbasis: Iterable[float]) -> List[float]:
+        products = [nb * w for nb, w in zip(nbasis, self.weights)]
+        s = sum(products)
+        return [0.0] * self.count if s == 0.0 else [p / s for p in products]
+
+
+class DBasis(Basis):
+    def basis(self, t: float) -> Tuple[List[float], List[float], List[float]]:
+        knots = self.knots
+        nbasis = self.create_nbasis2(t)
+        d1nbasis = [0.] * self.nplusc
+        d2nbasis = d1nbasis[:]
+
+        for k in range(2, self.order + 1):
+            for i in range(self.nplusc - k):
+                # calculate basis functions
+                b1 = ((t - knots[i]) * nbasis[i]) / (knots[i + k - 1] - knots[i]) if nbasis[i] != 0. else 0.
+                b2 = ((knots[i + k] - t) * nbasis[i + 1]) / (knots[i + k] - knots[i + 1]) if nbasis[i + 1] != 0. else 0.
+
+                # calculate first derivative
+                f1 = nbasis[i] / (knots[i + k - 1] - knots[i]) if nbasis[i] != 0. else 0.
+                f2 = -nbasis[i + 1] / (knots[i + k] - knots[i + 1]) if nbasis[i + 1] != 0. else 0.
+                f3 = ((t - knots[i]) * d1nbasis[i]) / (knots[i + k - 1] - knots[i]) if d1nbasis[i] != 0. else 0.
+                f4 = ((knots[i + k] - t) * d1nbasis[i + 1]) / (knots[i + k] - knots[i + 1]) if d1nbasis[
+                                                                                                   i + 1] != 0. else 0.
+
+                # calculate second derivative
+                s1 = (2 * d1nbasis[i]) / (knots[i + k - 1] - knots[i]) if d1nbasis[i] != 0. else 0.
+                s2 = (-2 * d1nbasis[i + 1]) / (knots[i + k] - knots[i + 1]) if d1nbasis[i + 1] != 0. else 0.
+                s3 = ((t - knots[i]) * d2nbasis[i]) / (knots[i + k - 1] - knots[i]) if d2nbasis[i] != 0. else 0.
+                s4 = ((knots[i + k] - t) * d2nbasis[i + 1]) / (knots[i + k] - knots[i + 1]) if d2nbasis[
+                                                                                                   i + 1] != 0. else 0.
+
+                nbasis[i] = b1 + b2
+                d1nbasis[i] = f1 + f2 + f3 + f4
+                d2nbasis[i] = s1 + s2 + s3 + s4
+
+        count = self.count
+        if self.weights is None:
+            return nbasis[:count], d1nbasis[:count], d2nbasis[:count]
+        else:
+            self.weighting(nbasis[:count]), self.weighting(d1nbasis[:count]), self.weighting(d2nbasis[:count])
+
+    def create_nbasis2(self, t: float) -> List[float]:
+        nbasis = self.create_nbasis(t)
+        if isclose(t, self.max_t):
+            nbasis[self.count - 1] = 1.
+        return nbasis
+
+
+class DBasisU(DBasis):
+    def create_nbasis2(self, t: float) -> List[float]:
+        nbasis = self.create_nbasis(t)
+        if isclose(t, self.knots[self.count]):
+            nbasis[self.count - 1] = 1.
+            nbasis[self.count] = 0.
+        return nbasis
+
+
+class BSpline:
+    """
+    Calculate the points of a B-spline curve, using an uniform open knot vector ("clamped").
+
+    Accepts 2d points as definition points, but output ist always 3d (z-axis is 0).
+
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'],
+                 order: int = 4,
+                 knots: Iterable[float] = None,
+                 weights: Iterable[float] = None):
+        self.control_points = Vector.list(control_points)
+        self.order = order
+        if order > self.count:
+            raise DXFValueError('Invalid need more control points for order {}'.format(order))
+
+        if knots is None:
+            knots = knot_open_uniform(self.count, self.order)
+        else:
+            knots = list(knots)
+            if len(knots) != self.nplusc:
+                raise ValueError("{} knot values required, got {}.".format(self.nplusc, len(knots)))
+
+        self.basis = Basis(knots, self.order, self.count, weights=weights)
+
+    @property
+    def nplusc(self) -> int:
+        return self.count + self.order
+
+    @property
+    def count(self) -> int:
+        return len(self.control_points)
+
+    @property
+    def max_t(self) -> float:
+        return self.basis.max_t
+
+    @property
+    def degree(self) -> int:
+        return self.order - 1
+
+    def knot_values(self) -> List[float]:
+        return self.basis.knots
+
+    def basis_values(self, t: float) -> List[float]:
+        return self.basis.basis(t)
+
+    def step_size(self, segments: int) -> float:
+        return self.max_t / float(segments)
+
+    def approximate(self, segments: int = 20) -> Iterable[Vector]:
+        step = self.step_size(segments)
+        for point_index in range(segments + 1):
+            yield self.point(point_index * step)
+
+    def point(self, t: float) -> Vector:
+        """
+        Get point at SplineCurve(t) as tuple (x, y, z).
+
+        Args:
+            t: parameter in range [0, max_t]
+
+        Returns: Vector(x, y, z)
+
+        """
+        if isclose(t, self.max_t):
+            t = self.max_t
+
+        p = Vector()
+        for control_point, basis in zip(self.control_points, self.basis_values(t)):
+            p += control_point * basis
+        return p
+
+    def insert_knot(self, t: float) -> None:
+        """
+        Insert additional knot, without altering the curve shape.
+
+        Args:
+            t: position of new knot 0 < t < max_t
+
+        """
+        if self.basis.weights is not None:
+            raise DXFValueError('Rational splines not supported.')
+
+        knots = self.basis.knots
+        cpoints = self.control_points
+        p = self.degree
+
+        def find_knot_index() -> int:
+            for knot_index in range(1, len(knots)):
+                if knots[knot_index - 1] <= t < knots[knot_index]:
+                    return knot_index - 1
+
+        def new_point(index: int) -> Vector:
+            a = (t - knots[index]) / (knots[index + p] - knots[index])
+            return cpoints[index - 1] * (1 - a) + cpoints[index] * a
+
+        if t <= 0. or t >= self.max_t:
+            raise DXFValueError('Invalid position t')
+
+        k = find_knot_index()
+        if k < p:
+            raise DXFValueError('Invalid position t')
+
+        cpoints[k - p + 1:k] = [new_point(i) for i in range(k - p + 1, k + 1)]
+        knots.insert(k + 1, t)  # knot[k] <= t < knot[k+1]
+        self.basis.count = len(cpoints)
+
+
+class BSplineU(BSpline):
+    """
+    Calculate the points of a B-spline curve, uniform (periodic) knot vector (not "clamped").
+
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'], order: int = 4, weights: Iterable[float] = None):
+        control_points = list(control_points)
+        knots = knot_uniform(len(control_points), order)
+        super().__init__(control_points, order=order, knots=knots, weights=weights)
+
+    def step_size(self, segments: int) -> float:
+        return float(self.count - self.order + 1) / segments
+
+    def approximate(self, segments=20) -> Iterable[Vector]:
+        step = self.step_size(segments)
+        base = float(self.order - 1)
+        for point_index in range(segments + 1):
+            yield self.point(base + point_index * step)
+
+    def t_array(self) -> List[float]:
+        raise NotImplemented
+
+
+class BSplineClosed(BSplineU):
+    """
+    Calculate the points of a closed uniform B-spline curve.
+
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'], order: int = 4, weights: Iterable[float] = None):
+        # control points wrap around
+        points = list(control_points)
+        points.extend(points[:order - 1])
+        if weights is not None:
+            weights = list(weights)
+            weights.extend(weights[:order - 1])
+        super().__init__(points, order=order, weights=weights)
+
+
+class DerivativePoint:  # Mixin
+    def point(self, t: float) -> Tuple[Vector, Vector, Vector]:
+        """
+        Get point, 1st and 2nd derivative at B-spline(t) as tuple (p, d1, d3),
+        where p, d1 nad d2 is a tuple (x, y, z).
+
+        Args:
+            t: parameter in range [0, max_t]
+
+        """
+        if isclose(self.max_t, t):
+            t = self.max_t
+
+        nbasis, d1nbasis, d2nbasis = self.basis_values(t)
+        point = Vector()
+        d1 = Vector()
+        d2 = Vector()
+        for i, control_point in enumerate(self.control_points):
+            point += control_point * nbasis[i]
+            d1 += control_point * d1nbasis[i]
+            d2 += control_point * d2nbasis[i]
+        return point, d1, d2
+
+
+class DBSpline(DerivativePoint, BSpline):
+    """
+    Calculate the Points and Derivative of an open uniform B-spline curve ("clamped").
+
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'],
+                 order: int = 4,
+                 knots: Iterable[float] = None,
+                 weights: Iterable[float] = None):
+        super().__init__(control_points, order=order, knots=knots, weights=weights)
+        self.basis = DBasis(self.knot_values(), self.order, self.count)
+
+
+class DBSplineU(DerivativePoint, BSplineU):
+    """
+    Calculate the Points and Derivative of an uniform B-spline curve (not "clamped").
+
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'], order: int = 4, weights: Iterable[float] = None):
+        super().__init__(control_points, order=order, weights=weights)
+        self.basis = DBasisU(self.knot_values(), self.order, self.count)
+
+
+class DBSplineClosed(DerivativePoint, BSplineClosed):
+    """
+    Calculate the Points and Derivative of a closed B-spline curve.
+
+    UNTESTED!
+    """
+
+    def __init__(self, control_points: Iterable['Vertex'], order: int = 4, weights: Iterable[float] = None):
+        super().__init__(control_points, order=order, weights=weights)
+        self.basis = DBasisU(self.knot_values(), self.order, self.count)

+ 135 - 0
venv/lib/python3.7/site-packages/ezdxf/math/bulge.py

@@ -0,0 +1,135 @@
+# Copyright (c) 2018 Manfred Moitzi
+# License: MIT License
+# source: http://www.lee-mac.com/bulgeconversion.html
+# source: http://www.afralisp.net/archive/lisp/Bulges1.htm
+from typing import Any, TYPE_CHECKING, Tuple
+from .vector import Vec2
+import math
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+def polar(p: Any, angle: float, distance: float) -> Vec2:
+    """
+    Returns the point at a specified `angle` and `distance` from point `p`.
+
+    Args:
+        p: point (x, y) as args accepted by Vec2()
+        angle: angle in radians
+        distance: distance
+
+    """
+    return Vec2(p) + Vec2.from_angle(angle, distance)
+
+
+def angle(p1: Any, p2: Any) -> float:
+    """
+    Returns an angle in radians of a line defined by two endpoints
+
+    Args:
+        p1: start point (x, y) as args accepted by Vec2()
+        p2: end point (x, y) as args accepted by Vec2()
+
+    """
+    return (Vec2(p2) - Vec2(p1)).angle
+
+
+def arc_to_bulge(center: 'Vertex', start_angle: float, end_angle: float, radius: float) -> Tuple[Vec2, Vec2, float]:
+    """
+    Calculate bulge parameters from arc parameters.
+
+    Args:
+        center: circle center point (x, y) as args accepted by Vec2()
+        start_angle: start angle in radians
+        end_angle: end angle in radians
+        radius: circle radius
+
+    Returns: (start_point, end_point, bulge)
+
+    """
+    start_point = polar(center, start_angle, radius)
+    end_point = polar(center, end_angle, radius)
+    pi2 = math.pi * 2
+    a = math.fmod((pi2 + (end_angle - start_angle)), pi2) / 4.
+    bulge = math.sin(a) / math.cos(a)
+    return start_point, end_point, bulge
+
+
+def bulge_3_points(start_point: 'Vertex', end_point: 'Vertex', point: 'Vertex') -> float:
+    """
+    Calculate bulge value defined by three points.
+
+    Args:
+        start_point: start point (x, y) of arc as args accepted by Vec2()
+        end_point: end point (x, y) of arc as args accepted by Vec2()
+        point: arbitrary point (x, y) on arc as args accepted by Vec2()
+
+    Returns: bulge value
+
+    Based on 3-Points to Bulge by Lee Mac
+
+    """
+    a = (math.pi - angle(point, start_point) + angle(point, end_point)) / 2
+    return math.sin(a) / math.cos(a)
+
+
+def bulge_to_arc(start_point: 'Vertex', end_point: 'Vertex', bulge: float) -> Tuple[Vec2, float, float, float]:
+    """
+    Calculate arc parameters from bulge parameters.
+
+    Based on Bulge to Arc by Lee Mac
+
+    Args:
+        start_point: start vertex (x, y) as args accepted by Vec2()
+        end_point: end vertex (x, y) as args accepted by Vec2()
+        bulge: bulge value
+
+    Returns: (center, start_angle, end_angle, radius)
+
+    """
+    r = signed_bulge_radius(start_point, end_point, bulge)
+    a = angle(start_point, end_point) + (math.pi / 2 - math.atan(bulge) * 2)
+    c = polar(start_point, a, r)
+    if bulge < 0:
+        return c, angle(c, end_point), angle(c, start_point), abs(r)
+    else:
+        return c, angle(c, start_point), angle(c, end_point), abs(r)
+
+
+def bulge_center(start_point: 'Vertex', end_point: 'Vertex', bulge: float) -> Vec2:
+    """
+    Calculate center of arc described by the given bulge parameters.
+
+    Based on  Bulge Center by Lee Mac.
+
+    Args:
+        start_point: start point (x, y) as args accepted by Vec2()
+        end_point: end point (x, y) as args accepted by Vec2()
+        bulge: bulge value
+
+    Returns: Vector
+
+    """
+    start_point = Vec2(start_point)
+    a = angle(start_point, end_point) + (math.pi / 2. - math.atan(bulge) * 2.)
+    return start_point + Vec2.from_angle(a, signed_bulge_radius(start_point, end_point, bulge))
+
+
+def signed_bulge_radius(start_point: 'Vertex', end_point: 'Vertex', bulge: float) -> float:
+    return Vec2(start_point).distance(Vec2(end_point)) * (1. + (bulge * bulge)) / 4. / bulge
+
+
+def bulge_radius(start_point: 'Vertex', end_point: 'Vertex', bulge: float) -> float:
+    """
+    Calculate radius of arc defined by the given bulge parameters.
+
+    Based on Bulge Radius by Lee Mac
+
+    Args:
+        start_point: start point (x, y) as args accepted by Vec2()
+        end_point: end point (x, y) as args accepted by Vec2()
+        bulge: bulge value
+
+    """
+    return abs(signed_bulge_radius(start_point, end_point, bulge))

+ 145 - 0
venv/lib/python3.7/site-packages/ezdxf/math/circle.py

@@ -0,0 +1,145 @@
+# Copyright (c) 2010-2019 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Sequence
+import math
+from .line import ConstructionRay
+from .vector import Vec2
+from .bbox import BoundingBox2d
+from .construct2d import ConstructionTool
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+HALF_PI = math.pi / 2.
+
+
+class ConstructionCircle(ConstructionTool):
+    def __init__(self, center: 'Vertex', radius: float = 1.0):
+        self.center = Vec2(center)
+        self.radius = float(radius)
+        if self.radius <= 0.:
+            raise ValueError("Radius has to be > 0.")
+
+    @staticmethod
+    def from_3p(p1: 'Vertex', p2: 'Vertex', p3: 'Vertex') -> 'ConstructionCircle':
+        """ Creates a circle from three points. """
+        p1 = Vec2(p1)
+        p2 = Vec2(p2)
+        p3 = Vec2(p3)
+        ray1 = ConstructionRay(p1, p2)
+        ray2 = ConstructionRay(p1, p3)
+        center_ray1 = ray1.orthogonal(p1.lerp(p2))
+        center_ray2 = ray2.orthogonal(p1.lerp(p3))
+        center = center_ray1.intersect(center_ray2)
+        return ConstructionCircle(center, center.distance(p1))
+
+    @property
+    def bounding_box(self) -> 'BoundingBox2d':
+        rvec = Vec2((self.radius, self.radius))
+        return BoundingBox2d((self.center - rvec, self.center + rvec))
+
+    def move(self, dx: float, dy: float) -> None:
+        """
+        Move circle about `dx` in x-axis and about `dy` in y-axis.
+
+        Args:
+            dx: translation in x-axis
+            dy: translation in y-axis
+
+        """
+        self.center += Vec2((dx, dy))
+
+    def point_at(self, angle: float) -> Vec2:
+        """
+        Returns point on circle at `angle` as 2d vector.
+
+        Args:
+            angle: angle in radians
+
+        """
+        return self.center + Vec2.from_angle(angle, self.radius)
+
+    def inside(self, point: 'Vertex') -> bool:
+        """ Test if `point` is inside circle. """
+        return self.radius >= self.center.distance(Vec2(point))
+
+    def tangent(self, angle: float) -> ConstructionRay:
+        """
+        Returns tangent to circle at `angle` as ConstructionRay().
+
+        Args:
+            angle: angle in radians
+
+        """
+        point_on_circle = self.point_at(angle)
+        ray = ConstructionRay(self.center, point_on_circle)
+        return ray.orthogonal(point_on_circle)
+
+    def intersect_ray(self, ray: ConstructionRay, abs_tol: float = 1e-12) -> Sequence[Vec2]:
+        """
+        Returns intersection points for intersection of this circle with `ray` as sequence of 2d points.
+
+        Args:
+            ray: intersection ray
+            abs_tol: absolute tolerance for tests (e.g. test for tangents)
+
+        Returns: tuple of Vec2()
+
+            tuple contains:
+            0 points .. no intersection
+            1 point .. ray is a tangent on the circle
+            2 points .. ray intersects with the circle
+
+        """
+        ortho_ray = ray.orthogonal(self.center)
+        intersection_point = ray.intersect(ortho_ray)
+        dist = self.center.distance(intersection_point)
+        result = []
+        if dist < self.radius:  # intersect in two points
+            if math.isclose(dist, 0., abs_tol=abs_tol):  # if ray goes through center point
+                angle = ortho_ray.angle
+                alpha = HALF_PI
+            else:
+                # the exact direction of angle (all 4 quadrants Q1-Q4) is important:
+                # ortho_ray.angle is only at the center point correct
+                angle = (intersection_point - self.center).angle
+                alpha = math.acos(intersection_point.distance(self.center) / self.radius)
+            result.append(self.point_at(angle + alpha))
+            result.append(self.point_at(angle - alpha))
+        elif math.isclose(dist, self.radius, abs_tol=abs_tol):  # ray is a tangent of circle
+            result.append(intersection_point)
+        # else no intersection
+        return tuple(result)
+
+    def intersect_circle(self, other: 'ConstructionCircle', abs_tol: float = 1e-12) -> Sequence[Vec2]:
+        """
+        Returns intersection points of two circles as sequence of 2d points.
+
+        Args:
+            other: intersection circle
+            abs_tol: absolute tolerance for tests (e.g. test for circle touch point)
+
+        Returns: tuple of Vec2()
+
+            tuple contains:
+            0 points .. no intersection
+            1 point .. circle touches the other_circle in one point
+            2 points .. circle intersects with the other_circle
+
+        """
+        r1 = self.radius
+        r2 = other.radius
+        d = self.center.distance(other.center)
+        d_max = r1 + r2
+        d_min = math.fabs(r1 - r2)
+        result = []
+        if d_min <= d <= d_max:
+            angle = (other.center - self.center).angle
+            # if circles touches in one point
+            if math.isclose(d, d_max, abs_tol=abs_tol) or math.isclose(d, d_min, abs_tol=abs_tol):
+                result.append(self.point_at(angle))
+            else:  # circles intersect in two points
+                alpha = math.acos((r2 ** 2 - r1 ** 2 - d ** 2) / (-2. * r1 * d))  # 'Cosinus-Satz'
+                result.append(self.point_at(angle + alpha))
+                result.append(self.point_at(angle - alpha))
+        return tuple(result)

+ 104 - 0
venv/lib/python3.7/site-packages/ezdxf/math/construct2d.py

@@ -0,0 +1,104 @@
+# Copyright (c) 2010-2018 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING
+from functools import partial
+import math
+from operator import le, ge, lt, gt
+from abc import abstractmethod
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import BoundingBox2d, Vertex
+
+HALF_PI = math.pi / 2.  # type: float
+THREE_PI_HALF = 1.5 * math.pi  # type: float
+DOUBLE_PI = math.pi * 2.  # type: float
+
+
+def is_close_points(p1: 'Vertex', p2: 'Vertex', abs_tol=1e-12) -> bool:
+    """
+    Returns true if `p1` is very close to `p2`.
+
+    Args:
+        p1: vertex 1
+        p2: vertex 2
+        abs_tol: absolute tolerance
+    """
+    if len(p1) != len(p2):
+        raise TypeError('incompatible points')
+
+    for v1, v2 in zip(p1, p2):
+        if not math.isclose(v1, v2, abs_tol=abs_tol):
+            return False
+    return True
+
+
+def normalize_angle(angle: float) -> float:
+    """
+    Returns normalized angle between 0 and 2*pi.
+    """
+    angle = math.fmod(angle, DOUBLE_PI)
+    if angle < 0:
+        angle += DOUBLE_PI
+    return angle
+
+
+def enclosing_angles(angle, start_angle, end_angle, ccw=True, abs_tol=1e-9):
+    isclose = partial(math.isclose, abs_tol=abs_tol)
+
+    s = normalize_angle(start_angle)
+    e = normalize_angle(end_angle)
+    a = normalize_angle(angle)
+    if isclose(s, e):
+        return isclose(s, a)
+
+    if s < e:
+        r = s < a < e
+    else:
+        r = not (e < a < s)
+    return r if ccw else not r
+
+
+def left_of_line(point: 'Vertex', p1: 'Vertex', p2: 'Vertex', online=False) -> bool:
+    """
+    True if `point` is "left of line" (`p1`, `p2`). Point on the line is "left of line" if `online` is True.
+
+    """
+
+    px, py, *_ = point
+    p1x, p1y, *_ = p1
+    p2x, p2y, *_ = p2
+
+    if online:
+        lower, greater = le, ge  # lower/greater or equal
+    else:
+        lower, greater = lt, gt  # real lower/greater then
+
+    # check if p1 and p2 are on the same vertical line
+    if math.isclose(p1x, p2x):
+        # compute on which side of the line point should be
+        should_be_left = p1y < p2y
+        return lower(px, p1x) if should_be_left else greater(px, p1y)
+    else:
+        # get pitch of line
+        pitch = (p2y - p1y) / (p2x - p1x)
+        # get y-value at points's x-position
+        y = pitch * (px - p1x) + p1y
+        # compute if point should be above or below the line
+        should_be_above = p1x < p2x
+        return greater(py, y) if should_be_above else lower(py, y)
+
+
+class ConstructionTool:
+    """
+    Abstract base class for all 2D construction classes.
+
+    """
+
+    @property
+    @abstractmethod
+    def bounding_box(self) -> 'BoundingBox2d':
+        pass
+
+    @abstractmethod
+    def move(self, dx: float, dy: float) -> None:
+        pass

+ 37 - 0
venv/lib/python3.7/site-packages/ezdxf/math/convexhull.py

@@ -0,0 +1,37 @@
+# Author:  mozman
+# Purpose: convex hull algorithm
+# Created: 28.02.2010
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable, List
+from ezdxf.math.construct2d import left_of_line
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+def convex_hull_2d(points: Iterable['Vertex'])->List['Vertex']:
+    def _convexhull(hull):
+        while len(hull) > 2:
+            start_point, check_point, destination_point = hull[-3:]  # the last three points
+            if not left_of_line(check_point, start_point, destination_point):  # curve not turns right
+                del hull[-2]  # remove the penultimate point
+            else:
+                break
+        return hull
+
+    points = sorted(set(points))  # remove duplicate points
+
+    if len(points) < 3:
+        raise ValueError("ConvexHull(): Less than 3 unique points given!")
+
+    upper_hull = points[:2]  # first two points
+    for next_point in points[2:]:
+        upper_hull.append(next_point)
+        upper_hull = _convexhull(upper_hull)
+    lower_hull = [points[-1], points[-2]]  # last two points
+
+    for next_point in reversed(points[:-2]):
+        lower_hull.append(next_point)
+        lower_hull = _convexhull(lower_hull)
+    upper_hull.extend(lower_hull[1:-1])
+    return upper_hull

+ 99 - 0
venv/lib/python3.7/site-packages/ezdxf/math/eulerspiral.py

@@ -0,0 +1,99 @@
+# Created: 26.03.2010
+# License: MIT License
+from typing import Dict, Iterable
+from ezdxf.math import Vector
+from ezdxf.math.bspline import bspline_control_frame, BSpline
+
+
+class EulerSpiral:
+    """
+    This object represents an euler spiral (clothoid) for *curvature* (Radius of curvature).
+
+    This is a parametric curve, which always starts at the origin = (0, 0).
+
+    """
+
+    def __init__(self, curvature: float = 1.0):
+        self.curvature = curvature  # Radius of curvature
+        self.curvature_powers = [curvature ** power for power in range(19)]
+        self._cache = {}  # type: Dict[float, Vector] # coordinates cache
+
+    def radius(self, t: float) -> float:
+        """
+        Get radius of circle at distance 't'.
+
+        """
+        if t > 0.:
+            return self.curvature_powers[2] / t
+        else:
+            return 0.  # radius = infinite
+
+    def tangent(self, t: float) -> Vector:
+        """
+        Get tangent at distance `t` as Vector() object.
+
+        """
+        angle = t ** 2 / (2. * self.curvature_powers[2])
+        return Vector.from_angle(angle)
+
+    def distance(self, radius: float) -> float:
+        """
+        Get distance L from origin for radius.
+
+        """
+        return self.curvature_powers[2] / float(radius)
+
+    def point(self, t: float) -> Vector:
+        """
+        Get point at distance `t` as Vector().
+
+        """
+
+        def term(length_power, curvature_power, const):
+            return t ** length_power / (const * self.curvature_powers[curvature_power])
+
+        if t not in self._cache:
+            y = term(3, 2, 6.) - term(7, 6, 336.) + term(11, 10, 42240.) - \
+                term(15, 14, 9676800.) + term(19, 18, 3530096640.)
+            x = t - term(5, 4, 40.) + term(9, 8, 3456.) - term(13, 12, 599040.) + \
+                term(17, 16, 175472640.)
+            self._cache[t] = Vector(x, y)
+        return self._cache[t]
+
+    def approximate(self, length: float, segments: int) -> Iterable[Vector]:
+        """
+        Approximate curve of length with line segments.
+
+        Generates segments+1 vertices as Vector() objects.
+
+        """
+        delta_l = float(length) / float(segments)
+        yield Vector(0, 0)
+        for index in range(1, segments + 1):
+            yield self.point(delta_l * index)
+
+    def circle_midpoint(self, t: float) -> Vector:
+        """
+        Get circle midpoint at distance `t`.
+
+        """
+        p = self.point(t)
+        r = self.radius(t)
+        return p + self.tangent(t).normalize(r).orthogonal()
+
+    def bspline(self, length: float, segments: int = 10, degree: int = 3, method: str = 'uniform') -> BSpline:
+        """
+        Approximate euler spiral by B-spline.
+
+        Args:
+            length: length of euler spiral
+            segments: count of fit points for B-spline calculation
+            degree: degree of BSpline
+            method: calculation method for parameter vector t
+
+        """
+        fit_points = list(self.approximate(length, segments=segments))
+        spline = bspline_control_frame(fit_points, degree, method=method)
+        knots = [v * length for v in spline.knot_values()]  # scale knot values to length
+        spline.basis.knots = knots
+        return spline

+ 274 - 0
venv/lib/python3.7/site-packages/ezdxf/math/line.py

@@ -0,0 +1,274 @@
+# Created: 13.03.2010
+# Copyright (c) 2010, Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Optional
+import math
+from .construct2d import ConstructionTool
+from .bbox import BoundingBox2d
+from .vector import Vec2
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+class ParallelRaysError(ArithmeticError):
+    pass
+
+
+HALF_PI = math.pi / 2.
+THREE_PI_HALF = 1.5 * math.pi
+DOUBLE_PI = math.pi * 2.
+
+
+class ConstructionRay:
+    """
+    Infinite construction ray.
+
+    Args:
+        p1: definition point 1
+        p2: ray direction as 2nd point or None
+        angle: ray direction as angle in radians or None
+
+    """
+
+    def __init__(self, p1: 'Vertex', p2: 'Vertex' = None, angle: float = None):
+        self._location = Vec2(p1)
+        if p2 is not None:
+            p2 = Vec2(p2)
+            if self._location.x < p2.x:
+                self._direction = (p2 - self._location).normalize()
+            else:
+                self._direction = (self._location - p2).normalize()
+            self._angle = self._direction.angle
+        elif angle is not None:
+            self._angle = angle
+            self._direction = Vec2.from_angle(angle)
+        else:
+            raise ValueError('p2 or angle required.')
+
+        if math.isclose(self._direction.x, 0., abs_tol=1e-12):
+            self._slope = None
+            self._yof0 = None
+        else:
+            self._slope = self._direction.y / self._direction.x
+            self._yof0 = self._location.y - self._slope * self._location.x
+        self._is_vertical = self._slope is None
+        self._is_horizontal = math.isclose(self._direction.y, 0., abs_tol=1e-12)
+
+    @property
+    def location(self) -> Vec2:
+        """ Returns location vector of ray. """
+        return self._location
+
+    @property
+    def direction(self) -> Vec2:
+        """ Returns direction vector of ray. """
+        return self._direction
+
+    @property
+    def slope(self) -> float:
+        """ Returns slope of ray or None if vertical. """
+        return self._slope
+
+    @property
+    def angle(self) -> float:
+        """ Returns angle of ray in radians. """
+        return self._angle
+
+    @property
+    def angle_deg(self) -> float:
+        """ Returns angle of ray in degrees. """
+        return math.degrees(self._angle)
+
+    @property
+    def is_vertical(self) -> bool:
+        """ Returns True if ray is vertical. """
+        return self._is_vertical
+
+    @property
+    def is_horizontal(self) -> bool:
+        """ Returns True if ray is horizontal. """
+        return self._is_horizontal
+
+    def __str__(self) -> str:
+        return 'ConstructionRay(x={0._x:.3f}, y={0._y:.3f}, phi={0.angle:.5f} rad)'.format(self)
+
+    def is_parallel(self, other: 'ConstructionRay') -> bool:
+        """
+        Return True if the rays are parallel, else False.
+
+        """
+
+        if self._is_vertical:
+            return other._is_vertical
+
+        if other._is_vertical:
+            return False
+
+        return math.isclose(self._slope, other._slope, abs_tol=1e-12)
+
+    def intersect(self, other: 'ConstructionRay') -> Vec2:
+        """
+        Returns the intersection point (xy-tuple) of self and other_ray.
+
+        Raises:
+             ParallelRaysError: if the rays are parallel
+
+        """
+        ray1 = self
+        ray2 = other
+        if not ray1.is_parallel(ray2):
+            if ray1._is_vertical:
+                x = self._location.x
+                y = ray2.yof(x)
+            elif ray2._is_vertical:
+                x = ray2._location.x
+                y = ray1.yof(x)
+            else:
+                # calc intersection with the 'straight-line-equation'
+                # based on y(x) = y0 + x*slope
+                x = (ray1._yof0 - ray2._yof0) / (ray2._slope - ray1._slope)
+                y = ray1.yof(x)
+            return Vec2((x, y))
+        else:
+            raise ParallelRaysError("Rays are parallel")
+
+    def orthogonal(self, point: 'Vertex') -> 'ConstructionRay':
+        """
+        Returns orthogonal construction ray through `point`.
+
+        """
+        return ConstructionRay(point, angle=self._angle + HALF_PI)
+
+    def yof(self, x: float) -> float:
+        if self._is_vertical:
+            raise ArithmeticError
+        return self._yof0 + float(x) * self._slope
+
+    def xof(self, y: float) -> float:
+        if self._is_vertical:
+            return self._location.x
+        elif not self._is_horizontal:
+            return (float(y) - self._yof0) / self._slope
+        else:
+            raise ArithmeticError
+
+    def bisectrix(self, other: 'ConstructionRay') -> 'ConstructionRay':
+        """
+        Bisectrix between self and other construction ray.
+
+        """
+        if self.is_parallel(other):
+            raise ParallelRaysError
+        intersection = self.intersect(other)
+        alpha = (self._angle + other._angle) / 2.
+        return ConstructionRay(intersection, angle=alpha)
+
+
+class ConstructionLine(ConstructionTool):
+    """
+    ConstructionLine is similar to ConstructionRay, but has a start and endpoint and therefor also an direction.
+    The direction goes from start to end, 'left of line' is always in relation to this line direction.
+
+    """
+
+    def __init__(self, start: 'Vertex', end: 'Vertex'):
+        self.start = Vec2(start)
+        self.end = Vec2(end)
+
+    def __str__(self):
+        return 'ConstructionLine({0.start}, {0.end})'.format(self)
+
+    # ConstructionTool interface
+    @property
+    def bounding_box(self):
+        return BoundingBox2d((self.start, self.end))
+
+    def move(self, dx: float, dy: float) -> None:
+        """
+        Move line about `dx` in x-axis and about `dy` in y-axis.
+
+        Args:
+            dx: translation in x-axis
+            dy: translation in y-axis
+
+        """
+        v = Vec2((dx, dy))
+        self.start += v
+        self.end += v
+
+    @property
+    def sorted_points(self):
+        return (self.end, self.start) if self.start > self.end else (self.start, self.end)
+
+    @property
+    def ray(self):
+        return ConstructionRay(self.start, self.end)
+
+    def __eq__(self, other: 'ConstructionLine') -> bool:
+        return self.sorted_points == other.sorted_points
+
+    def __lt__(self, other: 'ConstructionLine') -> bool:
+        return self.sorted_points < other.sorted_points
+
+    def length(self) -> float:
+        return (self.end - self.start).magnitude
+
+    def midpoint(self) -> Vec2:
+        return self.start.lerp(self.end)
+
+    @property
+    def is_vertical(self) -> bool:
+        return math.isclose(self.start.x, self.end.x)
+
+    def inside_bounding_box(self, point: 'Vertex') -> bool:
+        return self.bounding_box.inside(point)
+
+    def intersect(self, other: 'ConstructionLine') -> Optional[Vec2]:
+        """
+        Returns the intersection point of to lines or None if they have no intersection point.
+
+        Args:
+            other: other construction line
+
+        Returns: intersection point or None
+
+        """
+        try:
+            point = self.ray.intersect(other.ray)
+        except ParallelRaysError:
+            return None
+        else:
+            if self.inside_bounding_box(point) and other.inside_bounding_box(point):
+                return point
+            else:
+                return None
+
+    def has_intersection(self, other: 'ConstructionLine') -> bool:
+        # required because intersection Vector(0, 0, 0) is also False
+        return self.intersect(other) is not None
+
+    def left_of_line(self, point: 'Vertex') -> bool:
+        """
+        True if `point` is left of construction line in relation to the line direction from start to end.
+
+        Points exact at the line are not left of the line.
+
+        """
+        start, end = self.start, self.end
+        point = Vec2(point)
+        if self.is_vertical:
+            # compute on which site of the line self should be
+            should_be_left = self.start.y < self.end.y
+            if should_be_left:
+                return point.x < self.start.x
+            else:
+                return point.x > self.start.x
+        else:
+            y = self.ray.yof(point.x)
+            # compute if point should be above or below the line
+            should_be_above = start.x < end.x
+            if should_be_above:
+                return point.y > y
+            else:
+                return point.y < y

+ 209 - 0
venv/lib/python3.7/site-packages/ezdxf/math/matrix.py

@@ -0,0 +1,209 @@
+# Copyright (c) 2018 Manfred Moitzi
+# License: MIT License
+from typing import Iterable, Tuple, List, Sequence, Union, Any
+from itertools import repeat
+
+
+def zip_to_list(*args) -> Iterable[List]:
+    for e in zip(*args):  # returns immutable tuples
+        yield list(e)  # need mutable list
+
+
+class Matrix:
+    """
+    Simple unoptimized Matrix math.
+
+    Initialization:
+
+        - Matrix(shape=(rows, cols)) -> Matrix filled with zeros
+        - Matrix(matrix[, shape=(rows, cols)]) -> Matrix by copy a Matrix and optional reshape
+        - Matrix([[row_0], [row_1], ..., [row_n]]) -> Matrix from List[List[float]]
+        - Matrix([a1, a2, ..., an], shape=(rows, cols)) -> Matrix from List[float] and shape
+
+    """
+
+    def __init__(self, items: Any = None,
+                 shape: Tuple[int, int] = None,
+                 matrix: List[List[float]] = None):
+        self.matrix = matrix  # type: List[List[float]]
+        if items is None:
+            if shape is not None:
+                self.matrix = Matrix.reshape(repeat(0.), shape).matrix
+            else:  # items is None, shape is None
+                pass
+        elif isinstance(items, Matrix):
+            if shape is None:
+                shape = items.shape
+            self.matrix = Matrix.reshape(items, shape=shape).matrix
+        else:
+            items = list(items)
+            try:
+                self.matrix = [list(row) for row in items]
+            except TypeError:
+                self.matrix = Matrix.reshape(items, shape).matrix
+
+    def __iter__(self) -> Iterable[float]:
+        for row in self.matrix:
+            for item in row:
+                yield item
+
+    @staticmethod
+    def reshape(items: Iterable[float], shape: Tuple[int, int]) -> 'Matrix':
+        items = iter(items)
+        rows, cols = shape
+        return Matrix(matrix=[[next(items) for _ in range(cols)] for _ in range(rows)])
+
+    @property
+    def nrows(self) -> int:
+        return len(self.matrix)
+
+    @property
+    def ncols(self) -> int:
+        return len(self.matrix[0])
+
+    @property
+    def shape(self) -> Tuple[int, int]:
+        return self.nrows, self.ncols
+
+    def row(self, index) -> List[float]:
+        return self.matrix[index]
+
+    def col(self, index) -> List[float]:
+        return [row[index] for row in self.matrix]
+
+    def rows(self) -> List[List[float]]:
+        return self.matrix
+
+    def cols(self) -> List[List[float]]:
+        return [self.col(i) for i in range(self.ncols)]
+
+    def append_row(self, items: Sequence[float]) -> None:
+        if self.matrix is None:
+            self.matrix = [list(items)]
+        elif len(items) == self.ncols:
+            self.matrix.append(items)
+        else:
+            raise ValueError('Invalid item count.')
+
+    def append_col(self, items: Sequence[float]) -> None:
+        if self.matrix is None:
+            self.matrix = [[item] for item in items]
+        elif len(items) == self.nrows:
+            for row, item in zip(self.matrix, items):
+                row.append(item)
+        else:
+            raise ValueError('Invalid item count.')
+
+    def __getitem__(self, item: Tuple[int, int]) -> float:
+        row, col = item
+        return self.matrix[row][col]
+
+    def __setitem__(self, key: Tuple[int, int], value: float):
+        row, col = key
+        self.matrix[row][col] = value
+
+    def __eq__(self, other: 'Matrix') -> bool:
+        if not isinstance(other, Matrix):
+            raise TypeError('Only comparable to class Matrix.')
+        if self.shape != other.shape:
+            raise TypeError('Matrices has to have the same shape.')
+        for row1, row2 in zip(self.matrix, other.matrix):
+            if list(row1) != list(row2):
+                return False
+        return True
+
+    def __mul__(self, other: Union['Matrix', float]) -> 'Matrix':
+        if isinstance(other, Matrix):
+            matrix = Matrix(
+                matrix=[[sum(a * b for a, b in zip(X_row, Y_col)) for Y_col in zip(*other.matrix)] for X_row in
+                        self.matrix])
+        else:
+            factor = float(other)
+            matrix = Matrix.reshape([item * factor for item in self], shape=self.shape)
+        return matrix
+
+    __imul__ = __mul__
+
+    def __add__(self, other: Union['Matrix', float]) -> 'Matrix':
+        if isinstance(other, Matrix):
+            matrix = Matrix.reshape([a + b for a, b in zip(self, other)], shape=self.shape)
+        else:
+            other = float(other)
+            matrix = Matrix.reshape([item + other for item in self], shape=self.shape)
+        return matrix
+
+    __iadd__ = __add__
+
+    def __sub__(self, other: Union['Matrix', float]) -> 'Matrix':
+        if isinstance(other, Matrix):
+            matrix = Matrix.reshape([a - b for a, b in zip(self, other)], shape=self.shape)
+        else:
+            other = float(other)
+            matrix = Matrix.reshape([item - other for item in self], shape=self.shape)
+        return matrix
+
+    __isub__ = __sub__
+
+    def transpose(self) -> 'Matrix':
+        return Matrix(matrix=list(zip_to_list(*self.matrix)))
+
+    def gauss(self, col):
+        m = Matrix(self)
+        m.append_col(col)
+        return gauss(m.matrix)
+
+    def gauss_matrix(self, matrix) -> 'Matrix':
+        B = Matrix(matrix)
+        if self.nrows != B.nrows:
+            raise ValueError('Row count of matrices do not match.')
+        result = [self.gauss(col) for col in B.cols()]
+        return Matrix(items=zip(*result))
+
+
+def gauss(matrix: List[List[float]]) -> List[float]:
+    """
+    Solves a nxn Matrix A x = b, Matrix has 1 column more than rows.
+
+    Args:
+        matrix: matrix [[a11, a12, ..., a1n, b1],
+                   [a21, a22, ..., a2n, b2],
+                   [a21, a22, ..., a2n, b3],
+                   ...
+                   [an1, an2, ..., ann, bn],]
+
+    Returns: x vector as list
+
+    """
+    n = len(matrix)
+
+    for i in range(0, n):
+        # Search for maximum in this column
+        max_element = abs(matrix[i][i])
+        max_row = i
+        for k in range(i + 1, n):
+            if abs(matrix[k][i]) > max_element:
+                max_element = abs(matrix[k][i])
+                max_row = k
+
+        # Swap maximum row with current row (column by column)
+        for k in range(i, n + 1):
+            tmp = matrix[max_row][k]
+            matrix[max_row][k] = matrix[i][k]
+            matrix[i][k] = tmp
+
+        # Make all rows below this one 0 in current column
+        for k in range(i + 1, n):
+            c = -matrix[k][i] / matrix[i][i]
+            for j in range(i, n + 1):
+                if i == j:
+                    matrix[k][j] = 0
+                else:
+                    matrix[k][j] += c * matrix[i][j]
+
+    # Solve equation Ax=b for an upper triangular matrix A
+    x = [0.] * n
+    for i in range(n - 1, -1, -1):
+        x[i] = matrix[i][n] / matrix[i][i]
+        for k in range(i - 1, -1, -1):
+            matrix[k][n] -= matrix[k][i] * x[i]
+    return x

+ 541 - 0
venv/lib/python3.7/site-packages/ezdxf/math/matrix44.py

@@ -0,0 +1,541 @@
+# original code from package: gameobjects
+# Home-page: http://code.google.com/p/gameobjects/
+# Author: Will McGugan
+# Download-URL: http://code.google.com/p/gameobjects/downloads/list
+# Created: 19.04.2010
+# Copyright (c) 2010-2018 Manfred Moitzi
+# License: MIT License
+from typing import Sequence, Iterable, List, Tuple, TYPE_CHECKING
+from math import sin, cos, tan
+from itertools import chain
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+Tuple4Float = Tuple[float, float, float, float]
+
+
+# removed array.array because array is optimized for space not speed, and space optimization is not needed
+
+
+def floats(items: Iterable) -> List[float]:
+    return [float(v) for v in items]
+
+
+class Matrix44:
+    _identity = (
+        1.0, 0.0, 0.0, 0.0,
+        0.0, 1.0, 0.0, 0.0,
+        0.0, 0.0, 1.0, 0.0,
+        0.0, 0.0, 0.0, 1.0
+    )
+    __slots__ = ('matrix',)
+
+    def __init__(self, *args):
+        """
+        Matrix44() is the identity matrix.
+
+        Matrix44(values) values is an iterable with the 16 components of the matrix.
+
+        Matrix44(row1, row2, row3, row4) four rows, each row with four values.
+
+        """
+        self.matrix = None  # type: List
+        self.set(*args)
+
+    def set(self, *args) -> None:
+        """
+        Reset matrix values.
+
+        set() creates the identity matrix.
+
+        set(values) values is an iterable with the 16 components of the matrix.
+
+        set(row1, row2, row3, row4) four rows, each row with four values.
+
+        """
+        nargs = len(args)
+        if nargs == 0:
+            self.matrix = floats(Matrix44._identity)
+        elif nargs == 1:
+            self.matrix = floats(args[0])
+        elif nargs == 4:
+            self.matrix = floats(chain(*args))
+        else:
+            raise ValueError("Invalid count of arguments (4 row vectors or one list with 16 values).")
+        if len(self.matrix) != 16:
+            raise ValueError("Invalid matrix count")
+
+    def __repr__(self) -> str:
+        def format_row(row):
+            return "(%s)" % ", ".join(str(value) for value in row)
+
+        return "Matrix44(%s)" % \
+               ", ".join(format_row(row) for row in self.rows())
+
+    def get_row(self, row: int) -> Tuple4Float:
+        index = row * 4
+        return tuple(self.matrix[index:index + 4])
+
+    def set_row(self, row: int, values: Sequence[float]) -> None:
+        index = row * 4
+        self.matrix[index:index + len(values)] = floats(values)
+
+    def get_col(self, col: int) -> Tuple4Float:
+        """
+        Returns a column as a tuple of four floats.
+        """
+        m = self.matrix
+        return m[col], m[col + 4], m[col + 8], m[col + 12]
+
+    def set_col(self, col: int, values: Sequence[float]):
+        """
+        Sets the values in a column.
+        """
+        m = self.matrix
+        a, b, c, d = values
+        m[col] = float(a)
+        m[col + 4] = float(b)
+        m[col + 8] = float(c)
+        m[col + 12] = float(d)
+
+    def copy(self) -> 'Matrix44':
+        return self.__class__(self.matrix)
+
+    __copy__ = copy
+
+    @classmethod
+    def scale(cls, sx: float, sy: float = None, sz: float = None) -> 'Matrix44':
+        """
+        Returns a scaling transformation matrix. If sy is None, sy = sx, and if sz is None sz = sx.
+
+        """
+        if sy is None:
+            sy = sx
+        if sz is None:
+            sz = sx
+
+        return cls([
+            float(sx), 0., 0., 0.,
+            0., float(sy), 0., 0.,
+            0., 0., float(sz), 0.,
+            0., 0., 0., 1.
+        ])
+
+    @classmethod
+    def translate(cls, x: float, y: float, z: float) -> 'Matrix44':
+        """
+        Returns a translation matrix to (x, y, z).
+
+        """
+        return cls([
+            1., 0., 0., 0.,
+            0., 1., 0., 0.,
+            0., 0., 1., 0.,
+            float(x), float(y), float(z), 1.
+        ])
+
+    @classmethod
+    def x_rotate(cls, angle: float) -> 'Matrix44':
+        """
+        Returns a rotation matrix about the x-axis.
+
+        Args:
+            angle: rotation angle in radians
+
+        """
+        cos_a = cos(angle)
+        sin_a = sin(angle)
+        return cls([
+            1., 0., 0., 0.,
+            0., cos_a, sin_a, 0.,
+            0., -sin_a, cos_a, 0.,
+            0., 0., 0., 1.
+        ])
+
+    @classmethod
+    def y_rotate(cls, angle: float) -> 'Matrix44':
+        """
+        Returns a rotation matrix about the y-axis.
+
+        Args:
+            angle: rotation angle in radians
+
+        """
+        cos_a = cos(angle)
+        sin_a = sin(angle)
+        return cls([
+            cos_a, 0., -sin_a, 0.,
+            0., 1., 0., 0.,
+            sin_a, 0., cos_a, 0.,
+            0., 0., 0., 1.
+        ])
+
+    @classmethod
+    def z_rotate(cls, angle: float) -> 'Matrix44':
+        """
+        Returns a rotation matrix about the z-axis.
+
+        Args:
+            angle: rotation angle in radians
+
+        """
+        cos_a = cos(angle)
+        sin_a = sin(angle)
+        return cls([
+            cos_a, sin_a, 0., 0.,
+            -sin_a, cos_a, 0., 0.,
+            0., 0., 1., 0.,
+            0., 0., 0., 1.
+        ])
+
+    @classmethod
+    def axis_rotate(cls, axis: 'Vertex', angle: float) -> 'Matrix44':
+        """
+        Returns a rotation matrix about an arbitrary axis.
+
+        Args:
+            axis: rotation axis as (x, y, z) tuple
+            angle: rotation angle in radians
+
+        """
+        c = cos(angle)
+        s = sin(angle)
+        omc = 1. - c
+        x, y, z = axis
+        return cls([
+            x * x * omc + c, y * x * omc + z * s, x * z * omc - y * s, 0.,
+            x * y * omc - z * s, y * y * omc + c, y * z * omc + x * s, 0.,
+            x * z * omc + y * s, y * z * omc - x * s, z * z * omc + c, 0.,
+            0., 0., 0., 1.
+        ])
+
+    @classmethod
+    def xyz_rotate(cls, angle_x: float, angle_y: float, angle_z: float) -> 'Matrix44':
+        """
+        Returns a rotation matrix for rotation about each axis.
+
+        Args:
+            angle_x: rotation angle about x-axis in radians
+            angle_y: rotation angle about y-axis in radians
+            angle_z: rotation angle about z-axis in radians
+
+        """
+        cx = cos(angle_x)
+        sx = sin(angle_x)
+        cy = cos(angle_y)
+        sy = sin(angle_y)
+        cz = cos(angle_z)
+        sz = sin(angle_z)
+
+        sxsy = sx * sy
+        cxsy = cx * sy
+
+        return cls([
+            cy * cz, sxsy * cz + cx * sz, -cxsy * cz + sx * sz, 0.,
+            -cy * sz, -sxsy * sz + cx * cz, cxsy * sz + sx * cz, 0.,
+            sy, -sx * cy, cx * cy, 0.,
+            0., 0., 0., 1.])
+
+    @classmethod
+    def perspective_projection(cls, left: float, right: float, top: float, bottom: float, near: float,
+                               far: float) -> 'Matrix44':
+        """
+        Returns a matrix for a 2d projection.
+
+        Args:
+            left: Coordinate of left of screen
+            right: Coordinate of right of screen
+            top: Coordinate of the top of the screen
+            bottom: Coordinate of the bottom of the screen
+            near: Coordinate of the near clipping plane
+            far: Coordinate of the far clipping plane
+
+        """
+        return cls([
+            (2. * near) / (right - left), 0., 0., 0.,
+            0., (2. * near) / (top - bottom), 0., 0.,
+            (right + left) / (right - left), (top + bottom) / (top - bottom), -((far + near) / (far - near)), -1.,
+            0., 0., -((2. * far * near) / (far - near)), 0.
+        ])
+
+    @classmethod
+    def perspective_projection_fov(cls, fov: float, aspect: float, near: float, far: float) -> 'Matrix44':
+        """
+        Returns a matrix for a 2d projection.
+
+        Args:
+            fov: The field of view (in radians)
+            aspect: The aspect ratio of the screen (width / height)
+            near: Coordinate of the near clipping plane
+            far: Coordinate of the far clipping plane
+
+        """
+        vrange = near * tan(fov / 2.)
+        left = -vrange * aspect
+        right = vrange * aspect
+        bottom = -vrange
+        top = vrange
+        return cls.perspective_projection(left, right, bottom, top, near, far)
+
+    @staticmethod
+    def chain(*matrices: Iterable['Matrix44']) -> 'Matrix44':
+        """
+        Compose a transformation matrix from one or more matrices.
+
+        """
+        transformation = Matrix44()
+        for matrix in matrices:
+            transformation *= matrix
+        return transformation
+
+    @staticmethod
+    def ucs(ux: 'Vertex', uy: 'Vertex', uz: 'Vertex') -> 'Matrix44':
+        """
+        Returns a matrix for coordinate transformation from WCS to UCS.
+        Origin of both systems is (0, 0, 0).
+        For transformation from UCS to WCS, transpose the returned matrix.
+
+        All vectors as (x, y, z) tuples.
+
+        Args:
+            ux: x-axis for UCS as unit vector
+            uy: y-axis for UCS as unit vector
+            uz: z-axis for UCS as unit vector
+
+        Returns: Matrix44() object.
+
+        """
+        ux_x, ux_y, ux_z = ux
+        uy_x, uy_y, uy_z = uy
+        uz_x, uz_y, uz_z = uz
+        return Matrix44((
+            ux_x, uy_x, uz_x, 0,
+            ux_y, uy_y, uz_y, 0,
+            ux_z, uy_z, uz_z, 0,
+            0, 0, 0, 1,
+        ))
+
+    def __hash__(self) -> int:
+        return self.matrix.__hash__()
+
+    def __setitem__(self, coord: Tuple[int, int], value: float):
+        """
+        Set (row, column) element.
+
+        """
+        row, col = coord
+        self.matrix[row * 4 + col] = float(value)
+
+    def __getitem__(self, coord: Tuple[int, int]):
+        """
+        Get (row, column) element.
+
+        """
+        row, col = coord
+        return self.matrix[row * 4 + col]
+
+    def __iter__(self) -> Iterable[float]:
+        """
+        Iterates over all matrix values.
+
+        """
+        return iter(self.matrix)
+
+    def __mul__(self, other: 'Matrix44') -> 'Matrix44':
+        """
+        Returns a new matrix as result of the matrix multiplication with another matrix.
+
+        """
+        res_matrix = self.copy()
+        res_matrix.__imul__(other)
+        return res_matrix
+
+    def __imul__(self, other: 'Matrix44') -> 'Matrix44':
+        """
+        Inplace multiplication with another matrix.
+
+        """
+        m1 = self.matrix
+        m2 = other.matrix
+        self.matrix = [
+            m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8] + m1[3] * m2[12],
+            m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9] + m1[3] * m2[13],
+            m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10] + m1[3] * m2[14],
+            m1[0] * m2[3] + m1[1] * m2[7] + m1[2] * m2[11] + m1[3] * m2[15],
+
+            m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8] + m1[7] * m2[12],
+            m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9] + m1[7] * m2[13],
+            m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10] + m1[7] * m2[14],
+            m1[4] * m2[3] + m1[5] * m2[7] + m1[6] * m2[11] + m1[7] * m2[15],
+
+            m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8] + m1[11] * m2[12],
+            m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9] + m1[11] * m2[13],
+            m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10] + m1[11] * m2[14],
+            m1[8] * m2[3] + m1[9] * m2[7] + m1[10] * m2[11] + m1[11] * m2[15],
+
+            m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + m1[15] * m2[12],
+            m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + m1[15] * m2[13],
+            m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + m1[15] * m2[14],
+            m1[12] * m2[3] + m1[13] * m2[7] + m1[14] * m2[11] + m1[15] * m2[15]
+        ]
+        return self
+
+    def fast_mul(self, other: 'Matrix44') -> 'Matrix44':
+        """
+        Multiplies this matrix with other matrix.
+
+        Assumes that both matrices have a right column of (0, 0, 0, 1). This is True for matrices composed of
+        rotations,  translations and scales. fast_mul is approximately 25% quicker than the *= operator.
+
+        """
+        m1 = self.matrix
+        m2 = other.matrix
+        self.matrix = [
+            m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8],
+            m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9],
+            m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10],
+            0.0,
+
+            m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8],
+            m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9],
+            m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10],
+            0.0,
+
+            m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8],
+            m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9],
+            m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10],
+            0.0,
+
+            m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + m2[12],
+            m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + m2[13],
+            m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + m2[14],
+            1.0
+        ]
+        return self
+
+    def rows(self) -> Iterable[Tuple4Float]:
+        """
+        Iterate over rows as 4-tuples.
+
+        """
+        return (self.get_row(index) for index in (0, 1, 2, 3))
+
+    def columns(self) -> Iterable[Tuple4Float]:
+        """
+        Iterate over columns as 4-tuples.
+
+        """
+        return (self.get_col(index) for index in (0, 1, 2, 3))
+
+    def transform(self, vector: 'Vertex') -> Tuple[float, float, float]:
+        """
+        Transforms a 3d vector and return the result as a tuple.
+
+        """
+        m = self.matrix
+        x, y, z = vector
+        return (x * m[0] + y * m[4] + z * m[8] + m[12],
+                x * m[1] + y * m[5] + z * m[9] + m[13],
+                x * m[2] + y * m[6] + z * m[10] + m[14])
+
+    def transform_vectors(self, vectors: Iterable['Vertex']) -> List['Vertex']:
+        """
+        Returns a list of transformed vectors.
+
+        """
+        result = []
+        m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15 = self.matrix
+        for vector in vectors:
+            x, y, z = vector
+            result.append((
+                x * m0 + y * m4 + z * m8 + m12,
+                x * m1 + y * m5 + z * m9 + m13,
+                x * m2 + y * m6 + z * m10 + m14
+            ))
+        return result
+
+    def transpose(self) -> None:
+        """
+        Swaps the rows for columns inplace.
+
+        """
+        m00, m01, m02, m03, \
+        m10, m11, m12, m13, \
+        m20, m21, m22, m23, \
+        m30, m31, m32, m33 = self.matrix
+
+        self.matrix = [
+            m00, m10, m20, m30,
+            m01, m11, m21, m31,
+            m02, m12, m22, m32,
+            m03, m13, m23, m33
+        ]
+
+    def get_transpose(self) -> 'Matrix44':
+        """
+        Returns a new transposed matrix.
+
+        """
+        matrix = self.copy()
+        matrix.transpose()
+        return matrix
+
+    def determinant(self) -> float:
+        e11, e12, e13, e14, \
+        e21, e22, e23, e24, \
+        e31, e32, e33, e34, \
+        e41, e42, e43, e44 = self.matrix
+        return e11 * e22 * e33 * e44 - e11 * e22 * e34 * e43 + e11 * e23 * e34 * e42 - e11 * e23 * e32 * e44 + \
+               e11 * e24 * e32 * e43 - e11 * e24 * e33 * e42 - e12 * e23 * e34 * e41 + e12 * e23 * e31 * e44 - \
+               e12 * e24 * e31 * e43 + e12 * e24 * e33 * e41 - e12 * e21 * e33 * e44 + e12 * e21 * e34 * e43 + \
+               e13 * e24 * e31 * e42 - e13 * e24 * e32 * e41 + e13 * e21 * e32 * e44 - e13 * e21 * e34 * e42 + \
+               e13 * e22 * e34 * e41 - e13 * e22 * e31 * e44 - e14 * e21 * e32 * e43 + e14 * e21 * e33 * e42 - \
+               e14 * e22 * e33 * e41 + e14 * e22 * e31 * e43 - e14 * e23 * e31 * e42 + e14 * e23 * e32 * e41
+
+    def inverse(self) -> None:
+        """
+        Calculates the inverse of the matrix.
+
+        Raises ZeroDivisionError if matrix has no inverse.
+        """
+        det = self.determinant()
+        f = 1. / det  # catch ZeroDivisionError by caller
+        m00, m01, m02, m03, \
+        m10, m11, m12, m13, \
+        m20, m21, m22, m23, \
+        m30, m31, m32, m33 = self.matrix
+        self.matrix = [
+            (
+                    m12 * m23 * m31 - m13 * m22 * m31 + m13 * m21 * m32 - m11 * m23 * m32 - m12 * m21 * m33 + m11 * m22 * m33) * f,
+            (
+                    m03 * m22 * m31 - m02 * m23 * m31 - m03 * m21 * m32 + m01 * m23 * m32 + m02 * m21 * m33 - m01 * m22 * m33) * f,
+            (
+                    m02 * m13 * m31 - m03 * m12 * m31 + m03 * m11 * m32 - m01 * m13 * m32 - m02 * m11 * m33 + m01 * m12 * m33) * f,
+            (
+                    m03 * m12 * m21 - m02 * m13 * m21 - m03 * m11 * m22 + m01 * m13 * m22 + m02 * m11 * m23 - m01 * m12 * m23) * f,
+            (
+                    m13 * m22 * m30 - m12 * m23 * m30 - m13 * m20 * m32 + m10 * m23 * m32 + m12 * m20 * m33 - m10 * m22 * m33) * f,
+            (
+                    m02 * m23 * m30 - m03 * m22 * m30 + m03 * m20 * m32 - m00 * m23 * m32 - m02 * m20 * m33 + m00 * m22 * m33) * f,
+            (
+                    m03 * m12 * m30 - m02 * m13 * m30 - m03 * m10 * m32 + m00 * m13 * m32 + m02 * m10 * m33 - m00 * m12 * m33) * f,
+            (
+                    m02 * m13 * m20 - m03 * m12 * m20 + m03 * m10 * m22 - m00 * m13 * m22 - m02 * m10 * m23 + m00 * m12 * m23) * f,
+            (
+                    m11 * m23 * m30 - m13 * m21 * m30 + m13 * m20 * m31 - m10 * m23 * m31 - m11 * m20 * m33 + m10 * m21 * m33) * f,
+            (
+                    m03 * m21 * m30 - m01 * m23 * m30 - m03 * m20 * m31 + m00 * m23 * m31 + m01 * m20 * m33 - m00 * m21 * m33) * f,
+            (
+                    m01 * m13 * m30 - m03 * m11 * m30 + m03 * m10 * m31 - m00 * m13 * m31 - m01 * m10 * m33 + m00 * m11 * m33) * f,
+            (
+                    m03 * m11 * m20 - m01 * m13 * m20 - m03 * m10 * m21 + m00 * m13 * m21 + m01 * m10 * m23 - m00 * m11 * m23) * f,
+            (
+                    m12 * m21 * m30 - m11 * m22 * m30 - m12 * m20 * m31 + m10 * m22 * m31 + m11 * m20 * m32 - m10 * m21 * m32) * f,
+            (
+                    m01 * m22 * m30 - m02 * m21 * m30 + m02 * m20 * m31 - m00 * m22 * m31 - m01 * m20 * m32 + m00 * m21 * m32) * f,
+            (
+                    m02 * m11 * m30 - m01 * m12 * m30 - m02 * m10 * m31 + m00 * m12 * m31 + m01 * m10 * m32 - m00 * m11 * m32) * f,
+            (
+                    m01 * m12 * m20 - m02 * m11 * m20 + m02 * m10 * m21 - m00 * m12 * m21 - m01 * m10 * m22 + m00 * m11 * m22) * f,
+        ]

+ 20 - 0
venv/lib/python3.7/site-packages/ezdxf/math/points.py

@@ -0,0 +1,20 @@
+# Copyright (c) 2019 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Iterable
+from ezdxf.math.vector import Vector
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+def closest_point(base: 'Vertex', points: Iterable['Vertex']) -> Vector:
+    base = Vector(base)
+    min_dist = None
+    found = None
+    for point in points:
+        p = Vector(point)
+        dist = (base - p).magnitude
+        if (min_dist is None) or (dist < min_dist):
+            min_dist = dist
+            found = p
+    return found

+ 62 - 0
venv/lib/python3.7/site-packages/ezdxf/math/shape.py

@@ -0,0 +1,62 @@
+# Created: 2019-01-04
+# Copyright (c) 2019 Manfred Moitzi
+# License: MIT License
+from typing import Union, Iterable, List, TYPE_CHECKING
+import math
+from .vector import Vec2
+from .construct2d import ConstructionTool
+from .bbox import BoundingBox2d
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import Vertex
+
+
+class Shape2d(ConstructionTool):
+    """
+    2d geometry object as vertices list which can be moved, rotated and scaled.
+
+    """
+
+    def __init__(self, vertices: Iterable['Vertex'] = None):
+        self.vertices = [] if vertices is None else Vec2.list(vertices)  # type: List[Vec2]
+
+    def move(self, dx: float, dy: float) -> None:
+        self.translate(Vec2((dx, dy)))
+
+    def bounding_box(self)->BoundingBox2d:
+        return BoundingBox2d(self.vertices)
+
+    def translate(self, vector: 'Vertex') -> None:
+        delta = Vec2(vector)
+        self.vertices = [v + delta for v in self.vertices]
+
+    def scale(self, sx: float = 1., sy: float = 1.) -> None:
+        self.vertices = [Vec2((v.x * sx, v.y * sy)) for v in self.vertices]
+
+    def scale_uniform(self, scale: float) -> None:
+        self.vertices = [v * scale for v in self.vertices]
+
+    def rotate(self, angle: float, center: 'Vertex' = None) -> None:
+        self.rotate_rad(math.radians(angle), center)
+
+    def rotate_rad(self, angle: float, center: 'Vertex' = None) -> None:
+        if center is not None:
+            center = Vec2(center)
+            self.translate(-center)  # faster than a Matrix44 multiplication
+        self.vertices = [v.rotate(angle) for v in self.vertices]
+        if center is not None:
+            self.translate(center)  # faster than a Matrix44 multiplication
+
+    # Sequence interface
+    def __len__(self) -> int:
+        return len(self.vertices)
+
+    def __getitem__(self, item: Union[int, slice]) -> Vec2:
+        return self.vertices[item]
+
+    # limited List interface
+    def append(self, vertex: 'Vertex') -> None:
+        self.vertices.append(Vec2(vertex))
+
+    def extend(self, vertices: Iterable) -> None:
+        self.vertices.extend(Vec2.generate(vertices))

+ 46 - 0
venv/lib/python3.7/site-packages/ezdxf/math/surfaces.py

@@ -0,0 +1,46 @@
+# Copyright (c) 2012 Manfred Moitzi
+# License: MIT License
+from typing import List, Sequence
+from .bezier import bernstein_basis
+
+Vector = Sequence[float]
+
+
+class BezierSurface:
+    def __init__(self, defpoints: List[List[Vector]]):
+        """
+        BezierSurface constructor
+
+        Args:
+            defpoints: defpoints is matrix (list of lists) of m rows and n columns.
+            [ [m1n1, m1n2, ... ], [m2n1, m2n2, ...] ... ]
+            each element is a 3D point (x, y, z) tuple or list
+
+        """
+        self._defpoints = defpoints
+        self.nrows = len(defpoints)
+        self.ncols = len(defpoints[0])
+
+    def appoximate(self, usegs: int, vsegs: int) -> List[List[Vector]]:
+        stepu = 1.0 / float(usegs)
+        stepv = 1.0 / float(vsegs)
+        result = [[None] * self.ncols] * self.nrows  # type: List[List[Vector]]
+        for u_index in range(usegs + 1):
+            u = stepu * u_index
+            for v_index in range(vsegs + 1):
+                v = stepv * v_index
+                result[u_index][v_index] = self.get_point(u, v)
+        return result
+
+    def get_point(self, u: float, v: float) -> Vector:
+        """ u, v in range [0.0, 1.0].
+        """
+        point = [0.0, 0.0, 0.0]
+        for irow in range(self.nrows):
+            rowbasis = bernstein_basis(self.nrows, irow, u)
+            row = self._defpoints[irow]
+            for col in range(self.ncols):
+                colbasis = bernstein_basis(self.ncols, col, v)
+                for axis in (0, 1, 2):
+                    point[axis] += row[col][axis] * rowbasis * colbasis
+                return point

+ 314 - 0
venv/lib/python3.7/site-packages/ezdxf/math/ucs.py

@@ -0,0 +1,314 @@
+# Copyright (c) 2018 Manfred Moitzi
+# License: MIT License
+from typing import TYPE_CHECKING, Tuple, Sequence, Iterable
+from .vector import Vector, X_AXIS, Y_AXIS, Z_AXIS
+
+if TYPE_CHECKING:
+    from ezdxf.eztypes import GenericLayoutType, Vertex
+
+
+def render_axis(layout: 'GenericLayoutType',
+                start: 'Vertex',
+                points: Sequence['Vertex'],
+                colors: Tuple[int, int, int] = (1, 3, 5)) -> None:
+    for point, color in zip(points, colors):
+        layout.add_line(start, point, dxfattribs={'color': color})
+
+
+class Matrix33:
+    """
+    Simple 3x3 Matrix for coordinate transformation.
+
+    """
+    __slots__ = ('ux', 'uy', 'uz')
+
+    def __init__(self, ux: 'Vertex' = (1, 0, 0), uy: 'Vertex' = (0, 1, 0), uz: 'Vertex' = (0, 0, 1)):
+        self.ux = Vector(ux)
+        self.uy = Vector(uy)
+        self.uz = Vector(uz)
+
+    def transpose(self) -> 'Matrix33':
+        return Matrix33(
+            (self.ux.x, self.uy.x, self.uz.x),
+            (self.ux.y, self.uy.y, self.uz.y),
+            (self.ux.z, self.uy.z, self.uz.z),
+        )
+
+    def transform(self, vector: 'Vertex') -> Vector:
+        px, py, pz = Vector(vector)
+        ux = self.ux
+        uy = self.uy
+        uz = self.uz
+        x = px * ux.x + py * uy.x + pz * uz.x
+        y = px * ux.y + py * uy.y + pz * uz.y
+        z = px * ux.z + py * uy.z + pz * uz.z
+        return Vector(x, y, z)
+
+
+class OCS:
+    def __init__(self, extrusion: 'Vertex' = Z_AXIS):
+        Az = Vector(extrusion).normalize()
+        self.transform = not Az.isclose(Z_AXIS)
+        if self.transform:
+            if (abs(Az.x) < 1 / 64.) and (abs(Az.y) < 1 / 64.):
+                Ax = Y_AXIS.cross(Az)
+            else:
+                Ax = Z_AXIS.cross(Az)
+            Ax = Ax.normalize()
+            Ay = Az.cross(Ax).normalize()
+            self.matrix = Matrix33(Ax, Ay, Az)
+            self.transpose = self.matrix.transpose()
+
+    @property
+    def ux(self) -> Vector:
+        return self.matrix.ux if self.transform else X_AXIS
+
+    @property
+    def uy(self) -> Vector:
+        return self.matrix.uy if self.transform else Y_AXIS
+
+    @property
+    def uz(self) -> Vector:
+        return self.matrix.uz if self.transform else Z_AXIS
+
+    def from_wcs(self, point: 'Vertex') -> 'Vertex':
+        if self.transform:
+            return self.transpose.transform(point)
+        else:
+            return point
+
+    def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable['Vertex']:
+        for point in points:
+            yield self.from_wcs(point)
+
+    def to_wcs(self, point: 'Vertex') -> 'Vertex':
+        if self.transform:
+            return self.matrix.transform(point)
+        else:
+            return point
+
+    def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable['Vertex']:
+        for point in points:
+            yield self.to_wcs(point)
+
+    def render_axis(self, layout: 'GenericLayoutType', length: float = 1, colors: Tuple[int, int, int] = (1, 3, 5)):
+        render_axis(
+            layout,
+            start=(0, 0, 0),
+            points=(
+                self.to_wcs(X_AXIS * length),
+                self.to_wcs(Y_AXIS * length),
+                self.to_wcs(Z_AXIS * length),
+            ),
+            colors=colors,
+        )
+
+
+class UCS:
+    def __init__(self, origin: 'Vertex' = (0, 0, 0), ux: 'Vertex' = None, uy: 'Vertex' = None, uz: 'Vertex' = None):
+        self.origin = Vector(origin)
+        if ux is None and uy is None:
+            ux = X_AXIS
+            uy = Y_AXIS
+            uz = Z_AXIS
+        elif ux is None:
+            uy = Vector(uy).normalize()
+            uz = Vector(uz).normalize()
+            ux = Vector(uy).cross(uz).normalize()
+        elif uy is None:
+            ux = Vector(ux).normalize()
+            uz = Vector(uz).normalize()
+            uy = Vector(uz).cross(ux).normalize()
+        elif uz is None:
+            ux = Vector(ux).normalize()
+            uy = Vector(uy).normalize()
+            uz = Vector(ux).cross(uy).normalize()
+        else:  # all axis are given
+            ux = Vector(ux).normalize()
+            uy = Vector(uy).normalize()
+            uz = Vector(uz).normalize()
+
+        self.matrix = Matrix33(ux, uy, uz)
+        self.transpose = self.matrix.transpose()
+
+    @property
+    def ux(self) -> Vector:
+        return self.matrix.ux
+
+    @property
+    def uy(self) -> Vector:
+        return self.matrix.uy
+
+    @property
+    def uz(self) -> Vector:
+        return self.matrix.uz
+
+    def to_wcs(self, point: 'Vertex') -> Vector:
+        """
+        Calculate world coordinates for point in UCS coordinates.
+
+        """
+        return self.origin + self.matrix.transform(point)
+
+    def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]:
+        """
+        Translate multiple user coordinates into world coordinates (generator).
+
+        """
+        for point in points:
+            yield self.to_wcs(point)
+
+    def to_ocs(self, point: 'Vertex') -> 'Vertex':
+        """
+        Calculate OCS coordinates for point in UCS coordinates.
+
+        OCS is defined by the z-axis of the UCS.
+
+        """
+        wpoint = self.to_wcs(point)
+        return OCS(self.uz).from_wcs(wpoint)
+
+    def points_to_ocs(self, points: Iterable['Vertex']) -> Iterable['Vertex']:
+        """
+        Translate multiple user coordinates into OCS coordinates (generator).
+
+        OCS is defined by the z-axis of the UCS.
+
+        """
+        wcs = self.to_wcs
+        ocs = OCS(self.uz)
+        for point in points:
+            yield ocs.from_wcs(wcs(point))
+
+    def to_ocs_angle_deg(self, angle: float) -> float:
+        """
+        Transform angle in UCS xy-plane to angle in OCS xy-plane.
+
+        Args:
+            angle: in UCS in degrees
+
+        Returns: angle in OCS in degrees
+
+        """
+        vec = Vector.from_deg_angle(angle)
+        vec = self.to_ocs(vec) - self.origin
+        return vec.angle_deg
+
+    def to_ocs_angle_rad(self, angle: float) -> float:
+        """
+        Transform angle in UCS xy-plane to angle in OCS xy-plane.
+
+        Args:
+            angle: in UCS in radians
+
+        Returns: angle in OCS in radians
+
+        """
+        vec = Vector.from_angle(angle)
+        vec = self.to_ocs(vec) - self.origin
+        return vec.angle
+
+    def from_wcs(self, point: 'Vertex') -> Vector:
+        """
+        Calculate UCS coordinates for point in world coordinates.
+
+        """
+        return self.transpose.transform(point - self.origin)
+
+    def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]:
+        """
+        Translate multiple world coordinates into user coordinates (generator).
+
+        """
+        for point in points:
+            yield self.from_wcs(point)
+
+    @property
+    def is_cartesian(self) -> bool:
+        return self.uy.cross(self.uz).isclose(self.ux)
+
+    @staticmethod
+    def from_x_axis_and_point_in_xy(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        x_axis = Vector(axis)
+        z_axis = x_axis.cross(Vector(point) - origin)
+        return UCS(origin=origin, ux=x_axis, uz=z_axis)
+
+    @staticmethod
+    def from_x_axis_and_point_in_xz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        x_axis = Vector(axis)
+        xz_vector = Vector(point) - origin
+        y_axis = xz_vector.cross(x_axis)
+        return UCS(origin=origin, ux=x_axis, uy=y_axis)
+
+    @staticmethod
+    def from_y_axis_and_point_in_xy(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        y_axis = Vector(axis)
+        xy_vector = Vector(point) - origin
+        z_axis = xy_vector.cross(y_axis)
+        return UCS(origin=origin, uy=y_axis, uz=z_axis)
+
+    @staticmethod
+    def from_y_axis_and_point_in_yz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        y_axis = Vector(axis)
+        yz_vector = Vector(point) - origin
+        x_axis = yz_vector.cross(y_axis)
+        return UCS(origin=origin, ux=x_axis, uy=y_axis)
+
+    @staticmethod
+    def from_z_axis_and_point_in_xz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        z_axis = Vector(axis)
+        y_axis = z_axis.cross(Vector(point) - origin)
+        return UCS(origin=origin, uy=y_axis, uz=z_axis)
+
+    @staticmethod
+    def from_z_axis_and_point_in_yz(origin: 'Vertex', axis: 'Vertex', point: 'Vertex') -> 'UCS':
+        z_axis = Vector(axis)
+        yz_vector = Vector(point) - origin
+        x_axis = yz_vector.cross(z_axis)
+        return UCS(origin=origin, ux=x_axis, uz=z_axis)
+
+    def render_axis(self, layout: 'GenericLayoutType', length: float = 1, colors: Tuple[int, int, int] = (1, 3, 5)):
+        render_axis(
+            layout,
+            start=self.origin,
+            points=(
+                self.to_wcs(X_AXIS * length),
+                self.to_wcs(Y_AXIS * length),
+                self.to_wcs(Z_AXIS * length),
+            ),
+            colors=colors,
+        )
+
+
+class PassTroughUCS(UCS):
+    """ UCS is equal to the WCS and OCS (extrusion = 0, 0, 1) """
+
+    def __init__(self):
+        super().__init__()
+
+    def to_wcs(self, point: 'Vertex') -> Vector:
+        return Vector(point)
+
+    def points_to_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]:
+        for point in points:
+            yield Vector(point)
+
+    def to_ocs(self, point: 'Vertex') -> 'Vertex':
+        return Vector(point)
+
+    def points_to_ocs(self, points: Iterable['Vertex']) -> Iterable['Vertex']:
+        for point in points:
+            yield Vector(point)
+
+    def to_ocs_angle_deg(self, angle: float) -> float:
+        return angle
+
+    def to_ocs_angle_rad(self, angle: float) -> float:
+        return angle
+
+    def from_wcs(self, point: 'Vertex') -> Vector:
+        return Vector(point)
+
+    def points_from_wcs(self, points: Iterable['Vertex']) -> Iterable[Vector]:
+        for point in points:
+            yield Vector(point)

+ 0 - 0
venv/lib/python3.7/site-packages/ezdxf/math/vector.py


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov