├── huygens ├── doc │ ├── __init__.py │ ├── requirements.txt │ ├── images │ │ ├── Christiaan_Huygens.png │ │ ├── output-0.svg │ │ ├── empty.svg │ │ ├── output-2.svg │ │ ├── output-4.svg │ │ ├── output-6.svg │ │ ├── output-3.svg │ │ ├── vbox-empty.svg │ │ ├── output-9.svg │ │ ├── table.svg │ │ ├── obox.svg │ │ ├── output-11.svg │ │ ├── output-12.svg │ │ ├── output-7.svg │ │ ├── output-8.svg │ │ ├── output-1.svg │ │ ├── text.svg │ │ ├── yang-baxter.svg │ │ ├── canbox.svg │ │ ├── braid-Z.svg │ │ ├── table-2.svg │ │ ├── braid-strands.svg │ │ ├── vbox-text.svg │ │ └── hbox-text.svg │ ├── test_sat.py │ ├── superstyle.css │ ├── test_turtle.py │ ├── test_canvas.py │ ├── test_text.py │ ├── index.md │ ├── run_tests.py │ ├── test_box.py │ ├── mkdoc.py │ ├── template.html │ └── test_sat.html ├── .gitignore ├── all.py ├── wiggle │ ├── __init__.py │ ├── test_wiggle.py │ └── shapes.py ├── tool.py ├── __init__.py ├── argv.py ├── variable.py ├── namespace.py ├── loadsvg.py ├── text.py ├── huytex.py ├── live.py ├── flatten.py └── turtle.py ├── requirements.txt ├── doc ├── Makefile ├── turtle.py ├── canvas.py └── make_nb.py ├── pyproject.toml ├── setup.py └── README.md /huygens/doc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /huygens/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | __huygens__ 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycairo>=1.11.0 2 | cairosvg 3 | scipy 4 | -------------------------------------------------------------------------------- /huygens/doc/requirements.txt: -------------------------------------------------------------------------------- 1 | scipy 2 | Pygments 3 | markdown 4 | -------------------------------------------------------------------------------- /huygens/all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from huygens.namespace import * 4 | 5 | -------------------------------------------------------------------------------- /huygens/doc/images/Christiaan_Huygens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punkdit/huygens/HEAD/huygens/doc/images/Christiaan_Huygens.png -------------------------------------------------------------------------------- /huygens/wiggle/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from huygens import config, back 3 | if back.the_text_cls is back.CairoText: 4 | config(text="pdflatex", latex_header=r""" 5 | \usepackage{amsmath} 6 | \usepackage{amssymb} 7 | """) 8 | 9 | import huygens 10 | if huygens.EXPERIMENTAL: 11 | from huygens.wiggle.cell_experimental import * 12 | else: 13 | from huygens.wiggle.cell import * 14 | 15 | from huygens.wiggle.shapes import * 16 | 17 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | canvas.html: canvas.ipynb 4 | jupyter nbconvert --to html canvas.ipynb 5 | 6 | canvas.ipynb: canvas.py 7 | ./make_nb.py convert_to_nb canvas.py canvas.ipynb 8 | 9 | turtle.html: turtle.ipynb 10 | jupyter nbconvert --to html turtle.ipynb 11 | 12 | turtle.ipynb: turtle.py 13 | ./make_nb.py convert_to_nb turtle.py turtle.ipynb 14 | 15 | wiggle.html: wiggle.ipynb 16 | jupyter nbconvert --to html wiggle.ipynb 17 | 18 | wiggle.ipynb: wiggle.py 19 | ./make_nb.py convert_to_nb wiggle.py wiggle.ipynb 20 | 21 | 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "huygens" 7 | version = "0.0.1" 8 | authors = [ 9 | { name="Simon Burton", email="simon@arrowtheory.com" }, 10 | ] 11 | description = "compositional vector graphics" 12 | readme = "README.md" 13 | requires-python = ">=3.0" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [project.urls] 21 | "Homepage" = "https://github.com/punkdit/huygens" 22 | "Bug Tracker" = "https://github.com/punkdit/huygens/issues" 23 | 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="huygens", 10 | version="0.1", 11 | author="Simon Burton", 12 | author_email="simon@arrowtheory.com", 13 | description="huygens graphic design package", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/punkdit/huygens", 17 | packages=setuptools.find_packages(), 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: GPL-3.0 License", 21 | "Operating System :: OS Independent", 22 | ], 23 | python_requires='>=3.5', 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /huygens/doc/images/output-0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /huygens/tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from random import random 4 | 5 | def rnd(a=0., b=1.): 6 | return (b-a)*random() + a 7 | 8 | 9 | def conv(a, b, alpha=0.5): 10 | return (1.-alpha)*a + alpha*b 11 | 12 | 13 | def smooth(val0, val1, alpha): 14 | #assert 0.<=alpha<=1. 15 | if alpha <= 0: 16 | return val0 17 | if alpha >= 1.: 18 | return val1 19 | s = -2*(alpha**3) + 3*(alpha**2) # 0.<=s<=1. 20 | val = (1-s)*val0 + s*val1 # conv 21 | return val 22 | 23 | 24 | def bump(val0, val1, alpha): 25 | if alpha <= 0.5: 26 | val = smooth(val0, val1, 2*alpha) 27 | else: 28 | val = smooth(val0, val1, 2*(1.-alpha)) 29 | return val 30 | 31 | 32 | def clamp(value, vmin=0., vmax=1.): 33 | value = max(vmin, value) 34 | value = min(vmax, value) 35 | return value 36 | 37 | 38 | -------------------------------------------------------------------------------- /huygens/doc/test_sat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | 5 | def test_sat(): 6 | 7 | # 8 | # [<<< table of contents](index.html) 9 | # 10 | # --- 11 | # 12 | # Constraint satisfier 13 | # ==================== 14 | # 15 | # 16 | # 17 | 18 | 19 | from huygens.sat import Variable, Solver, System 20 | 21 | x = Variable('x') 22 | y = Variable('y') 23 | z = Variable('z') 24 | 25 | items = [ 26 | x+y >= 1., 27 | x+z == 5, 28 | y >= 3. 29 | ] 30 | 31 | solver = Solver(items) 32 | 33 | result = solver.solve() 34 | print(result) 35 | 36 | system = System() 37 | v = system.get_var() 38 | u = system.get_var() 39 | w = system.get_var() 40 | system.add(v+u+w == 3.) 41 | system.solve() 42 | print(system[v] + system[u] + system[w]) 43 | 44 | 45 | if __name__ == "__main__": 46 | 47 | test_variable() 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /huygens/doc/images/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | huygens 5 | ======= 6 | 7 | This is a python package for drawing diagrams. 8 | Intended to have multiple backends, currently the only 9 | backend implemented uses [cairo](https://www.cairographics.org/). 10 | 11 | It can also include text from TeX via pdftex, pdflatex, xetex or xelatex. 12 | 13 | Diagrams are able to be composed in various ways, based 14 | on a linear constraint solver. Therefore, this system becomes 15 | a declarative graphics layout package. 16 | 17 | User guide 18 | ---------- 19 | 20 | Read it online 21 | [here](https://arrowtheory.com/huygens/huygens/doc/index.html). 22 | The documentation is distributed in [huygens/doc](huygens/doc/). 23 | 24 | wiggle.py 25 | ---------- 26 | 27 | This is a package for rendering string/surface diagrams 28 | for monoidal bicategories. 29 | Here are 30 | the slides from a [talk at SYCO 11](https://arrowtheory.com/wiggle.pdf), 31 | and there's more [demo code here](https://arrowtheory.com/wiggle_demo.pdf). 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /huygens/doc/images/output-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /doc/turtle.py: -------------------------------------------------------------------------------- 1 | 2 | # ## Turtle graphics 3 | # 4 | # Use a turtle to keep track of a path as you build it. 5 | 6 | from huygens import canvas, style, color 7 | from huygens.turtle import Turtle 8 | 9 | cvs = canvas.canvas() 10 | turtle = Turtle(cvs=cvs) 11 | 12 | n = 8 13 | angle = 360. / n 14 | R = 3.0 15 | for i in range(n): 16 | turtle.fwd(1.*R) 17 | turtle.left((1./3)*angle) 18 | turtle.back(0.5*R) 19 | turtle.left((1./3)*angle) 20 | turtle.back(0.7*R) 21 | turtle.left((1./3)*angle) 22 | turtle.stroke() 23 | cvs 24 | 25 | # We can change the stroke style that the turtle uses 26 | # to a thick green line. 27 | 28 | attrs = [style.linewidth.THIck, color.rgb(0.2, 0.6, 0.2)] 29 | 30 | cvs = canvas.canvas() 31 | turtle = Turtle(cvs=cvs, attrs=attrs) 32 | 33 | turtle.fwd(2.) 34 | turtle.right(300, 1.) 35 | turtle.fwd(2.) 36 | turtle.arrow(0.4) 37 | turtle.stroke() 38 | 39 | cvs 40 | 41 | # Another example: 42 | 43 | cvs = canvas.canvas() 44 | turtle = Turtle(cvs=cvs, attrs=attrs) 45 | 46 | R = 1.0 47 | for i in range(24*2): 48 | turtle.left(320, 0.6*R) 49 | turtle.left(-60, 0.3*R) 50 | turtle.right(90, 0.6*R) 51 | turtle.stroke(attrs=attrs) 52 | 53 | cvs 54 | 55 | -------------------------------------------------------------------------------- /huygens/doc/superstyle.css: -------------------------------------------------------------------------------- 1 | /* Base styles */ 2 | 3 | * { -webkit-tap-highlight-color:rgba(0,0,0,0); } 4 | 5 | 6 | canvas { 7 | align-items: center; 8 | /* border: 2px solid #000000; */ 9 | } 10 | 11 | footer { 12 | align-items: center; 13 | display: flex; 14 | justify-content: center; 15 | margin-top: 4em; 16 | text-align: center; 17 | } 18 | 19 | ol { 20 | list-style-type: disc; 21 | } 22 | 23 | /* 01 Centering */ 24 | 25 | header, 26 | main { 27 | margin: 0 auto; 28 | max-width: 40em; 29 | } 30 | 31 | /* 03 Spacing */ 32 | 33 | body { 34 | line-height: 1.5; 35 | } 36 | 37 | /* 04 Color and contrast */ 38 | 39 | body { 40 | color: #333; 41 | } 42 | 43 | h1, 44 | h2, 45 | strong { 46 | color: #222; 47 | } 48 | 49 | 50 | code, 51 | pre { 52 | background: #eee; 53 | } 54 | 55 | code { 56 | padding: 2px 4px; 57 | vertical-align: text-bottom; 58 | } 59 | 60 | pre { 61 | padding: 1em; 62 | } 63 | 64 | /* 06 Primary color */ 65 | 66 | /* a { color: #e81c4f; } */ 67 | 68 | 69 | 70 | span.frac { 71 | display: inline-block; 72 | font-size: 80%; 73 | text-align: center; 74 | } 75 | span.frac > sup { 76 | display: block; 77 | border-bottom: 1.3px solid; 78 | font: inherit; 79 | } 80 | span.frac > span { 81 | display: none; 82 | } 83 | span.frac > sub { 84 | display: block; 85 | font: inherit; 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /huygens/__init__.py: -------------------------------------------------------------------------------- 1 | from huygens import front, back 2 | 3 | from huygens.front import canvas, style, path, color, trafo, linestyle 4 | from huygens import box, diagram 5 | import huygens.text 6 | 7 | tex_header = None 8 | latex_header = None 9 | xelatex_header = None 10 | font_size = "11pt" 11 | 12 | _prev = {} # previous config 13 | 14 | def config(text=None, tex_header=None, latex_header=None, xelatex_header=None, font_size="11pt"): 15 | 16 | args = { 17 | "text":text, 18 | "tex_header":tex_header, 19 | "latex_header":latex_header, 20 | "xelatex_header":xelatex_header, 21 | "font_size":font_size, 22 | } 23 | 24 | #print("huygens.config(%r)"%(args,)) 25 | 26 | if text is None: 27 | pass 28 | elif text == "cairo": 29 | back.the_text_cls = back.CairoText 30 | elif text in "pdftex xetex xelatex pdflatex".split(): 31 | back.the_text_cls = back.MkText 32 | back.MkText.tex_engine = text 33 | else: 34 | raise Exception("config text option %r not understood" % text) 35 | 36 | huygens.text.tex_header = tex_header 37 | huygens.text.latex_header = latex_header 38 | huygens.text.xelatex_header = xelatex_header 39 | huygens.text.font_size = font_size 40 | 41 | global _prev 42 | prev = _prev 43 | _prev = args 44 | 45 | return prev 46 | 47 | 48 | EXPERIMENTAL = False 49 | 50 | -------------------------------------------------------------------------------- /huygens/argv.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | class Argv(object): 5 | def __init__(self): 6 | self.args = sys.argv[:] 7 | self._args = [] 8 | self.kw = self.argmap = {} 9 | for arg in sys.argv: 10 | if '=' in arg: 11 | items = arg.split('=') 12 | name, val = items[0], '='.join(items[1:]) 13 | self.argmap[name] = self.parse(val) 14 | self.args.remove(arg) 15 | 16 | def next(self): 17 | return self.args.pop(1) if len(self.args)>1 else None 18 | 19 | def parse(self, value): 20 | try: 21 | return eval(value) 22 | except: 23 | return value 24 | 25 | def get(self, name, default=None): 26 | return self.argmap.get(name, default) 27 | 28 | def __str__(self): 29 | return ' '.join(sys.argv) 30 | 31 | def __getattr__(self, name): 32 | value = self.argmap.get(name) 33 | if value is None: 34 | if name in self.args: 35 | self.args.remove(name) 36 | value = True 37 | setattr(self, name, True) 38 | return value 39 | 40 | def __len__(self): 41 | return len(self.args) 42 | 43 | def __getitem__(self, index): 44 | return self.args[index] 45 | 46 | def __len__(self): 47 | return len(self.args) 48 | 49 | 50 | argv = Argv() 51 | 52 | -------------------------------------------------------------------------------- /doc/canvas.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # ## Canvas 4 | # 5 | # The canvas is a front-end drawing API 6 | # modelled after the amazing 7 | # [PyX](https://pyx-project.org/) package. 8 | # 9 | # The canvas coordinates are positive in the 10 | # upper right quadrant. 11 | 12 | from huygens import canvas, path, color 13 | 14 | cvs = canvas.canvas() 15 | cvs.stroke(path.line(0., 0., 3., 2.)) 16 | cvs.fill(path.circle(3., 2., 0.2), [color.rgb.red]) 17 | cvs.writeSVGfile("output.svg") 18 | 19 | # This produces the following SVG image: 20 | 21 | cvs 22 | 23 | # The canvas also has a `writePDFfile` method. 24 | # 25 | # Unlike PyX, angles are specified in 26 | # radians in calls to `path.arc` and `trafo.rotate`. 27 | 28 | from math import pi 29 | from huygens import style, trafo, linestyle 30 | 31 | cvs = canvas.canvas() 32 | 33 | cvs.stroke(path.circle(0., 0., 1.), 34 | [style.linewidth.thick, color.rgb.blue, linestyle.dashed]) 35 | cvs.text(0., 0., "hey there!", [trafo.rotate(0.5*pi)]) 36 | 37 | # Composite paths are built using the `path.path` constructor: 38 | 39 | cvs = canvas.canvas() 40 | p = path.path([ 41 | path.moveto(0., 0.), 42 | path.arc(0., 0., 1., 0., 0.5*pi), 43 | path.lineto(-1., 1.), path.arc(-1., 0., 1., 0.5*pi, 1.0*pi), 44 | path.arc(-1.5, 0., 0.5, 1.0*pi, 2.0*pi), path.closepath() ]) 45 | 46 | cvs.fill(p, [color.rgb.red, trafo.scale(1.2, 1.2)]) 47 | cvs.stroke(p, [color.rgb.black, style.linewidth.THick]) 48 | 49 | # Building these paths are easier using 50 | # the [turtle](turtle.html) module. 51 | 52 | 53 | -------------------------------------------------------------------------------- /huygens/doc/images/output-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /huygens/doc/test_turtle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | #st = [color.rgb.blue, style.linewidth.THick, style.linecap.round] 5 | #attrs = [style.linewidth.THIck, color.rgb(0.2, 0.6, 0.2, 0.6), style.linejoin.bevel] 6 | 7 | 8 | def test_turtle(): 9 | 10 | # 11 | # [<<< table of contents](index.html) 12 | # 13 | # --- 14 | # 15 | # Turtle graphics 16 | # =============== 17 | # Use a turtle to keep track of a path as you build it. 18 | 19 | from huygens import canvas, style, color 20 | from huygens.turtle import Turtle 21 | 22 | cvs = canvas.canvas() 23 | turtle = Turtle(cvs=cvs) 24 | 25 | n = 8 26 | angle = 360. / n 27 | R = 3.0 28 | for i in range(n): 29 | turtle.fwd(1.*R) 30 | turtle.left((1./3)*angle) 31 | turtle.back(0.5*R) 32 | turtle.left((1./3)*angle) 33 | turtle.back(0.7*R) 34 | turtle.left((1./3)*angle) 35 | turtle.stroke() 36 | 37 | cvs.writeSVGfile("output.svg") 38 | 39 | yield cvs 40 | 41 | # We can change the stroke style that the turtle uses 42 | # to a thick green line. 43 | 44 | attrs = [style.linewidth.THIck, color.rgb(0.2, 0.6, 0.2)] 45 | 46 | cvs = canvas.canvas() 47 | turtle = Turtle(cvs=cvs, attrs=attrs) 48 | 49 | turtle.fwd(2.) 50 | turtle.right(300, 1.) 51 | turtle.fwd(2.) 52 | turtle.arrow(0.4) 53 | turtle.stroke() 54 | 55 | yield cvs 56 | 57 | 58 | cvs = canvas.canvas() 59 | turtle = Turtle(cvs=cvs, attrs=attrs) 60 | 61 | R = 1.0 62 | for i in range(24*2): 63 | turtle.left(320, 0.6*R) 64 | turtle.left(-60, 0.3*R) 65 | turtle.right(90, 0.6*R) 66 | turtle.stroke(attrs=attrs) 67 | 68 | yield cvs 69 | 70 | -------------------------------------------------------------------------------- /huygens/doc/images/output-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /huygens/doc/test_canvas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | #st = [color.rgb.blue, style.linewidth.THick, style.linecap.round] 5 | 6 | 7 | def test_canvas(): 8 | 9 | # 10 | # [<<< table of contents](index.html) 11 | # 12 | # --- 13 | # 14 | # Canvas 15 | # ====== 16 | # 17 | # The canvas is a front-end drawing API 18 | # modelled after the amazing 19 | # [PyX](https://pyx-project.org/) package. 20 | # 21 | # The canvas coordinates are positive in the 22 | # upper right quadrant. 23 | 24 | from huygens import canvas, path, color 25 | 26 | cvs = canvas.canvas() 27 | cvs.stroke(path.line(0., 0., 3., 2.)) 28 | cvs.fill(path.circle(3., 2., 0.2), [color.rgb.red]) 29 | cvs.writeSVGfile("output.svg") 30 | 31 | # This produces the following SVG image: 32 | 33 | yield cvs 34 | 35 | # The canvas also has a `writePDFfile` method. 36 | # 37 | # Unlike PyX, angles are specified in 38 | # radians in calls to `path.arc` and `trafo.rotate`. 39 | 40 | from math import pi 41 | from huygens import style, trafo, linestyle 42 | 43 | cvs = canvas.canvas() 44 | 45 | cvs.stroke(path.circle(0., 0., 1.), 46 | [style.linewidth.thick, color.rgb.blue, linestyle.dashed]) 47 | cvs.text(0., 0., "hey there!", [trafo.rotate(0.5*pi)]) 48 | 49 | yield cvs 50 | 51 | # Composite paths are built using the `path.path` constructor: 52 | 53 | cvs = canvas.canvas() 54 | p = path.path([ 55 | path.moveto(0., 0.), 56 | path.arc(0., 0., 1., 0., 0.5*pi), 57 | path.lineto(-1., 1.), path.arc(-1., 0., 1., 0.5*pi, 1.0*pi), 58 | path.arc(-1.5, 0., 0.5, 1.0*pi, 2.0*pi), path.closepath() ]) 59 | 60 | cvs.fill(p, [color.rgb.red, trafo.scale(1.2, 1.2)]) 61 | cvs.stroke(p, [color.rgb.black, style.linewidth.THick]) 62 | 63 | yield cvs 64 | 65 | # Building these paths are easier using 66 | # the [turtle](test_turtle.html) module. 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /huygens/variable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | class Variable(object): 5 | "Turn into a float at the _slightest provocation." 6 | 7 | def __str__(self): 8 | #return "%s(%s)"%(self.__class__.__name__, float(self)) # ? 9 | return str(float(self)) 10 | 11 | def __repr__(self): 12 | ks = list(self.__dict__.keys()) 13 | ks.sort() 14 | ns = ", ".join("%s=%s"%(k,self.__dict__[k]) for k in ks) 15 | return "%s(%s, %s, %s)"%(self.__class__.__name__, id(self), float(self), ns) 16 | 17 | def __lt__(self, other): 18 | return float(self) < other 19 | 20 | def __le__(self, other): 21 | return float(self) <= other 22 | 23 | def __eq__(self, other): 24 | return float(self) == other 25 | 26 | def __gt__(self, other): 27 | return float(self) > other 28 | 29 | def __ge__(self, other): 30 | return float(self) >= other 31 | 32 | def __add__(self, other): 33 | return other + float(self) 34 | __radd__ = __add__ 35 | 36 | def __sub__(self, other): 37 | return float(self) - other 38 | 39 | def __rsub__(self, other): 40 | return other - float(self) 41 | 42 | def __mul__(self, other): 43 | return float(self) * other 44 | __rmul__ = __mul__ 45 | 46 | def __truediv__(self, other): 47 | return float(self) / other 48 | __floordiv__ = __truediv__ 49 | 50 | def __rtruediv__(self, other): 51 | return other / float(self) 52 | __rfloordiv__ = __rtruediv__ 53 | 54 | def __mod__(self, other): 55 | return float(self) % other 56 | 57 | def __rmod__(self, other): 58 | return other % float(self) 59 | 60 | def __pow__(self, other): 61 | return float(self) ** other 62 | 63 | def __rpow__(self, other): 64 | return other ** float(self) 65 | 66 | def __neg__(self): 67 | return -float(self) 68 | 69 | def __pos__(self): 70 | return float(self) 71 | 72 | def __abs__(self): 73 | return abs(float(self)) 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /huygens/doc/test_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from huygens import config 4 | #config(text="xetex") # works, but pdf-->svg buggy on my system, lines too thick 5 | config(text="xelatex") # works 6 | #config(text="pdftex") # works 7 | #config(text="pdflatex") # works 8 | 9 | from huygens.front import * 10 | 11 | 12 | north = [text.halign.boxcenter, text.valign.top] 13 | northeast = [text.halign.boxright, text.valign.top] 14 | northwest = [text.halign.boxleft, text.valign.top] 15 | south = [text.halign.boxcenter, text.valign.bottom] 16 | southeast = [text.halign.boxright, text.valign.bottom] 17 | southwest = [text.halign.boxleft, text.valign.bottom] 18 | east = [text.halign.boxright, text.valign.middle] 19 | west = [text.halign.boxleft, text.valign.middle] 20 | center = [text.halign.boxcenter, text.valign.middle] 21 | 22 | 23 | x, y = 0., 0. 24 | h = 0.7 25 | 26 | def show(text, attrs=[]): 27 | global x, y 28 | r = 0.1 29 | st = [color.rgb.red] 30 | cvs.stroke(path.line(x, y-r, x, y+r), st) 31 | cvs.stroke(path.line(x-r, y, x+r, y), st) 32 | cvs.text(x, y, text, attrs) 33 | y -= h 34 | 35 | 36 | cvs = canvas.canvas() 37 | 38 | show("great!") 39 | show("great!", [text.halign.boxright]) 40 | show("great!", [text.halign.boxcenter]) 41 | 42 | if 1: 43 | x, y = 2., 0. 44 | show("great!", [text.valign.top]) 45 | show("great!", [text.valign.middle]) 46 | show("great!", [text.valign.bottom]) 47 | 48 | x, y = 6., 0. 49 | show("north", north) 50 | show("northeast", northeast) 51 | show("northwest", northwest) 52 | show("south", south) 53 | show("southeast", southeast) 54 | show("southwest", southwest) 55 | show("east", east) 56 | show("west", west) 57 | show("center", center) 58 | 59 | if 1: 60 | x, y = 10., 0 61 | show("tiny", [text.size.tiny]) 62 | show("script", [text.size.script]) 63 | show("footnote", [text.size.footnote]) 64 | show("small", [text.size.small]) 65 | show("normal", [text.size.normal]) 66 | show("large", [text.size.large]) 67 | show("Large", [text.size.Large]) 68 | show("LARGE", [text.size.LARGE]) 69 | show("huge", [text.size.huge]) 70 | show("Huge", [text.size.Huge]) 71 | 72 | 73 | cvs.writePDFfile("text.pdf") 74 | 75 | 76 | -------------------------------------------------------------------------------- /huygens/doc/images/output-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /huygens/doc/images/vbox-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /huygens/namespace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from huygens.front import * 4 | from huygens.turtle import mkpath 5 | 6 | 7 | def conv(alpha, a, b): 8 | return (1.-alpha)*a + alpha*b 9 | 10 | 11 | red = color.rgb(1.0, 0.0, 0.0) 12 | darkred = color.rgb(0.8, 0.2, 0.2) 13 | green = color.rgb(0.0, 0.6, 0.0) 14 | yellow = color.rgb(1.0, 1.0, 0.0) 15 | blue = color.rgb(0.2, 0.2, 0.7) 16 | white = color.rgb(1., 1., 1.) 17 | grey = color.rgb(0.8, 0.8, 0.8) 18 | darkgrey = color.rgb(0.5, 0.5, 0.5) 19 | black = color.rgb(0, 0, 0) 20 | 21 | st_red = [red] 22 | st_darkred = [darkred] 23 | st_green = [green] 24 | st_blue = [blue] 25 | st_white = [white] 26 | st_grey = [grey] 27 | st_darkgrey = [darkgrey] 28 | st_black = [black] 29 | 30 | st_bevel = st_joinbevel = [style.linejoin.bevel] 31 | st_miter = st_joinmiter = [style.linejoin.miter] 32 | st_joinround = [style.linejoin.round] 33 | st_butt = st_capbutt = [style.linecap.butt] 34 | st_capround = [style.linecap.round] 35 | st_square = st_capsquare = [style.linecap.square] 36 | 37 | st_round = [style.linecap.round, style.linejoin.round] 38 | 39 | st_dashed = [style.linestyle.dashed] 40 | st_dotted = [style.linestyle.dotted] 41 | 42 | st_north = [text.halign.boxcenter, text.valign.top] 43 | st_northeast = [text.halign.boxright, text.valign.top] 44 | st_northwest = [text.halign.boxleft, text.valign.top] 45 | st_south = [text.halign.boxcenter, text.valign.bottom] 46 | st_southeast = [text.halign.boxright, text.valign.bottom] 47 | st_southwest = [text.halign.boxleft, text.valign.bottom] 48 | st_east = [text.halign.boxright, text.valign.middle] 49 | st_west = [text.halign.boxleft, text.valign.middle] 50 | st_hcenter = [text.halign.boxcenter] 51 | st_center = [text.halign.boxcenter, text.valign.middle] 52 | st_hcenter = [text.halign.boxcenter] 53 | 54 | st_footnote = [text.size.footnote] # does not work...?? 55 | st_small = [text.size.small] # does not work...?? 56 | st_large = [text.size.large] # does not work...?? 57 | 58 | st_barrow = [deco.barrow] 59 | st_arrow = [deco.earrow] 60 | st_marrow = [deco.marrow] 61 | 62 | orange = color.rgb(0.8, 0.2, 0.0) 63 | st_arrow = [deco.earrow()] 64 | st_rarrow = [deco.earrow(reverse=True)] 65 | 66 | thin = style.linewidth.thin 67 | normal = style.linewidth.normal 68 | st_normal = [normal] 69 | st_THIN = [style.linewidth.THIN] 70 | st_THIn = [style.linewidth.THIn] 71 | st_THin = [style.linewidth.THin] 72 | st_Thin = [style.linewidth.Thin] 73 | st_thin = [style.linewidth.thin] 74 | st_thick = [style.linewidth.thick] 75 | st_Thick = [style.linewidth.Thick] 76 | st_THick = [style.linewidth.THick] 77 | st_THIck = [style.linewidth.THIck] 78 | st_THICk = [style.linewidth.THICk] 79 | st_THICK = [style.linewidth.THICK] 80 | 81 | 82 | lw = style.linewidth 83 | 84 | -------------------------------------------------------------------------------- /huygens/loadsvg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Hijack cairosvg to load svg into our internal data structures. 5 | 6 | Does not work on all svg files, but seems good enough for text. 7 | """ 8 | 9 | from cairosvg.parser import Tree 10 | from cairosvg.surface import Surface 11 | 12 | from huygens.argv import argv 13 | from huygens import back 14 | from huygens.flatten import Flatten 15 | 16 | 17 | class DummySurf(Surface): 18 | 19 | def __init__(self, tree, output, dpi, context=None): 20 | 21 | W, H = 600., 200. # point == 1/72 inch 22 | 23 | if context is None: 24 | context = Flatten() 25 | self.context = context 26 | 27 | self.dpi = dpi 28 | 29 | self._old_parent_node = self.parent_node = None 30 | self.output = output 31 | self.font_size = None 32 | 33 | self.context_width = W 34 | self.context_height = H 35 | 36 | self.cursor_position = [0, 0] 37 | self.cursor_d_position = [0, 0] 38 | self.text_path_width = 0 39 | self.stroke_and_fill = True 40 | 41 | self.tree_cache = {(tree.url, tree.get('id')): tree} 42 | 43 | self.markers = {} 44 | self.gradients = {} 45 | self.patterns = {} 46 | self.masks = {} 47 | self.paths = {} 48 | self.filters = {} 49 | 50 | self.map_rgba = None 51 | self.map_image = None 52 | 53 | self.draw(tree) 54 | 55 | #surface.finish() 56 | 57 | self.paths = self.context.paths 58 | 59 | 60 | class SkipColors(Flatten): 61 | def set_source_rgba(self, r, g, b, a): 62 | pass 63 | 64 | 65 | def loadsvg(name, dpi=72., keep_colors=True): 66 | assert name.endswith(".svg") 67 | s = open(name).read() 68 | tree = Tree(bytestring=s) 69 | if keep_colors: 70 | context = Flatten() 71 | else: 72 | context = SkipColors() 73 | dummy = DummySurf(tree, None, dpi, context=context) 74 | item = back.Compound(dummy.paths) 75 | return item 76 | 77 | 78 | 79 | def loadtex(filename, width=1.): 80 | # XXX does not work very well... XXX 81 | from huygens.front import Canvas, Translate, Scale 82 | tex = loadsvg(filename) 83 | bb = tex.get_bound_box() 84 | llx, lly, urx, ury = bb 85 | 86 | conv = lambda a,b:(a+b)/2 87 | x0, y0 = conv(llx, urx), conv(lly, ury) 88 | w, h = urx-llx, ury-lly 89 | 90 | X0, Y0 = 0., 0. 91 | cvs = Canvas() 92 | cvs.append(Translate(X0-x0, Y0-y0)) 93 | cvs.append(Scale(width/w, width/w, x0, y0)) 94 | cvs.append(tex) 95 | return cvs 96 | 97 | 98 | 99 | 100 | if __name__ == "__main__": 101 | 102 | name = argv.next() 103 | s = open(name).read() 104 | tree = Tree(bytestring=s) 105 | my = DummySurf(tree, None, 72.) 106 | from huygens.front import Canvas 107 | cvs = Canvas(my.paths) 108 | if 0: 109 | for item in cvs: 110 | print(item.__class__) 111 | for sub in item: 112 | print(sub) 113 | print() 114 | cvs.writePDFfile("output.pdf") 115 | cvs.writeSVGfile("output.svg") 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /huygens/doc/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | huygens 5 | ======= 6 | 7 | This is a python package for drawing diagrams. 8 | Intended to have multiple backends, currently the only 9 | backend implemented uses [cairo](https://www.cairographics.org/). 10 | Perhaps there will also be a [PyX](https://pyx-project.org/) backend, or a 11 | [TikZ](https://ctan.org/pkg/pgf?lang=en) backend. 12 | 13 | The huygens package can also include text from $\TeX$ via pdftex, pdflatex, xetex or xelatex. 14 | 15 | The [huygens.box](test_box.html) and [huygens.diagram](test_diagram.html) modules build on the basic graphics primitives 16 | to define composable elements. 17 | Geometry and layout of these elements is generated using a linear constraint solver. 18 | Therefore, this system becomes a declarative graphics layout package. 19 | 20 | User guide 21 | ---------- 22 | 23 | __[huygens](test_canvas.html)__ 24 | Basic drawing functions. Lines, curves, text and so on. 25 | 26 | __[huygens.turtle](test_turtle.html)__ 27 | A simple Turtle class for sequentially building paths. 28 | Also fun. 29 | 30 | __[huygens.sat](test_sat.html)__ 31 | A convenient interface to a linear programming constraint solver. 32 | Currently uses 33 | [scipy.optimize.linprog](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html) 34 | 35 | __[huygens.box](test_box.html)__ 36 | Structure figures into rectangular `Box`s. 37 | Various ways of combining these: `HBox`, `VBox`, `TableBox`, etc. 38 | The layout is determined by constraints, and so this uses 39 | the `huygens.sat` module. 40 | 41 | __[huygens.diagram](test_diagram.html)__ 42 | Building on the `box` module to make string diagrams. 43 | 44 | See also 45 | -------- 46 | 47 | Some related projects: 48 | 49 | [Foundations of brick diagrams](https://arxiv.org/abs/1908.10660) 50 | Describes horizontal and vertical composition of string diagrams. 51 | 52 | [diagrams](https://archives.haskell.org/projects.haskell.org/diagrams/) 53 | A declarative, domain-specific language written in Haskell. 54 | 55 | [Compose.jl](https://github.com/GiovineItalia/Compose.jl) 56 | A declarative vector graphics library for Julia. 57 | 58 | [Experiments in Constraint-based Graphic Design](https://www.anishathalye.com/2019/12/12/constraint-based-graphic-design/#case-studies) 59 | Describes a DSL called "Basalt" written in Python, based on 60 | the logic of non-linear real arithmetic, which is an 61 | extension of linear programming. No code available. 62 | 63 | [Penrose](http://penrose.ink) 64 | "Create beautiful diagrams just by typing mathematical notation in plain text." 65 | [Source here](https://github.com/penrose/penrose) 66 | Written in Haskell. Users not welcome (yet). 67 | 68 | [Graphviz](https://graphviz.org/) 69 | Declarative language for rendering heirarchical data structures. 70 | [Apparently](https://news.ycombinator.com/item?id=23477034) 71 | "20 year old code that was basically a prototype that escaped from the lab". 72 | See also 73 | [this blog post.](https://ncona.com/2020/06/create-diagrams-with-code-using-graphviz/) 74 | 75 | 76 | [TikZ](https://ctan.org/pkg/pgf?lang=en) 77 | Possibly the most widely used package for creating 78 | graphics for integration with latex documents. 79 | 80 | [Asymptote](https://asymptote.sourceforge.io/) 81 | A custom language for doing vector graphics inspired by MetaPost. 82 | 83 | [manim](https://github.com/3b1b/manim) 84 | Primarily used for animations, this library also has 85 | an interface to latex. It also is a python library using pycairo. 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /huygens/text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import hashlib 5 | from subprocess import Popen, PIPE 6 | 7 | 8 | def file_exists(name): 9 | try: 10 | os.stat(name) 11 | return True 12 | except: 13 | pass 14 | return False 15 | 16 | 17 | def verbose_command(cmd): 18 | ret = os.system(cmd) 19 | assert ret == 0, "%r failed with return value %d"%(cmd, ret) 20 | 21 | 22 | def command(cmd): 23 | p = Popen(cmd, shell=True, 24 | stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 25 | (child_stdin, 26 | child_stdout, 27 | child_stderr) = (p.stdin, p.stdout, p.stderr) 28 | ret = p.wait() 29 | assert ret == 0, "%r failed with return value %d"%(cmd, ret) 30 | 31 | command = verbose_command 32 | 33 | tex_header = None 34 | latex_header = None 35 | xelatex_header = None 36 | font_size = "11pt" 37 | 38 | def tex_output(text): 39 | lines = [] 40 | lines.append(r"\def\folio{}") 41 | if tex_header: 42 | lines += tex_header.split("\n") 43 | lines.append(r"%s\bye"%text) 44 | return '\n'.join(lines) 45 | 46 | 47 | def latex_output(text): 48 | lines = [] 49 | lines.append(r"\documentclass[%s]{article}"%font_size) 50 | lines.append(r"\pagenumbering{gobble}") 51 | lines.append(r"\def\folio{}") 52 | if latex_header: 53 | lines += latex_header.split("\n") 54 | lines.append(r"\begin{document}") 55 | lines.append(text) 56 | lines.append(r"\end{document}") 57 | return '\n'.join(lines) 58 | 59 | 60 | def xelatex_output(text): 61 | lines = [] 62 | lines.append(r"\documentclass[%s]{article}"%font_size) 63 | lines.append(r"\pagenumbering{gobble}") 64 | lines.append(r"\usepackage{fontspec}") 65 | lines.append(r"\setromanfont{Gentium Book Basic}") 66 | lines.append(r"\def\folio{}") 67 | if xelatex_header: 68 | lines += xelatex_header.split("\n") 69 | lines.append(r"\begin{document}") 70 | lines.append(text) 71 | lines.append(r"\end{document}") 72 | return '\n'.join(lines) 73 | 74 | 75 | def make_text(text, tex_engine="pdftex"): 76 | assert tex_engine in "pdftex xetex xelatex pdflatex".split() 77 | cache = "__huygens__" 78 | if not file_exists(cache): 79 | os.mkdir(cache) 80 | 81 | os.chdir(cache) # <---------- chdir <----- 82 | 83 | try: 84 | 85 | if tex_engine == "pdftex" or tex_engine == "xetex": 86 | output = tex_output(text) 87 | elif tex_engine=="pdflatex": 88 | output = latex_output(text) 89 | elif tex_engine == "xelatex": 90 | output = xelatex_output(text) 91 | else: 92 | assert 0, tex_engine 93 | 94 | data = output.encode('utf-8') 95 | stem = hashlib.sha1(data).hexdigest() 96 | 97 | tex_name = "%s.tex"%stem 98 | svg_name = "%s.svg"%stem 99 | pdf_name = "%s.pdf"%stem 100 | 101 | if not file_exists(svg_name): 102 | 103 | f = open(tex_name, 'w') 104 | print(output, file=f) 105 | f.close() 106 | 107 | command("%s %s"%(tex_engine, tex_name)) 108 | command("pdf2svg %s %s" % (pdf_name, svg_name)) 109 | 110 | from huygens import loadsvg 111 | item = loadsvg.loadsvg(svg_name) 112 | 113 | finally: 114 | 115 | os.chdir("..") # <---------- chdir <----- 116 | 117 | return item 118 | 119 | 120 | def test(): 121 | 122 | item = make_text(".") 123 | print(item) 124 | 125 | 126 | if __name__ == "__main__": 127 | 128 | test() 129 | 130 | -------------------------------------------------------------------------------- /huygens/doc/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Note: use mkdoc.py to rebuild all docs & images. 5 | """ 6 | 7 | import os 8 | import collections 9 | 10 | import huygens.doc 11 | from huygens.front import Canvas, Scale, Base 12 | from huygens.box import Box 13 | 14 | 15 | class TestRun(Base): 16 | def __init__(self, func, start=None, end=None, img=None, result=None): 17 | self.func = func 18 | self.start = start 19 | self.end = end 20 | self.img = img 21 | self.result = result 22 | 23 | 24 | all_names = set() 25 | counter = 0 26 | 27 | 28 | def run_test(func, dummy=False): 29 | 30 | global counter 31 | 32 | items = func() 33 | 34 | if not isinstance(items, collections.abc.Iterator): 35 | yield TestRun(func, func.__code__.co_firstlineno, result=items) 36 | return 37 | 38 | start = items.gi_frame.f_lineno # index 39 | 40 | while 1: 41 | try: 42 | box = None 43 | cvs = None 44 | name = None 45 | 46 | result = items.__next__() 47 | 48 | if isinstance(result, tuple): 49 | result, name = result 50 | 51 | if isinstance(result, Box): 52 | box = result 53 | elif isinstance(result, Canvas): 54 | cvs = result 55 | else: 56 | assert 0, "%r not understood" % (result,) 57 | 58 | if not name: 59 | name = "output-%d"%counter 60 | counter += 1 61 | 62 | assert name not in all_names, "name dup: %r"%name 63 | all_names.add(name) 64 | 65 | svgname = "images/%s.svg"%name 66 | pdfname = "images/%s.pdf"%name 67 | 68 | end = items.gi_frame.f_lineno-1 # index 69 | test = TestRun(func, start, end, svgname) 70 | yield test 71 | start = end+1 72 | 73 | if dummy: 74 | svgname = "/dev/null" 75 | pdfname = "/dev/null" 76 | 77 | try: 78 | print("run_tests: rendering", name, func) 79 | if cvs is None: 80 | cvs = Canvas() 81 | cvs.append(Scale(2.0)) 82 | box.render(cvs) 83 | else: 84 | cvs = Canvas([Scale(2.0), cvs]) 85 | 86 | cvs.writeSVGfile(svgname) 87 | #cvs.writePDFfile(pdfname) # pdf's change on every write :-( 88 | print() 89 | except: 90 | print("run_tests: render failed for", 91 | name, func.__name__, "line", end) 92 | raise 93 | 94 | except StopIteration: 95 | break 96 | 97 | 98 | 99 | def harvest(path, name, dummy=False): 100 | print("run_tests.harvest", name) 101 | assert name.endswith(".py") 102 | stem = name[:-len(".py")] 103 | desc = "huygens.doc."+stem 104 | __import__(desc) 105 | m = getattr(huygens.doc, stem) 106 | funcs = [] 107 | for attr in dir(m): 108 | value = getattr(m, attr) 109 | if attr.startswith("test_") and isinstance(value, collections.abc.Callable): 110 | funcs.append(value) 111 | 112 | funcs.sort(key = lambda f : (f.__module__, f.__code__.co_firstlineno)) 113 | for func in funcs: 114 | for test in run_test(func, dummy=dummy): 115 | yield test 116 | 117 | 118 | def run(): 119 | 120 | path = os.path.dirname(__file__) 121 | names = os.listdir(path) 122 | names = [name for name in names 123 | if name.endswith(".py") and name.startswith("test_")] 124 | names.sort() 125 | 126 | for name in names: 127 | for test in harvest(path, name, True): 128 | yield test 129 | 130 | 131 | def main(): 132 | for test in run(): 133 | pass 134 | 135 | print("run_tests.main: finished") 136 | 137 | 138 | if __name__ == "__main__": 139 | main() 140 | 141 | 142 | -------------------------------------------------------------------------------- /doc/make_nb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | import json 6 | 7 | import nbformat 8 | from nbformat.v4 import new_notebook, new_code_cell, new_markdown_cell, new_output 9 | 10 | import papermill 11 | 12 | from huygens.argv import argv 13 | 14 | 15 | def save_nb(nb, name): 16 | f = open(name, 'w') 17 | json.dump(nb, f, indent=1) 18 | f.close() 19 | 20 | 21 | def main(): 22 | 23 | nb = new_notebook() 24 | append = lambda s : nb.cells.append(new_code_cell(s)) 25 | 26 | append("a=1") 27 | append("a+5") 28 | 29 | append("from huygens.namespace import *") 30 | append("from huygens.wiggle import Cell0, Cell1, Cell2") 31 | append("pink = color.rgba(1.0, 0.37, 0.36, 0.5)") 32 | append("Cell0('n', fill=pink)") 33 | 34 | save_nb(nb, "test.ipynb") 35 | nb = papermill.execute_notebook("test.ipynb", None, kernel_name="python3") 36 | save_nb(nb, "test_exec_1.ipynb") 37 | 38 | if 0: 39 | text, image = ['text/plain', 'image/svg+xml'] 40 | #print(nb) 41 | for cell in nb.cells: 42 | for output in cell["outputs"]: 43 | data = output["data"] 44 | #print(list(data.keys())) 45 | if image in data and text in data: 46 | item = data[text] 47 | del data[text] 48 | data[text] = [item] 49 | if image in data: 50 | item = data[image] 51 | item = [item+"\n" for item in item.split("\n")] 52 | data[image] = item 53 | 54 | 55 | 56 | def convert_to_py(name, output=None): 57 | 58 | assert name.endswith(".ipynb") 59 | 60 | s = open(name).read() 61 | nb = nbformat.reads(s, as_version=4) 62 | 63 | output = sys.stdout if output is None else open(output, 'w') 64 | 65 | for cell in nb.cells: 66 | #print(list(cell.keys())) 67 | source = cell["source"] 68 | tp = cell["cell_type"] 69 | if tp == "markdown": 70 | lines = source.split('\n') 71 | source = '\n'.join("# "+line for line in lines) 72 | print(source+"\n", file=output) 73 | 74 | def convert_to_nb(name, output): 75 | 76 | assert name.endswith(".py") 77 | assert output and output.endswith(".ipynb") 78 | 79 | s = open(name).read() 80 | lines = s.split('\n') 81 | 82 | lines.append("# ") # trigger last code block 83 | 84 | nb = new_notebook() 85 | add_code = lambda s : nb.cells.append(new_code_cell(s)) 86 | add_md = lambda s : nb.cells.append(new_markdown_cell(s)) 87 | 88 | code = None 89 | md = None 90 | for line in lines: 91 | if line.startswith("# ") and md is None: 92 | if code is not None: 93 | while code and not code[-1].strip(): 94 | code.pop() 95 | if code: 96 | code = '\n'.join(code) 97 | add_code(code) 98 | code = None 99 | md = [] 100 | if line.startswith("# "): 101 | assert code is None 102 | line = line[2:] 103 | md.append(line) 104 | else: 105 | if code is None: 106 | if md is not None: 107 | md = '\n'.join(md) 108 | if md.strip(): 109 | add_md(md) 110 | md = None 111 | code = [] 112 | if code or line.strip(): 113 | code.append(line) 114 | 115 | 116 | save_nb(nb, output) 117 | nb = papermill.execute_notebook(output, None, kernel_name="python3") 118 | save_nb(nb, output) 119 | 120 | 121 | 122 | if __name__ == "__main__": 123 | 124 | if argv.convert_to_py: 125 | convert_to_py(argv.next(), argv.next()) 126 | 127 | elif argv.convert_to_nb: 128 | convert_to_nb(argv.next(), argv.next()) 129 | 130 | 131 | -------------------------------------------------------------------------------- /huygens/doc/images/output-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /huygens/doc/images/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /huygens/huytex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | replace %py lines with appropriate latex commands, 5 | and exec the %py 6 | """ 7 | 8 | import os 9 | import hashlib 10 | 11 | def exists(name): 12 | try: 13 | open(name).close() 14 | return True 15 | except OSError: 16 | return False 17 | 18 | 19 | #import config 20 | #config.weld = False 21 | #from config import * 22 | 23 | 24 | state = [] 25 | stems = set() 26 | lookup = {} 27 | 28 | match_py = "%py " 29 | match_PY = "%PY " 30 | GRI = r"\raisebox{-0.4\height}{\includegraphics{%s/%s.pdf}}%%" 31 | GRD = r"{\begin{center}\includegraphics{%s/%s.pdf}\end{center}}%%" 32 | 33 | suspend = False 34 | def process(line, ns): 35 | global suspend 36 | if "get_latex" not in ns: 37 | ns["get_latex"] = get_latex 38 | 39 | if match_py in line and line.index("%") == line.index(match_py): 40 | idx = line.index(match_py) 41 | cmd = GRI 42 | #size = 0.4 43 | elif match_PY in line and line.index("%") == line.index(match_PY): 44 | idx = line.index(match_PY) 45 | cmd = GRD 46 | #size = 1.0 47 | else: 48 | return line 49 | 50 | pre = line[:idx] 51 | post = line[idx+4:] 52 | 53 | if not post.strip(): 54 | # nothing to do here 55 | return pre+"\n" if pre else pre # <------ return 56 | 57 | if post.strip() == "%py exit": 58 | suspend = True # arfff... 59 | return pre+"\n" if pre else pre # <------ return 60 | 61 | data = state 62 | try: 63 | #print("eval: %r"%post) 64 | value = eval(post, ns, ns) 65 | if value is None: # function call... 66 | state.append(post) 67 | return pre+"\n" if pre else pre # <------ return 68 | # WARNING: we _assume expr's don't mutate state 69 | # otherwise, uncomment the next line: 70 | #state.append(post) 71 | data = state + [post] 72 | except SyntaxError: 73 | exec(post, ns, ns) 74 | state.append(post) 75 | value = None 76 | return pre+"\n" if pre else pre # <------ return 77 | except: 78 | print("proc.py failed at %r"%(post,)) 79 | raise 80 | 81 | if hasattr(value, "_latex_"): 82 | value = value._latex_() 83 | 84 | if type(value) is str: 85 | return pre + value 86 | #value = value.replace("_", r"\_") 87 | #return r"{\tt %s}"%(value,) 88 | 89 | data = '\n'.join(data) 90 | data = data.encode('utf-8') 91 | stem = hashlib.sha1(data).hexdigest() 92 | 93 | if stem not in stems: 94 | name = "%s/%s.pdf"%(imdir, stem,) 95 | if not exists(name): 96 | try: 97 | save(stem, value, imdir=imdir) # monkeypatch from local config 98 | except: 99 | print("proc.py failed at %r"%(post,)) 100 | raise 101 | stems.add(stem) 102 | 103 | latex = cmd%(imdir, stem,) + "\n" 104 | lookup[id(value)] = latex 105 | line = pre + latex 106 | 107 | return line 108 | 109 | 110 | def get_latex(value, cmd=GRI, **kw): 111 | key = id(value) 112 | if key in lookup: 113 | return lookup[key] 114 | data = str(key).encode('utf-8') # ? 115 | stem = hashlib.sha1(data).hexdigest() 116 | save(stem, value, imdir=imdir, **kw) # monkeypatch from local config 117 | latex = cmd%(imdir, stem,) + "\n" 118 | lookup[id(value)] = latex 119 | return latex 120 | 121 | 122 | def main(tgt, src, ns={}): 123 | global imdir 124 | 125 | f = open(src) 126 | if exists(tgt): 127 | print("file %r exists!"%tgt) 128 | return 129 | 130 | g = open(tgt, 'w') 131 | 132 | assert tgt.endswith(".tex") 133 | imdir = tgt[:-len(".tex")] + ".imcache" 134 | try: 135 | os.mkdir(imdir) 136 | except FileExistsError: 137 | pass 138 | 139 | for line in f.readlines(): 140 | line = process(line, ns) 141 | print(line, end="", file=g) 142 | 143 | if line.strip() == r"\end{document}": 144 | break 145 | 146 | 147 | if __name__ == "__main__": 148 | 149 | import sys 150 | for item in sys.argv[3:]: 151 | state.append(open(item).read()) 152 | 153 | main(sys.argv[1], sys.argv[2]) 154 | 155 | -------------------------------------------------------------------------------- /huygens/doc/images/obox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /huygens/doc/test_box.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def test_box(): 5 | 6 | 7 | # 8 | # [<<< table of contents](index.html) 9 | # 10 | # --- 11 | # 12 | # Layout with Box's 13 | # ================= 14 | # 15 | 16 | from random import random, seed 17 | seed(0) 18 | from huygens.front import canvas, path 19 | from huygens.box import (Box, EmptyBox, CanBox, TextBox, 20 | HBox, VBox, OBox, TableBox, FillBox, MarginBox, AlignBox) 21 | 22 | # First we set a debug flag so we can see the shape of every box 23 | # 24 | 25 | Box.DEBUG = True 26 | 27 | # Every Box has an anchor point, shown with a cross. 28 | # Then, the distance to the four sides of the box 29 | # are stored as attributes `top`, `bot`, `left`, and `right`. 30 | 31 | box = EmptyBox(top=0.5, bot=1.0, left=0.2, right=2.) 32 | 33 | # To render a box onto a canvas: 34 | cvs = canvas.canvas() 35 | box.render(cvs) 36 | 37 | # We can then call `cvs.writeSVGfile("output.svg")` to save an svg file: 38 | 39 | yield box, "empty" 40 | 41 | #-------------------------------------------------- 42 | 43 | box = TextBox("Hey there!") 44 | yield box, "text" 45 | 46 | #-------------------------------------------------- 47 | 48 | cvs = canvas.canvas() 49 | cvs.stroke(path.line(0., 0., 1., 1.)) 50 | cvs.text(0., 0., "hello everyone") 51 | box = CanBox(cvs) 52 | yield box, 'canbox' 53 | 54 | #-------------------------------------------------- 55 | 56 | # You cannot use the same box more than once in a container Box: 57 | 58 | box = TextBox("hello") 59 | box = HBox([box, box]) # FAIL 60 | #yield box, "hbox-fail" # raises assert error 61 | 62 | #-------------------------------------------------- 63 | 64 | box = HBox("geghh xxde xyeey".split()) 65 | yield box, "hbox-text" 66 | 67 | #-------------------------------------------------- 68 | 69 | box = VBox("geghh xxde xyeey".split()) 70 | yield box, "vbox-text" 71 | 72 | #-------------------------------------------------- 73 | 74 | r = 1.0 75 | a = EmptyBox(top=r, bot=r) 76 | b = EmptyBox(top=r, bot=r) 77 | c = EmptyBox(left=r, right=r) 78 | #box = StrictVBox([a, c]) 79 | box = VBox([a, c]) 80 | yield box, 'vbox-empty' 81 | 82 | #-------------------------------------------------- 83 | 84 | box = OBox([ 85 | EmptyBox(.4, .1, 0., 2.2), 86 | EmptyBox(.3, 0., .5, 2.5), 87 | EmptyBox(1., .5, .5, .5), 88 | FillBox(.2, .2), 89 | ]) 90 | yield box, "obox" 91 | 92 | 93 | #-------------------------------------------------- 94 | 95 | box = HBox([ 96 | VBox([TextBox(text) for text in "xxx1 ggg2 xxx3 xx4".split()]), 97 | VBox([TextBox(text) for text in "123 xfdl sdal".split()]), 98 | ]) 99 | yield box, "hbox-vbox" 100 | 101 | 102 | #-------------------------------------------------- 103 | 104 | box = TableBox([ 105 | [EmptyBox(.4, .1, 0.2, 2.2), EmptyBox(.3, 1.2, .5, 2.5),], 106 | [EmptyBox(.8, .1, 0.4, 1.2), EmptyBox(.5, 0.4, .5, 1.5),] 107 | ]) 108 | yield box, "table" 109 | 110 | 111 | #-------------------------------------------------- 112 | 113 | def rnd(a, b): 114 | return (b-a)*random() + a 115 | 116 | a, b = 0.2, 1.0 117 | rows = [] 118 | for row in range(3): 119 | row = [] 120 | for col in range(3): 121 | box = EmptyBox(rnd(a,b), rnd(a,b), rnd(a,b), rnd(a,b)) 122 | row.append(box) 123 | rows.append(row) 124 | 125 | box = TableBox(rows) 126 | yield box, "table-2" 127 | 128 | 129 | #-------------------------------------------------- 130 | 131 | rows = [] 132 | for i in range(3): 133 | row = [] 134 | for j in range(3): 135 | box = TextBox(("xbcgef"[i+j])*(i+1)*(j+1)) 136 | box = MarginBox(box, 0.1) 137 | box = AlignBox(box, "north") 138 | row.append(box) 139 | row.append(EmptyBox(bot=1.)) 140 | rows.append(row) 141 | box = TableBox(rows) 142 | yield box, "table-3" 143 | 144 | 145 | #-------------------------------------------------- 146 | 147 | a, b = 0.2, 2.0 148 | rows = [] 149 | for row in range(2): 150 | boxs = [] 151 | for col in range(2): 152 | top = rnd(a, b) 153 | bot = rnd(a, b) 154 | left = rnd(a, b) 155 | right = rnd(a, b) 156 | box = EmptyBox(top, bot, left, right) 157 | boxs.append(box) 158 | boxs.append(TextBox("Hig%d !"%row)) 159 | box = HBox(boxs) 160 | rows.append(box) 161 | box = VBox(rows) 162 | yield box, "table-4" 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /huygens/doc/images/output-11.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /huygens/doc/images/output-12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /huygens/doc/images/output-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /huygens/doc/images/output-8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /huygens/doc/images/output-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /huygens/wiggle/test_wiggle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #from huygens.wiggle import cell_experimental 4 | #assert 0 5 | import huygens 6 | huygens.EXPERIMENTAL = True 7 | 8 | from huygens.namespace import * 9 | from huygens.wiggle import Cell0, Cell1, Cell2 10 | from huygens.wiggle.strict import l_unit, mul, comul, pull_over, unit, braid_over, pull_over, pull_over_i, assoc 11 | 12 | import warnings 13 | warnings.filterwarnings('ignore') 14 | 15 | pink = color.rgba(1.0, 0.37, 0.36, 0.5) 16 | cream = color.rgba(1.0, 1.0, 0.92, 0.5) 17 | cyan = color.rgba(0.0, 0.81, 0.80, 0.5) 18 | yellow = color.rgba(1.0, 0.80, 0.3, 0.5) 19 | 20 | grey = color.rgba(0.75, 0.75, 0.75, 0.5) 21 | red = color.rgba(1., 0.2, 0.2, 0.5) 22 | green = color.rgba(0.3, 0.7, 0.2, 0.5) 23 | blue = color.rgba(0.2, 0.2, 0.7, 0.5) 24 | 25 | scheme = [grey, blue, yellow, red, pink] # colour scheme 26 | 27 | 28 | def dump(cell): 29 | cell = cell.translate() 30 | print(cell) 31 | for path in cell.get_paths(): 32 | print("\t", ' '.join(str(c) for c in path)) 33 | 34 | 35 | def test_compose(): 36 | M = Cell0("M", fill=scheme[0]) 37 | N = Cell0("N", fill=scheme[1]) 38 | 39 | mul2 = lambda M,N : (mul(M)@mul(N)) << (M.i @ braid_over(N,M) @ N.i) 40 | src = mul2(M,N) << (mul2(M,N) @ (M.i@N.i)) 41 | tgt = mul2(M,N) << ((M.i@N.i) @ mul2(M,N)) 42 | top = (mul(M).i @ mul(N).i) << (M.i.i @ pull_over_i(N, mul(M)) @ mul(N).i) << (M.i.i@N.i.i@M.i.i@braid_over(N,M).i@N.i.i) 43 | mid = ((mul(M)<<(M.i@mul(M))).i @ assoc(N)) << (M.i@M.i@braid_over(N,M)@N.i@N.i).i << (M.i@braid_over(N,M)@braid_over(N,M)@N.i).i 44 | bot = (assoc(M)@mul(N).i) << (M.i.i@M.i.i @ pull_over(mul(N), M) @ N.i.i) << (M.i.i@braid_over(N,M).i@N.i.i@M.i.i@N.i.i) 45 | 46 | # these don't compose because we need to insert identity 1-cells 47 | cell = top * mid 48 | cell = mid * bot 49 | cell.render_cvs() 50 | 51 | 52 | #counter = 0 53 | #def save(cell): 54 | # global counter 55 | 56 | def save(cell, name): 57 | print("writePDFfile: %s.pdf"%name) 58 | cell = cell.layout() # ARGHHH 59 | cell.render_cvs(pos="northeast").writePDFfile(name) 60 | print() 61 | print("_"*79) 62 | 63 | 64 | def test_strict(): 65 | M = Cell0("M", fill=scheme[0]) 66 | N = Cell0("N", fill=scheme[1]) 67 | 68 | f = Cell1(N, M) 69 | m_mm = mul(M) 70 | n_nn = mul(N) 71 | m_ = unit(M) 72 | n_ = unit(N) 73 | 74 | f_mul = Cell2(f << m_mm, n_nn << (f@f), pip_color=blue) 75 | f_unit = Cell2(f << m_, n_, pip_color=blue) 76 | 77 | mid = f_mul << (unit(M).i @ M.i.i) 78 | bot = mul(N).i << (f_unit @ f.i) << (M.i.i) 79 | 80 | #bot = (f_unit @ f.i) << (M.i.i) 81 | 82 | # print("\n\ntranslate") 83 | # bot.translate() 84 | 85 | save(mid, "test_mid") 86 | save(bot, "test_bot") 87 | return 88 | 89 | print("mid*bot") 90 | cell = mid * bot 91 | 92 | print("\n\n") 93 | save(cell, "test_strict") 94 | 95 | 96 | 97 | def test_compose_units(): 98 | M = Cell0("M", fill=scheme[0]) 99 | N = Cell0("N", fill=scheme[1]) 100 | 101 | cell = (M @ unit(N)) << (M.i << M.i) 102 | save(cell, "test_0") 103 | 104 | cell = M.i 105 | cell = cell.insert_identity_src(1) 106 | 107 | unitor = Cell2(M.i, mul(M)<<(unit(M) @ M)) 108 | cell = unitor << Cell1(M,M, stroke=blue, pip_color=blue).i 109 | save(cell, "test_unitor") 110 | 111 | #cell = unitor << mul(M) 112 | src = mul(M) << comul(M) 113 | cell = Cell2(M.i, src) 114 | #cell = cell.v_op() 115 | counitor = unitor.h_op() 116 | cell = unitor << counitor 117 | bot = cell.v_op() 118 | cell = cell * bot 119 | save(cell, "test_mul") 120 | 121 | cell = M @ unit(N) @ M 122 | cell = cell << comul(M) 123 | cell = cell.translate() 124 | print(cell.src) 125 | for path in cell.get_paths(): 126 | print(path) 127 | #for cell in path: 128 | # if isinstance(cell, Cell0) and cell.is_identity(): 129 | # print(repr(cell)) 130 | assert len(list(cell.get_paths())) == 3 131 | save(cell, "test_1") 132 | 133 | return 134 | 135 | cell = (unit(N) @ M) << M 136 | dump(cell) 137 | cell = (M @ unit(N) @ M) << (M @ M) 138 | dump(cell) 139 | return 140 | 141 | # ((M<---M)@((N<---N@N)<<((N<---ident)@(N<---N)))) 142 | # (((M<---M)@(N<---N@N))<<((((M<---M)@(N<---ident))<<(M<---M))@(N<---N))) 143 | 144 | bot = (l_unit(M) @ mul(N).i) << (pull_over(unit(N), M) @ N.i.i) 145 | top = (M.i.i @ l_unit(N)) 146 | 147 | lhs = top.src.translate() 148 | rhs = bot.tgt.translate() 149 | 150 | dump(lhs) 151 | dump(rhs) 152 | 153 | dump(rhs[1]) 154 | dump(rhs[1][0]) 155 | 156 | return 157 | 158 | # compose fail 159 | cvs = Canvas([top.render_cvs(pos="northeast"), Translate(6,0), bot.render_cvs(pos="northeast")]) 160 | cvs.writePDFfile("compose.pdf") 161 | 162 | cell = top * bot 163 | cvs = cell.render_cvs() 164 | 165 | # compose fail 166 | #top.d_rev() 167 | #bot.d_rev() 168 | 169 | 170 | 171 | def test_level(): 172 | 173 | m, n = Cell0("m"), Cell0("n") 174 | A = Cell1(m, n) 175 | B = Cell1(m, n) 176 | f = Cell2(B, A) 177 | 178 | mn = m @ n 179 | assert mn.level == 0 180 | 181 | mA = m @ A 182 | assert mA.level == 1 183 | assert mA[0].level == 1 184 | assert mA[1].level == 1 185 | 186 | Am = A @ m 187 | assert Am.level == 1 188 | assert Am[0].level == 1 189 | assert Am[1].level == 1 190 | 191 | 192 | 193 | 194 | if __name__ == "__main__": 195 | 196 | #test_strict() 197 | #test_level() 198 | test_compose_units() 199 | 200 | print("OK\n") 201 | 202 | 203 | -------------------------------------------------------------------------------- /huygens/doc/images/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /huygens/doc/mkdoc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import collections 5 | 6 | import markdown 7 | from pygments.formatters import HtmlFormatter 8 | from pygments import highlight 9 | from pygments.lexers import Python3Lexer 10 | 11 | from huygens.argv import argv 12 | from huygens.doc import run_tests 13 | from huygens.box import Box 14 | 15 | 16 | def html_head(s): 17 | html = """\ 18 | 19 | 20 | 21 | 22 | 23 | %s 24 | 38 | 39 | 40 |
41 | """ % s 42 | return html 43 | 44 | def html_tail(): 45 | return """ 46 |
47 | 48 | Copyright (c) 2018 - 2020. 49 | 50 |
51 | 52 | 53 | """ 54 | 55 | def html_style(body): 56 | return "\n"%body 57 | 58 | def html_p(body): 59 | return "

%s

\n"%body 60 | 61 | def html_pre(body): 62 | return "
%s
\n"%body 63 | 64 | def html_img(name): 65 | return '

\n'%name 66 | 67 | 68 | # This is like recursive descent parsing: 69 | 70 | def main(): 71 | dummy = argv.dummy 72 | select = argv.get("select", "") 73 | path = "." 74 | names = "test_canvas.py test_turtle.py test_sat.py test_box.py test_diagram.py" 75 | names = names.split() 76 | for name in names: 77 | if select in name: 78 | process(path, name, dummy) 79 | 80 | 81 | def process(path, name, dummy=False): 82 | 83 | print("mkdoc.proces(%r, %r)"%(path, name)) 84 | fullname = os.path.join(path, name) 85 | code = open(fullname).read().split('\n') 86 | 87 | assert fullname.endswith(".py") 88 | output = fullname[:-len(".py")] + ".html" 89 | 90 | output = open(output, 'w') 91 | style = HtmlFormatter().get_style_defs('.highlight') 92 | print(html_head(html_style(style)), file=output) 93 | 94 | func = None 95 | test = None 96 | start = None 97 | 98 | try: 99 | for test in run_tests.harvest(path, name, dummy=dummy): 100 | 101 | if func is None: 102 | # first func in test script 103 | func = test.func 104 | 105 | if test.func is not func: 106 | end = find_dedent(code, start) 107 | #print(html_p(str((start, end))), file=output) # DEBUG 108 | snip = code[start+1: end] 109 | for block in html_snip(snip): 110 | print(block, file=output) 111 | print("\n\n
\n", file=output) 112 | func = test.func 113 | 114 | #print(html_pre(test), file=output) 115 | 116 | end = test.end or find_dedent(code, test.start) 117 | 118 | # DEBUG 119 | #print(html_p("start=%s, end=%s" % (test.start, end)), file=output) 120 | snip = code[test.start : end] 121 | for block in html_snip(snip): 122 | print(block, file=output) 123 | 124 | if test.img: 125 | print(html_img(test.img), file=output) 126 | 127 | start = end 128 | if test and test.end: 129 | start = end 130 | end = find_dedent(code, start) 131 | if end > start: 132 | snip = code[start+1 : end] 133 | for block in html_snip(snip): 134 | print(block, file=output) 135 | #print(html_p("end of func"), file=output) 136 | 137 | except Exception: 138 | import traceback 139 | print("\n
", file=output)
140 |         traceback.print_exc(file=output)
141 |         print("
", file=output) 142 | traceback.print_exc() 143 | 144 | print(html_tail(), file=output) 145 | output.close() 146 | Box.DEBUG = False 147 | 148 | 149 | def html_code(lines): 150 | s = '\n'.join(lines) 151 | if not s.strip(): 152 | return "" 153 | return highlight(s, Python3Lexer(), HtmlFormatter()) 154 | 155 | COMMENT = "# " 156 | 157 | def html_comment(lines): 158 | lines = [line[len(COMMENT):] for line in lines] 159 | s = '\n'.join(lines) 160 | #return html_p(s) 161 | 162 | md = markdown.Markdown() 163 | html = md.convert(s) 164 | return html 165 | 166 | 167 | def html_snip(lines): 168 | 169 | lines = dedent(lines) 170 | 171 | code = [] 172 | comment = [] 173 | idx = 0 174 | while idx < len(lines): 175 | line = lines[idx] 176 | if line.startswith(COMMENT): 177 | if code: 178 | yield html_code(code) 179 | code = [] 180 | comment.append(line) 181 | elif line.startswith("#") and comment: 182 | assert not code 183 | comment.append(line) 184 | elif line.startswith("#"): 185 | pass 186 | else: 187 | if comment: 188 | yield html_comment(comment) 189 | comment = [] 190 | code.append(line) 191 | idx += 1 192 | 193 | if code: 194 | yield html_code(code) 195 | 196 | if comment: 197 | yield html_comment(comment) 198 | 199 | 200 | def get_indent(line): 201 | i = 0 202 | while line.startswith(' '*i): 203 | i += 1 204 | space = ' '*(i-1) 205 | return space 206 | 207 | 208 | def find_dedent(lines, idx): 209 | 210 | space = None 211 | while idx < len(lines): 212 | line = lines[idx] 213 | if line.strip(): 214 | indent = get_indent(lines[idx]) 215 | if space is None: 216 | space = indent 217 | if len(indent) < len(space): 218 | break 219 | idx += 1 220 | return idx 221 | 222 | 223 | def dedent(lines): 224 | indent = None 225 | assert lines 226 | for line in lines: 227 | if not line.strip(): 228 | continue 229 | space = get_indent(line) 230 | if indent is None or indent.startswith(space): 231 | indent = space 232 | if indent is None: 233 | return [] 234 | lines = [line[len(indent):] for line in lines] 235 | return lines 236 | 237 | 238 | if __name__ == "__main__": 239 | main() 240 | print("mkdoc: OK") 241 | 242 | 243 | -------------------------------------------------------------------------------- /huygens/wiggle/shapes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | make some shapes: pair of pants, tube's, etc. 6 | 7 | """ 8 | 9 | from huygens.namespace import * 10 | from huygens.argv import argv 11 | 12 | from huygens import pov 13 | from huygens.pov import View, Mat 14 | 15 | from huygens.wiggle import Cell0, Cell1, Cell2 16 | 17 | def make_pants_rev(m=None, i=None, cone=1.0): 18 | 19 | if i is None: 20 | i = Cell0("i", stroke=None) 21 | if m is None: 22 | m = Cell0("m", fill=grey) 23 | 24 | mm = Cell1(m(skip=True), m(skip=True), 25 | stroke=None, pip_color=None, 26 | _width=0.001, skip=True) 27 | m_m = Cell1(m, m, stroke=None, pip_color=None) 28 | i_mm = Cell1(i, m@m, pip_color=None) 29 | mm_i = Cell1(m@m, i, pip_color=None) 30 | saddle = Cell2( mm_i<= 2*w0) # space between legs 65 | add(left.tgt.pip_x - left.tgt.tgt.pip_x >= 0.5*w0) # space to the left of left leg 66 | add(right.tgt.src.pip_x - right.tgt.pip_x >= 0.5*w0) # space to the right of right leg 67 | pants = pants(on_constrain = constrain_pants, assoc=False) 68 | return pants 69 | 70 | 71 | def make_pants(m=None, i=None, cone=1.0): 72 | 73 | if i is None: 74 | i = Cell0("i", stroke=None) 75 | if m is None: 76 | m = Cell0("m", fill=grey) 77 | 78 | mm = Cell1(m(skip=True), m(skip=True), 79 | stroke=None, pip_color=None, 80 | _width=0.001, skip=True) 81 | 82 | m_m = Cell1(m, m, stroke=None, pip_color=None) 83 | i_mm = Cell1(i, m@m, pip_color=None) 84 | mm_i = Cell1(m@m, i, pip_color=None) 85 | 86 | saddle = Cell2( mm@mm, mm_i << i_mm, pip_color=None) 87 | 88 | lfold = Cell2(i_mm, i_mm, cone=cone, pip_color=None) 89 | rfold = Cell2(mm_i, mm_i, cone=cone, pip_color=None) 90 | pants = lfold << saddle << rfold 91 | 92 | def constrain_pants(cell, system): 93 | #print("constrain_pants", cell) 94 | add = system.add 95 | left, saddle, right = cell 96 | ws = [ 97 | left.tgt.src[0].pip_x - left.tgt.pip_x, 98 | left.tgt.src[1].pip_x - left.tgt.pip_x, 99 | left.src.src[0].pip_x - left.src.pip_x, 100 | left.src.src[1].pip_x - left.src.pip_x, 101 | saddle.src[0].pip_x - saddle.src[0].tgt[0].pip_x, 102 | saddle.src[0].pip_x - saddle.src[0].tgt[1].pip_x, 103 | saddle.src[1].src[0].pip_x - saddle.src[1].pip_x, 104 | saddle.src[1].src[1].pip_x - saddle.src[1].pip_x, 105 | right.tgt.pip_x - right.tgt.tgt[0].pip_x, 106 | right.tgt.pip_x - right.tgt.tgt[1].pip_x, 107 | right.src.pip_x - right.src.tgt[0].pip_x, 108 | right.src.pip_x - right.src.tgt[1].pip_x, 109 | ] 110 | w0 = ws[0] 111 | for w in ws[1:]: 112 | add(w == w0) 113 | center = (1/2)*(saddle.src[0].pip_x + saddle.src[1].pip_x) 114 | for cell in saddle.tgt: 115 | add(cell.src.pip_x == cell.tgt.pip_x) 116 | add(cell.src.pip_x == center) # center the waist between the legs 117 | add(left.pip_x == 0.5*(left.tgt.pip_x + left.src.pip_x)) 118 | add(right.pip_x == 0.5*(right.tgt.pip_x + right.src.pip_x)) 119 | add(saddle.src[1].pip_x - saddle.src[0].pip_x >= 2*w0) # space between legs 120 | add(left.src.pip_x - left.src.tgt.pip_x >= 0.5*w0) # space to the left of left leg 121 | add(right.src.src.pip_x - right.src.pip_x >= 0.5*w0) # space to the right of right leg 122 | pants = pants(on_constrain = constrain_pants, assoc=False) 123 | return pants 124 | 125 | 126 | def make_tube(m=None, i=None): 127 | if i is None: 128 | i = Cell0("i", stroke=None) 129 | if m is None: 130 | m = Cell0("m", fill=grey) 131 | 132 | i_mm = Cell1(i, m@m, pip_color=None) 133 | mm_i = Cell1(m@m, i, pip_color=None) 134 | lfold = Cell2(i_mm, i_mm, cone=1.0, pip_color=None) 135 | rfold = Cell2(mm_i, mm_i, cone=1.0, pip_color=None) 136 | tube = lfold << rfold 137 | def constrain_tube(cell, system): 138 | #print("constrain_tube", cell) 139 | add = system.add 140 | left, right = cell 141 | add(left.src.pip_x == left.tgt.pip_x) 142 | add(right.src.pip_x == right.tgt.pip_x) 143 | add(left.pip_x == 0.5*(left.tgt.pip_x + left.src.pip_x)) 144 | add(right.pip_x == 0.5*(right.tgt.pip_x + right.src.pip_x)) 145 | #add(right.src.pip_x - left.src.pip_x == right.tgt.pip_x - left.tgt.pip_x) 146 | tube = tube(on_constrain = constrain_tube, assoc=False) 147 | return tube 148 | 149 | 150 | def test(): 151 | 152 | m = Cell0("m", fill=grey.alpha(0.5)) 153 | pop = make_pants(m) 154 | rpop = make_pants_rev(m) 155 | tube = make_tube(m) 156 | cell = rpop * tube * pop 157 | 158 | cell = cell.layout(height=1.) 159 | cvs = cell.render_cvs(pos="north") 160 | cvs.writePDFfile("pants.pdf") 161 | 162 | print("OK\n") 163 | 164 | 165 | if __name__ == "__main__": 166 | 167 | test() 168 | 169 | 170 | -------------------------------------------------------------------------------- /huygens/live.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from math import log, exp, floor, pi 4 | 5 | from huygens.variable import Variable 6 | from huygens.tool import conv, smooth, clamp 7 | 8 | 9 | EPSILON = 1e-6 10 | 11 | 12 | class Dynamic(Variable): 13 | def __init__(self, state): 14 | self.state = state 15 | self._t_start = state.t # _t_start is creation time (now) 16 | 17 | @property 18 | def t_now(self): 19 | return self.state.t - self._t_start 20 | 21 | def reset_t(self): 22 | #print(self.__class__.__name__, "reset_t") 23 | self._t_start = self.state.t # _t_start is now 24 | 25 | def is_done(self): 26 | return False 27 | 28 | @property 29 | def done(self): 30 | return self.is_done() 31 | 32 | def __lshift__(self, other): 33 | return Sequence(self.state, [self, other]) 34 | #__rshift__ = __lshift__ # ? 35 | 36 | def repeat(self): 37 | return Repeat(self.state, self) 38 | 39 | 40 | class Sequence(Dynamic): 41 | "Sequence of Dynamic Variable's" 42 | def __init__(self, state, children): 43 | Dynamic.__init__(self, state) 44 | assert len(children) 45 | self.children = list(children) 46 | #print("Sequence.__init__", id(self)) 47 | assert not hasattr(self, "idx") 48 | self.idx = 0 49 | 50 | def reset_t(self): 51 | Dynamic.reset_t(self) 52 | self.idx = 0 53 | child = self.children[self.idx] 54 | child.reset_t() 55 | 56 | def _update(self): 57 | children = self.children 58 | assert self.idx < len(children) 59 | #print("Sequence._update: enter idx=", self.idx) 60 | while 1: 61 | child = children[self.idx] 62 | if not child.is_done() or self.idx+1==len(children): 63 | break 64 | #print("Sequence: idx+=1", self.idx) 65 | self.idx += 1 66 | child = children[self.idx] 67 | child.reset_t() 68 | #print("Sequence._update: exit idx=", self.idx) 69 | assert self.idx < len(children) 70 | 71 | def is_done(self): 72 | self._update() 73 | children = self.children 74 | child = children[self.idx] 75 | return self.idx+1==len(children) and child.is_done() 76 | 77 | def __float__(self): 78 | self._update() 79 | child = self.children[self.idx] 80 | return child.__float__() 81 | 82 | def __lshift__(self, other): 83 | return Sequence(self.state, self.children+[other]) 84 | 85 | 86 | class Repeat(Dynamic): 87 | def __init__(self, state, child): 88 | Dynamic.__init__(self, state) 89 | self.child = child 90 | 91 | def reset_t(self): 92 | Dynamic.reset_t(self) 93 | self.child.reset_t() 94 | 95 | def __float__(self): 96 | child = self.child 97 | if child.is_done(): 98 | child.reset_t() 99 | value = child.__float__() 100 | return value 101 | 102 | 103 | class Const(Dynamic): 104 | def __init__(self, state, x, dt=None): 105 | Dynamic.__init__(self, state) 106 | self.x = float(x) 107 | self.dt = dt 108 | 109 | def is_done(self): 110 | return self.dt is not None and self.t_now < self.dt 111 | 112 | def __float__(self): 113 | return self.x 114 | 115 | 116 | class Linear(Dynamic): 117 | def __init__(self, state, x0, dx, modulus=None): 118 | Dynamic.__init__(self, state) 119 | self.x0 = float(x0) 120 | self.dx = float(dx) 121 | self.modulus = modulus 122 | 123 | def __float__(self): 124 | x = self.x0 + self.dx * self.t_now 125 | if self.modulus is not None: 126 | x %= self.modulus 127 | return x 128 | 129 | 130 | class Stepper(Dynamic): 131 | def __init__(self, state, i0, i1, dt, repeat=False): 132 | Dynamic.__init__(self, state) 133 | self.i0 = int(i0) 134 | self.i1 = int(i1) 135 | self.dt = float(dt) 136 | self.repeat = repeat 137 | 138 | def __index__(self): 139 | i = int(floor(self.t_now / self.dt)) + self.i0 140 | if self.repeat: 141 | i %= (self.i1 - self.i0) 142 | return i 143 | 144 | 145 | class Cyclic(Dynamic): 146 | def __init__(self, state, mod=1.0, period=1.0): 147 | Dynamic.__init__(self, state) 148 | self.mod = mod 149 | self.period = period 150 | 151 | def __float__(self): 152 | x = self.t_now / self.period 153 | x %= self.mod 154 | return x 155 | 156 | 157 | class Slider(Dynamic): 158 | " got a _start and stop value over a time period" 159 | def __init__(self, state, x0=0., x1=1., period=1., smooth=True): 160 | Dynamic.__init__(self, state) 161 | self.x0 = float(x0) 162 | self.x1 = float(x1) 163 | self.period = float(period) 164 | self.smooth = smooth 165 | 166 | def is_done(self): 167 | return self.t_now >= self.period-EPSILON 168 | 169 | 170 | class Slew(Slider): 171 | def __float__(self): 172 | # XX do this properly... 173 | self.x0 = conv(self.x0, self.x1, 0.01*self.period) 174 | return self.x0 175 | 176 | class LinSlide(Slider): 177 | def __float__(self): 178 | if self.smooth: 179 | a = smooth(0., 1., self.t_now/self.period) 180 | else: 181 | a = clamp(self.t_now/self.period) 182 | x = conv(self.x0, self.x1, a) 183 | return x 184 | 185 | 186 | class LogSlide(Slider): 187 | def __float__(self): 188 | state = self.state 189 | a = smooth(0, 1, self.t_now/self.period) 190 | r0, r1 = log(self.x0), log(self.x1) 191 | r = conv(r0, r1, a) 192 | x = exp(r) 193 | return x 194 | 195 | 196 | class World(object): 197 | def __init__(self, state): 198 | self.state = state 199 | 200 | def const(self, *args, **kw): 201 | v = Const(self.state, *args, **kw) 202 | return v 203 | 204 | def linear(self, *args, **kw): 205 | v = Linear(self.state, *args, **kw) 206 | return v 207 | lin = linear 208 | 209 | def slide(self, *args, **kw): 210 | v = LinSlide(self.state, *args, **kw) 211 | return v 212 | slider = slide 213 | 214 | def smooth(self, values, dt): 215 | assert len(values)>1 216 | lin = self.slide(values[0], values[1], dt) 217 | idx = 1 218 | while idx + 1 < len(values): 219 | lin = lin << self.slide(values[idx], values[idx+1], dt) 220 | idx += 1 221 | return lin 222 | 223 | def stepper(self, *args, **kw): 224 | v = Stepper(self.state, *args, **kw) 225 | return v 226 | 227 | def log_slide(self, *args, **kw): 228 | v = LogSlide(self.state, *args, **kw) 229 | return v 230 | log_slider = log_slide 231 | 232 | def slew(self, *args, **kw): 233 | v = Slew(self.state, *args, **kw) 234 | return v 235 | 236 | def cyclic(self, *args, **kw): 237 | v = Cyclic(self.state, *args, **kw) 238 | return v 239 | 240 | 241 | -------------------------------------------------------------------------------- /huygens/doc/images/yang-baxter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /huygens/doc/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | 28 | 29 | 30 | 34 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
240 | 241 | CONTENT 242 | 243 |
244 | 245 | Copyright (c) 2018 - 2021. 246 | 247 |
248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /huygens/flatten.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Interpret all transforms, thereby flattening cairo operations 5 | to just a few primitives, such as move_to line_to and curve_to. 6 | """ 7 | 8 | import sys 9 | from math import sin, cos, pi 10 | 11 | from huygens import argv 12 | from huygens import back 13 | from huygens.base import Context, SCALE_CM_TO_POINT 14 | 15 | # Internally huygens uses cm units, even though we are exposing a cairo interface: 16 | 17 | class Flatten(Context): 18 | 19 | def __init__(self): 20 | Context.__init__(self) 21 | self.path = back.Compound() 22 | self.paths = [] 23 | 24 | def move_to(self, x, y): 25 | x = x/SCALE_CM_TO_POINT # scale to cm units 26 | y = y/SCALE_CM_TO_POINT # scale to cm units 27 | x, y = self.matrix(x, y) 28 | item = back.MoveTo(x, -y) 29 | self.path.append(item) 30 | self.pos = x, y 31 | 32 | def line_to(self, x, y): 33 | x = x/SCALE_CM_TO_POINT # etc.. 34 | y = y/SCALE_CM_TO_POINT 35 | x, y = self.matrix(x, y) 36 | item = back.LineTo(x, -y) 37 | self.path.append(item) 38 | self.pos = x, y 39 | 40 | def curve_to(self, x0, y0, x1, y1, x2, y2): 41 | #print("curve_to", x0, y0, x1, y1, x2, y2) 42 | x0 = x0/SCALE_CM_TO_POINT 43 | y0 = y0/SCALE_CM_TO_POINT 44 | x1 = x1/SCALE_CM_TO_POINT 45 | y1 = y1/SCALE_CM_TO_POINT 46 | x2 = x2/SCALE_CM_TO_POINT 47 | y2 = y2/SCALE_CM_TO_POINT 48 | #print("\t", x0, y0, x1, y1, x2, y2) 49 | x0, y0 = self.matrix(x0, y0) 50 | x1, y1 = self.matrix(x1, y1) 51 | x2, y2 = self.matrix(x2, y2) 52 | item = back.CurveTo(x0, -y0, x1, -y1, x2, -y2) 53 | self.path.append(item) 54 | self.pos = x2, y2 55 | 56 | def rel_move_to(self, dx, dy): 57 | assert self.pos is not None, "no current point" 58 | dx = dx/SCALE_CM_TO_POINT 59 | dy = dy/SCALE_CM_TO_POINT 60 | x, y = self.pos 61 | dx, dy = self.matrix.transform_distance(dx, dy) 62 | x, y = x+dx, y+dy 63 | item = back.MoveTo(x, -y) 64 | self.path.append(item) 65 | self.pos = x, y 66 | 67 | def rel_line_to(self, dx, dy): 68 | assert self.pos is not None, "no current point" 69 | dx = dx/SCALE_CM_TO_POINT 70 | dy = dy/SCALE_CM_TO_POINT 71 | x, y = self.pos 72 | dx, dy = self.matrix.transform_distance(dx, dy) 73 | x, y = x+dx, y+dy 74 | item = back.LineTo(x, -y) 75 | self.path.append(item) 76 | self.pos = x, y 77 | 78 | def rel_curve_to(self, dx0, dy0, dx1, dy1, dx2, dy2): 79 | assert self.pos is not None, "no current point" 80 | dx0 = dx0/SCALE_CM_TO_POINT 81 | dy0 = dy0/SCALE_CM_TO_POINT 82 | dx1 = dx1/SCALE_CM_TO_POINT 83 | dy1 = dy1/SCALE_CM_TO_POINT 84 | dx2 = dx2/SCALE_CM_TO_POINT 85 | dy2 = dy2/SCALE_CM_TO_POINT 86 | x, y = self.pos 87 | dx0, dy0 = self.matrix.transform_distance(dx0, dy0) 88 | dx1, dy1 = self.matrix.transform_distance(dx1, dy1) 89 | dx2, dy2 = self.matrix.transform_distance(dx2, dy2) 90 | x0, y0 = x+dx0, y+dy0 91 | x1, y1 = x+dx1, y+dy1 92 | x2, y2 = x+dx2, y+dy2 93 | item = back.CurveTo(x0, -y0, x1, -y1, x2, -y2) 94 | self.path.append(item) 95 | self.pos = x2, y2 96 | 97 | def arc(self, x, y, radius, angle1, angle2): 98 | # stay in user space coordinates 99 | x = x/SCALE_CM_TO_POINT 100 | y = y/SCALE_CM_TO_POINT 101 | radius = radius/SCALE_CM_TO_POINT 102 | if self.pos is None: 103 | x1, y1 = x+radius*cos(angle1), y+radius*sin(angle1) 104 | self.move_to(x1, y1) 105 | p = back.arc_to_bezier(x, -y, radius, -angle2, -angle1) 106 | p = p.reversed() 107 | p.process_cairo(self) 108 | 109 | def arc_negative(self, x, y, radius, angle1, angle2): 110 | # stay in user space coordinates 111 | x = x/SCALE_CM_TO_POINT 112 | y = y/SCALE_CM_TO_POINT 113 | radius = radius/SCALE_CM_TO_POINT 114 | if self.pos is None: 115 | x1, y1 = x+radius*cos(angle1), y+radius*sin(angle1) 116 | self.move_to(x1, y1) 117 | p = back.arc_to_bezier(x, -y, radius, -angle1, -angle2) 118 | p.process_cairo(self) 119 | 120 | def close_path(self): 121 | item = back.ClosePath() 122 | self.path.append(item) 123 | 124 | def set_source_rgba(self, r, g, b, a): 125 | deco = back.RGBA(r, g, b, a) 126 | self.path.append(deco) 127 | 128 | def set_line_width(self, w): 129 | w /= SCALE_CM_TO_POINT 130 | wx, wy = self.matrix.transform_distance(w, w) 131 | w = wx 132 | deco = back.LineWidth(w) 133 | self.path.append(deco) 134 | 135 | def stroke(self): 136 | deco = back.Stroke() 137 | self.path.append(deco) 138 | self.paths.append(self.path) 139 | self.path = back.Compound() 140 | self.pos = None 141 | 142 | def fill_preserve(self): 143 | deco = back.FillPreserve() 144 | self.path.append(deco) 145 | 146 | def fill(self): 147 | deco = back.Fill() 148 | self.path.append(deco) 149 | self.paths.append(self.path) 150 | self.path = back.Compound() 151 | self.pos = None 152 | 153 | def set_line_cap(self, arg): 154 | import cairo 155 | for desc in "butt round square".split(): 156 | if getattr(cairo, "LINE_CAP_"+(desc.upper())) == arg: 157 | self.path.append(back.LineCap(desc)) 158 | break 159 | else: 160 | assert 0 161 | 162 | def ignore(self, *args, **kw): 163 | pass 164 | 165 | def set_fill_rule(self, arg): 166 | print("TODO: Flatten.set_fill_rule", arg) 167 | self.set_fill_rule = self.ignore 168 | 169 | 170 | def test(): 171 | 172 | def draw_test(cxt): 173 | cxt.move_to(10, 10) 174 | cxt.line_to(50, 50) 175 | 176 | cxt.translate(100., 0.) 177 | cxt.scale(0.8, 0.7) 178 | 179 | cxt.move_to(10., 10.) 180 | cxt.line_to(100., 100.) 181 | cxt.arc(200., 200., 80., 0., 1.1*pi) 182 | cxt.rotate(0.3) 183 | 184 | cxt.scale(0.7, 1.2) 185 | cxt.translate(50., 50.) 186 | 187 | cxt.line_to(300., 300.) 188 | cxt.rotate(-0.3) 189 | cxt.arc_negative(400., 300., 60., 0., -1.8*pi) 190 | cxt.line_to(600.-10, 400.-10) 191 | cxt.stroke() 192 | 193 | def _draw_test(cxt): 194 | cxt.move_to(10, 10) 195 | cxt.line_to(50, 50) 196 | cxt.stroke() 197 | 198 | import cairo 199 | 200 | W, H = 600., 400. # point == 1/72 inch 201 | 202 | # black line should follow the red line. 203 | surface = cairo.PDFSurface("output.pdf", W, H) 204 | context = cairo.Context(surface) 205 | 206 | context.save() 207 | context.set_source_rgba(1., 0., 0., 0.5) 208 | context.set_line_width(10.) 209 | draw_test(context) 210 | context.restore() 211 | 212 | cxt = Flatten() 213 | draw_test(cxt) 214 | for path in cxt.paths: 215 | path.process_cairo(context) 216 | 217 | surface.finish() 218 | 219 | print("OK") 220 | 221 | 222 | 223 | if __name__ == "__main__": 224 | 225 | test() 226 | 227 | 228 | -------------------------------------------------------------------------------- /huygens/doc/images/canbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /huygens/doc/images/braid-Z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /huygens/doc/images/table-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /huygens/doc/test_sat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 82 | 83 | 97 | 98 | 99 |
100 | 101 | 102 |

<<< table of contents

103 |
104 |

Constraint satisfier

105 |
from huygens.sat import Variable, Solver, System
106 | 
107 | x = Variable('x')
108 | y = Variable('y')
109 | z = Variable('z')
110 | 
111 | items = [
112 |     x+y >= 1.,
113 |     x+z == 5,
114 |     y >= 3.
115 | ]
116 | 
117 | solver = Solver(items)
118 | 
119 | result = solver.solve()
120 | print(result)
121 | 
122 | system = System()
123 | v = system.get_var()
124 | u = system.get_var()
125 | w = system.get_var()
126 | system.add(v+u+w == 3.)
127 | system.solve()
128 | print(system[v] + system[u] + system[w])
129 | 
130 | 131 | 132 |
133 | 134 | Copyright (c) 2018 - 2020. 135 | 136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /huygens/doc/images/braid-strands.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /huygens/turtle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | copied from arrowtheory repo 4 | 5 | newer version: huygens.the_turtle 6 | """ 7 | 8 | 9 | import sys 10 | 11 | from math import sin, cos, pi, asin, acos, atan 12 | from huygens.front import * 13 | 14 | # XXX TODO 15 | # Cleanup this mess: 16 | # (1) don't just build paths out of straight lines 17 | # (2) construct actual Path object in Turtle instances 18 | 19 | 20 | def mkpath(ps, closepath=False): 21 | ps = [path.moveto(*ps[0])]+[path.lineto(*p) for p in ps[1:]] 22 | if closepath: 23 | ps.append(path.closepath()) 24 | p = path.path(*ps) 25 | return p 26 | 27 | 28 | def dopath(ps, attrs=[], fill=[], closepath=False, smooth=0.0, stroke=True, cvs=None): 29 | #print("dopath:", ps) 30 | c = cvs 31 | if len(ps) < 2: 32 | return 33 | p = mkpath(ps) 34 | attrs = list(attrs) 35 | if smooth: 36 | attrs.append(deformer.smoothed(smooth)) # XXX not implemented 37 | if fill: 38 | c.fill(p, attrs+fill) 39 | if stroke: 40 | c.stroke(p, attrs) 41 | 42 | 43 | class Turtle(object): 44 | default_attrs = [style.linewidth.thin, style.linecap.round, style.linejoin.round] 45 | 46 | def __init__(self, x=0.0, y=0.0, angle=0.0, cvs=None, attrs=None): 47 | "angle: clockwise degrees starting from angle=0.0 is up" 48 | print("Turtle is deprecated: use the_turtle") 49 | self.x = x 50 | self.y = y 51 | self.theta = (angle/360.)*2*pi 52 | self.ps = [(x, y)] 53 | self.paths = [] 54 | self.pen = True 55 | self._save = None 56 | self.cvs = cvs 57 | if attrs is None: 58 | attrs = self.default_attrs 59 | self.attrs = attrs 60 | 61 | def startat(self): 62 | angle = 360*self.theta/(2*pi) 63 | return Turtle(self.x, self.y, angle, self.cvs, self.attrs) 64 | 65 | def copy(self): 66 | import copy 67 | t = copy.deepcopy(self) 68 | return t 69 | 70 | def save(self): 71 | assert self._save is None 72 | self._save = (self.x, self.y, self.theta) 73 | 74 | def restore(self): 75 | assert self._save is not None 76 | self.x, self.y, self.theta = self._save 77 | self._save = None 78 | 79 | def penup(self): 80 | if self.ps: 81 | self.paths.append(self.ps) 82 | self.pen = False 83 | self.ps = None 84 | return self 85 | 86 | def pendown(self): 87 | if self.pen: 88 | return 89 | self.pen = True 90 | self.ps = [(self.x, self.y)] 91 | return self 92 | 93 | def getpos(self): 94 | return self.x, self.y 95 | 96 | def lookat(self, x, y): 97 | dx = x-self.x 98 | dy = y-self.y 99 | r = (dx**2 + dy**2)**0.5 100 | #assert r > 1e-8, "can't lookat self" 101 | if r < 1e-8: 102 | return self 103 | if dy > EPSILON: 104 | theta = atan(dx/dy) 105 | elif dy < -EPSILON: 106 | theta = atan(dx/dy) + pi 107 | elif dx > EPSILON: 108 | theta = 0.5*pi 109 | elif dx < -EPSILON: 110 | theta = -0.5*pi 111 | else: 112 | assert 0 113 | self.theta = theta 114 | return self 115 | 116 | def fwd(self, d): 117 | self.x += d*sin(self.theta) 118 | self.y += d*cos(self.theta) 119 | if self.pen: 120 | self.ps.append((self.x, self.y)) 121 | return self 122 | 123 | def goto(self, x=None, y=None, angle=None): 124 | if x is not None and y is None: 125 | y = self.y 126 | elif y is not None and x is None: 127 | x = self.x 128 | if x is not None and y is not None: 129 | self.lookat(x, y) 130 | self.ps.append((x, y)) 131 | self.x = x 132 | self.y = y 133 | if angle is not None: 134 | self.theta = (angle/360.)*2*pi 135 | return self 136 | moveto = goto 137 | 138 | def reverse(self, d): 139 | self.fwd(-d) 140 | return self 141 | back = reverse 142 | rev = reverse 143 | 144 | def right(self, angle, r=0., N=40): 145 | dtheta = (angle/360.)*2*pi 146 | theta = self.theta 147 | self.theta += dtheta 148 | if r==0.: 149 | return self 150 | x, y = self.x, self.y 151 | x0 = x - r*sin(theta-pi/2) 152 | y0 = y - r*cos(theta-pi/2) 153 | for i in range(N): 154 | theta += (1./(N))*dtheta 155 | x = x0 - r*sin(theta+pi/2) 156 | y = y0 - r*cos(theta+pi/2) 157 | if self.pen: 158 | self.ps.append((x, y)) 159 | self.x = x 160 | self.y = y 161 | return self 162 | 163 | def left(self, angle, r=0.): 164 | self.right(-angle, -r) 165 | return self 166 | 167 | def flat_arrow(self, size=0.15, angle=30.): 168 | self.penup() 169 | self.right(angle) 170 | self.back(size) 171 | self.pendown() 172 | self.fwd(size) 173 | self.left(2*angle) 174 | self.back(size) 175 | self.penup() 176 | self.fwd(size) 177 | self.right(angle) 178 | self.pendown() 179 | return self 180 | 181 | def dart_arrow(self, size=0.15, angle=30.): 182 | assert 01: 264 | p = mkpath(ps, closepath) 265 | method = getattr(cvs, name) 266 | if name=="stroke": 267 | method(p, attrs) 268 | elif name=="fill": 269 | method(p, attrs) 270 | else: 271 | method(p) 272 | if not preserve: 273 | self.paths = [] 274 | if self.ps is not None: 275 | self.ps = self.ps[-1:] 276 | return self 277 | 278 | def stroke(self, *args, **kw): 279 | kw["name"] = "stroke" 280 | self._render(*args, **kw) 281 | return self 282 | 283 | def fill(self, *args, **kw): 284 | kw["name"] = "fill" 285 | self._render(*args, **kw) 286 | return self 287 | 288 | def clip(self, *args, **kw): 289 | kw["name"] = "clip" 290 | self._render(*args, **kw) 291 | return self 292 | 293 | def stroke_preserve(self, *args, **kw): 294 | kw["name"] = "stroke" 295 | kw["preserve"] = True 296 | self._render(*args, **kw) 297 | return self 298 | 299 | def fill_preserve(self, *args, **kw): 300 | kw["name"] = "fill" 301 | kw["preserve"] = True 302 | self._render(*args, **kw) 303 | return self 304 | 305 | 306 | -------------------------------------------------------------------------------- /huygens/doc/images/vbox-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /huygens/doc/images/hbox-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | --------------------------------------------------------------------------------