├── backyard ├── mill │ ├── README │ ├── old │ │ ├── zero.prn │ │ ├── drip.py │ │ ├── view.py │ │ └── art.py │ └── send.py ├── make_horn.py ├── tentacle.py ├── design_horn.py ├── math.txt ├── whistle_tuning.py ├── math.py └── one_hole.py ├── MANIFEST.in ├── doc ├── pflute-fingering.odt ├── pflute-fingering.pdf ├── shawm-fingering.odt ├── shawm-fingering.pdf ├── folk-shawm-fingering.odt ├── folk-shawm-fingering.pdf ├── three-hole-whistle-fingering.odt ├── three-hole-whistle-fingering.pdf ├── folk-flute-and-whistle-fingering.odt └── folk-flute-and-whistle-fingering.pdf ├── demakein ├── __main__.py ├── raphs_curves │ ├── cloth_off.py │ ├── __init__.py │ ├── offset.py │ ├── euler-elastica.py │ ├── polymat-bad.py │ ├── clothoid.py │ ├── band.py │ ├── cornu.py │ ├── poly3.py │ ├── plot_solve_clothoid.py │ ├── numintsynth.py │ ├── bezfigs.py │ ├── pcorn.py │ ├── mvc.py │ └── fromcubic.py ├── make_cork.py ├── grace.py ├── __init__.py ├── make_windcap.py ├── make_bauble.py ├── make_reed.py ├── selection.py ├── make_mouthpiece.py ├── svg.py ├── tune.py ├── sketch.py ├── all.py ├── workspace.py ├── optimize.py ├── make_flute.py ├── engine_trimesh.py ├── mask.py ├── make_panpipe.py ├── profile.py ├── make_shawm.py └── design_flute.py ├── .gitignore ├── examples ├── mywhistle.py ├── simple_reedpipe.py ├── simple_flute.py ├── simple_shawm.py ├── stepped_shawm.py ├── drinking_straw.py └── pentatonic_flute.py ├── LICENSE ├── pyproject.toml ├── CHANGES ├── test.sh └── README.md /backyard/mill/README: -------------------------------------------------------------------------------- 1 | 2 | These are scripts for controlling my Roland MDX-20. 3 | 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README 2 | include CHANGES 3 | include doc/* 4 | include examples/*.py -------------------------------------------------------------------------------- /doc/pflute-fingering.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/pflute-fingering.odt -------------------------------------------------------------------------------- /doc/pflute-fingering.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/pflute-fingering.pdf -------------------------------------------------------------------------------- /doc/shawm-fingering.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/shawm-fingering.odt -------------------------------------------------------------------------------- /doc/shawm-fingering.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/shawm-fingering.pdf -------------------------------------------------------------------------------- /demakein/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | from demakein import main 3 | 4 | if __name__ == '__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /demakein/raphs_curves/cloth_off.py: -------------------------------------------------------------------------------- 1 | # Fancy new algorithms for computing the offset of a clothoid. 2 | 3 | -------------------------------------------------------------------------------- /doc/folk-shawm-fingering.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/folk-shawm-fingering.odt -------------------------------------------------------------------------------- /doc/folk-shawm-fingering.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/folk-shawm-fingering.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | test/output/* 3 | MANIFEST 4 | build/* 5 | dist/* 6 | demakein.egg-info/* 7 | output 8 | uv.lock 9 | -------------------------------------------------------------------------------- /doc/three-hole-whistle-fingering.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/three-hole-whistle-fingering.odt -------------------------------------------------------------------------------- /doc/three-hole-whistle-fingering.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/three-hole-whistle-fingering.pdf -------------------------------------------------------------------------------- /doc/folk-flute-and-whistle-fingering.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/folk-flute-and-whistle-fingering.odt -------------------------------------------------------------------------------- /doc/folk-flute-and-whistle-fingering.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfh/demakein/HEAD/doc/folk-flute-and-whistle-fingering.pdf -------------------------------------------------------------------------------- /demakein/raphs_curves/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Raph Levien's cornu spiral utilities 4 | GPL 5 | http://www.levien.com/spiro/ 6 | """ 7 | -------------------------------------------------------------------------------- /backyard/mill/old/zero.prn: -------------------------------------------------------------------------------- 1 | ;;^IN;!MC0; 2 | V15.0;^PR;Z0,0,2420;^PA; 3 | !MC1; 4 | Z0,0,2420; 5 | V0.5;Z0,0,40; 6 | V0.5;Z0,0,0; 7 | !MC0; 8 | -------------------------------------------------------------------------------- /examples/mywhistle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import demakein 4 | 5 | class Design_my_whistle(demakein.Design_folk_whistle): 6 | transpose = 12 7 | 8 | # ... and any other things you want to change ... 9 | 10 | 11 | if __name__ == '__main__': 12 | demakein.run_toolbox( 13 | [ Design_my_whistle, demakein.Make_whistle ], 14 | show_make_flags=False) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | demakein - software to help design and make wind instruments. 2 | Copyright (C) 2025 Paul Harrison 3 | 4 | This library is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU Lesser General Public 6 | License as published by the Free Software Foundation; either 7 | version 2.1 of the License, or (at your option) any later version. 8 | 9 | This library is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | Lesser General Public License for more details. -------------------------------------------------------------------------------- /demakein/raphs_curves/offset.py: -------------------------------------------------------------------------------- 1 | # offset curve of piecewise cornu curves 2 | 3 | from math import * 4 | 5 | from . import pcorn 6 | from .clothoid import mod_2pi 7 | 8 | def seg_offset(seg, d): 9 | th0 = seg.th(0) 10 | th1 = seg.th(seg.arclen) 11 | z0 = [seg.z0[0] + d * sin(th0), seg.z0[1] - d * cos(th0)] 12 | z1 = [seg.z1[0] + d * sin(th1), seg.z1[1] - d * cos(th1)] 13 | chth = atan2(z1[1] - z0[1], z1[0] - z0[0]) 14 | return [pcorn.Segment(z0, z1, mod_2pi(chth - th0), mod_2pi(th1 - chth))] 15 | 16 | 17 | def offset(curve, d): 18 | segs = [] 19 | for seg in curve.segs: 20 | segs.extend(seg_offset(seg, d)) 21 | return pcorn.Curve(segs) 22 | -------------------------------------------------------------------------------- /backyard/mill/old/drip.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, time, serial 3 | 4 | commands = open(sys.argv[1],'rb').read().rstrip(';').split(';') 5 | print(len(commands), 'commands') 6 | 7 | port = serial.Serial( 8 | port = 'COM3', #Change this to the appropriate port 9 | baudrate = 9600, 10 | ) 11 | 12 | start = time.time() 13 | for i, command in enumerate(commands): 14 | command = command.strip() + ';' 15 | 16 | while not port.getDSR(): 17 | time.sleep(0.01) 18 | port.write(command) 19 | 20 | delta = int(time.time()-start) 21 | sys.stdout.write('\r%3dmin %3.0f%%' % ( 22 | delta // 60, 23 | (i+1.0)/len(commands)*100.0 24 | )) 25 | sys.stdout.flush() 26 | 27 | port.close() 28 | print() -------------------------------------------------------------------------------- /backyard/make_horn.py: -------------------------------------------------------------------------------- 1 | 2 | import nesoni 3 | from demakein import make 4 | 5 | from design_horn import Design_horn 6 | 7 | class Make_horn(make.Make_millable_instrument): 8 | def run(self): 9 | spec = self.working.spec 10 | 11 | self.make_instrument( 12 | inner_profile=spec.inner, 13 | outer_profile=spec.outer, 14 | hole_positions=[], 15 | hole_diameters=[], 16 | hole_vert_angles=[], 17 | hole_horiz_angles=[], 18 | xpad=[], 19 | ypad=[], 20 | with_fingerpad=[] 21 | ) 22 | 23 | self.make_parts(up=True, flip_top=True) 24 | #l = spec.length 25 | #self.segment([l*0.333,l*0.666], spec.length, up=True) 26 | 27 | if __name__ == '__main__': 28 | nesoni.run_tool(Make_horn) -------------------------------------------------------------------------------- /demakein/make_cork.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from . import config, shape, profile, make 4 | 5 | @config.help( 6 | 'Make a cork.', 7 | """\ 8 | The cork will taper from a diameter of diameter - taper-in to a diamter of diameter + taper-out. 9 | """) 10 | @config.Float_flag('length') 11 | @config.Float_flag('diameter') 12 | @config.Float_flag('taper_in') 13 | @config.Float_flag('taper_out') 14 | class Make_cork(make.Make, make.Miller): 15 | diameter = 10.0 16 | length = 10.0 17 | taper_in = 0.25 18 | taper_out = 0.125 19 | 20 | def run(self): 21 | d1 = self.diameter + self.taper_out 22 | d2 = self.diameter - self.taper_in 23 | l = self.length 24 | cork = shape.extrude_profile(profile.make_profile([(0.0,d1),(l,d2)])) 25 | 26 | self.save(cork, 'cork') 27 | 28 | mill = self.miller(cork) 29 | self.save(mill, 'mill-cork') 30 | -------------------------------------------------------------------------------- /demakein/raphs_curves/euler-elastica.py: -------------------------------------------------------------------------------- 1 | from math import * 2 | 3 | def plot_elastica(a, c): 4 | s = 500 5 | cmd = 'moveto' 6 | dx = .001 7 | x, y = 0, 0 8 | if c * c > 2 * a * a: 9 | g = sqrt(c * c - 2 * a * a) 10 | x = g + .01 11 | if c == 0: 12 | x = .001 13 | try: 14 | for i in range(1000): 15 | print(6 + s * x, 200 + s * y, cmd) 16 | cmd = 'lineto' 17 | x += dx 18 | if 1 and c * c > 2 * a * a: 19 | print((c * c - x * x) * (x * x - g * g)) 20 | dy = dx * (x * x - .5 * c * c - .5 * g * g) / sqrt((c * c - x * x) * (x * x - g * g)) 21 | else: 22 | dy = dx * (a * a - c * c + x * x)/sqrt((c * c - x * x) * (2 * a * a - c * c + x * x)) 23 | y += dy 24 | except ValueError as e: 25 | pass 26 | print('stroke') 27 | 28 | plot_elastica(1, 0) 29 | print('showpage') 30 | -------------------------------------------------------------------------------- /backyard/tentacle.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, os 3 | 4 | sys.path.insert(0, os.path.normpath(os.path.join(__file__,'..','..'))) 5 | 6 | import demakein 7 | from demakein import shape, geom, make, profile 8 | 9 | import nesoni 10 | 11 | class Tentacle(make.Make): 12 | def run(self): 13 | path = geom.path( 14 | geom.XYZ(0.0,0.0,0.0), 15 | geom.XYZ(0.0,1.0,0.0), 16 | geom.XYZ(1.0,0.0,0.0), 17 | geom.XYZ(0.0,0.0,100.0), 18 | geom.XYZ(0.0,0.0,1.0), 19 | geom.XYZ(1.0,0.0,0.0), 20 | ) 21 | 22 | #geom.plot(path.position) 23 | 24 | print(path.get_length()) 25 | print(path.find(90.0)) 26 | print(path.get_point(90.0)) 27 | 28 | l = path.get_length() 29 | 30 | prof = profile.make_profile([(0.0,20.0),(l,0.0)]) 31 | 32 | tentacle = shape.path_extrusion(path,shape.circle,prof) 33 | self.save(tentacle, 'tentacle') 34 | 35 | if __name__ == '__main__': 36 | nesoni.run_tool(Tentacle) -------------------------------------------------------------------------------- /demakein/grace.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | 4 | Some helper facilities, originally from nesoni. 5 | 6 | """ 7 | 8 | import sys 9 | 10 | from .config import Error, filesystem_friendly_name 11 | 12 | class Log: 13 | def __init__(self): 14 | self.text = [ ] 15 | self.f = None 16 | 17 | def attach(self, f): 18 | assert not self.f 19 | self.f = f 20 | self.f.write(''.join(self.text)) 21 | self.f.flush() 22 | 23 | def close(self): 24 | if self.f is not None: 25 | self.f.close() 26 | self.f = None 27 | 28 | def log(self, text): 29 | sys.stderr.write(text) 30 | self.quietly_log(text) 31 | 32 | def quietly_log(self, text): 33 | self.text.append(text) 34 | if self.f: 35 | self.f.write(text) 36 | self.f.flush() 37 | 38 | 39 | def status(string): 40 | """ Display a status string. """ 41 | from . import legion 42 | return legion.coordinator().set_status( legion.process_identity(), string ) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/simple_reedpipe.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | A basic reedpipe. 4 | 5 | Note that the finger holes are underconstrained by the fingering system. Constraints such as balance and min_hole_spacing can be added to produce a comfortable instrument. 6 | 7 | Note also the reed has some virtual length, so the real instrument length will be shorter than designed. 8 | 9 | """ 10 | 11 | import demakein 12 | 13 | class Reedpipe(demakein.design.Instrument_designer): 14 | closed_top = True 15 | 16 | inner_diameters = [ 6.0, 6.0 ] 17 | outer_diameters = [ 10.0, 10.0 ] 18 | 19 | min_hole_diameters = [ 3.0 ]*7 20 | max_hole_diameters = [ 5.0 ]*7 21 | 22 | #min_hole_spacing = [ 15.0 ]*6 23 | #and/or 24 | balance = [ 0.07 ]*5 25 | 26 | initial_length = demakein.design.wavelength('C4') * 0.25 27 | 28 | fingerings = [ 29 | ('C4', [1,1,1,1,1,1,1]), 30 | ('D4', [0,1,1,1,1,1,1]), 31 | ('E4', [0,0,1,1,1,1,1]), 32 | ('F4', [0,0,0,1,1,1,1]), 33 | ('G4', [0,0,0,0,1,1,1]), 34 | ('A4', [0,0,0,0,0,1,1]), 35 | ('B4', [0,0,0,0,0,0,1]), 36 | ('C5', [0,0,0,0,0,1,0]), 37 | ] 38 | 39 | if __name__ == '__main__': 40 | demakein.run_tool(Reedpipe) 41 | 42 | -------------------------------------------------------------------------------- /backyard/design_horn.py: -------------------------------------------------------------------------------- 1 | 2 | import nesoni, demakein 3 | from demakein import design 4 | 5 | class Design_horn(demakein.design.Instrument_designer): 6 | closed_top = True 7 | 8 | # inner_diameters = [ 70.0, 50.0, 30.0, 10.0, 3.0, 3.0, 15.0 ] 9 | inner_diameters = [ 50.0, 40.0, 30.0, 20.0, 10.0, 5.0, 2.5, 2.5, 15.0 ] 10 | 11 | min_inner_fraction_sep = [ 0.001 ] * (len(inner_diameters)-2) + [ 0.01 ] 12 | 13 | initial_inner_fractions = [ 14 | 1.0 - item / inner_diameters[0] 15 | for item in inner_diameters[1:-1] 16 | ] 17 | 18 | outer_diameters = [ 8.0, 8.0 ] 19 | outer_add = True 20 | 21 | initial_length = demakein.design.wavelength('C4') * 0.5 22 | fingerings = [ 23 | ('C4', []), 24 | ('C4*2', []), 25 | ('C4*3', []), 26 | ('C4*4', []), 27 | ('C4*5', []), 28 | ('C4*6', []), 29 | ('C4*7', []), 30 | ('C4*8', []), 31 | ('C4*9', []), 32 | #('C4*10', []), 33 | #('C4*11', []), 34 | #('C4*12', []), 35 | ] 36 | 37 | divisions = [ 38 | #[(-1,0.333),(-1,0.6666)], 39 | #[(-1,0.25),(-1,0.5),(-1,0.75)], 40 | [(-1,i/8.0) for i in range(1,8) ], 41 | ] 42 | 43 | if __name__ == '__main__': 44 | nesoni.run_tool(Design_horn) -------------------------------------------------------------------------------- /backyard/mill/old/view.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, pylab 3 | 4 | f = open('r.dump','rb') 5 | item = eval(f.read().strip()) 6 | f.close() 7 | 8 | xs = [ ] 9 | ys = [ ] 10 | zs = [ ] 11 | for item2 in item: 12 | xs.append(item2[0][0]) 13 | ys.append(item2[0][1]) 14 | zs.append(item2[0][2]) 15 | 16 | if 't' in sys.argv[1:]: 17 | pylab.subplot(3,1,1) 18 | pylab.plot(xs) 19 | pylab.subplot(3,1,2) 20 | pylab.plot(ys) 21 | pylab.subplot(3,1,3) 22 | pylab.plot(zs) 23 | pylab.show() 24 | 25 | else: 26 | 27 | import matplotlib as mpl 28 | from mpl_toolkits.mplot3d import Axes3D 29 | import numpy as np 30 | import matplotlib.pyplot as plt 31 | 32 | mpl.rcParams['legend.fontsize'] = 10 33 | 34 | fig = plt.figure() 35 | ax = fig.gca(projection='3d') 36 | #theta = np.linspace(-4 * np.pi, 4 * np.pi, 100) 37 | #z = np.linspace(-2, 2, 100) 38 | #r = z**2 + 1 39 | #x = r * np.sin(theta) 40 | #y = r * np.cos(theta) 41 | #ax.plot(xs, ys, zs, ',') 42 | 43 | n = 20 44 | for i in range(n-1,-1,-1): 45 | a = i*(len(xs)-1)//n 46 | b = (i+1)*(len(xs)-1)//n + 1 47 | c = 1.0*i/n 48 | c2 = abs(c*2-1) 49 | ax.plot(xs[a:b], ys[a:b], zs[a:b], color=(c,c2,1-c)) 50 | 51 | ax.legend() 52 | 53 | plt.show() 54 | 55 | -------------------------------------------------------------------------------- /examples/simple_flute.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | A basic flute or ney. 4 | 5 | The embouchure hole is not modelled. 6 | 7 | To use the design produced: 8 | - make a flute without finger holes, just embouchure hole and end stopper. 9 | - trim so that bottom note is in tune. 10 | - drill finger holes. 11 | 12 | The flute is a simple cylinder, so the tuning is not perfect over the two registers unless the finger holes are very large. 13 | 14 | """ 15 | 16 | import demakein 17 | 18 | class Flute(demakein.design.Instrument_designer): 19 | closed_top = False 20 | 21 | inner_diameters = [ 10.0, 10.0 ] 22 | outer_diameters = [ 14.0, 14.0 ] 23 | 24 | min_hole_diameters = [ 3.0 ]*6 25 | max_hole_diameters = [ 8.0 ]*6 26 | 27 | #min_hole_spacing = [ 15.0 ]*5 28 | #and/or 29 | balance = [ 0.05, None, None, 0.05 ] 30 | 31 | initial_length = demakein.design.wavelength('D5') * 0.5 32 | 33 | fingerings = [ 34 | ('D5', [1,1,1,1,1,1]), 35 | ('E5', [0,1,1,1,1,1]), 36 | ('F#5', [0,0,1,1,1,1]), 37 | ('G5', [0,0,0,1,1,1]), 38 | ('A5', [0,0,0,0,1,1]), 39 | ('B5', [0,0,0,0,0,1]), 40 | ('C#6', [0,0,0,0,0,0]), 41 | ('D6', [1,1,1,1,1,1]), 42 | ('E6', [0,1,1,1,1,1]), 43 | ('F#6', [0,0,1,1,1,1]), 44 | ('G6', [0,0,0,1,1,1]), 45 | ('A6', [0,0,0,0,1,1]), 46 | ('B6', [0,0,0,0,0,1]), 47 | ('C#7', [0,0,0,0,0,0]), 48 | ('D8', [1,1,1,1,1,1]), 49 | ] 50 | 51 | if __name__ == '__main__': 52 | demakein.run_tool(Flute) 53 | 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | 8 | name = "demakein" 9 | 10 | authors = [ 11 | { name="Paul Harrison", email="paul.francis.harrison@gmail.com" } 12 | ] 13 | 14 | description = "Design woodwind instruments and make them with a 3D printer or CNC mill." 15 | 16 | readme = "README.md" 17 | 18 | requires-python = ">=3.8" 19 | 20 | classifiers = [ 21 | "Programming Language :: Python :: 3", 22 | "Operating System :: OS Independent", 23 | "Topic :: Scientific/Engineering", 24 | "Topic :: Multimedia :: Graphics :: 3D Modeling", 25 | ] 26 | 27 | license = "LGPL-2.1" 28 | license-files = ["LICENSE"] 29 | 30 | dependencies = [ 31 | # "cffi", # Needed for CGAL 3D engine, no longer the default 3D engine. 32 | "trimesh", 33 | "manifold3d", # Needed by trimesh for correct boolean operations. 34 | "scipy", # Needed by trimesh for convex hull, used in milling. Also KDTree for merging nearby vertices to clean up meshes. 35 | "shapely", # Needed for milling. 36 | ] 37 | 38 | dynamic = [ "version" ] 39 | 40 | [project.scripts] 41 | demakein = "demakein:main" 42 | 43 | [project.urls] 44 | Homepage = "https://logarithmic.net/pfh/design" 45 | Source = "https://github.com/pfh/demakein" 46 | 47 | 48 | [tool.hatch.build.targets.sdist] 49 | only-include = [ "demakein" ] 50 | 51 | [tool.hatch.version] 52 | path = "demakein/__init__.py" 53 | pattern = "VERSION = '(?P[^']+)'" 54 | 55 | [tool.hatch.envs.pypy] 56 | type = "virtual" 57 | installer = "uv" 58 | python = "pypy" -------------------------------------------------------------------------------- /examples/simple_shawm.py: -------------------------------------------------------------------------------- 1 | 2 | import demakein 3 | 4 | class Design_simple_shawm(demakein.design.Instrument_designer): 5 | closed_top = True 6 | 7 | # Bore will be 35mm at end, 4mm at top 8 | inner_diameters = [ 35.0, 4.0 ] 9 | 10 | # Outer profile to be in addition to inner profile 11 | outer_add = True 12 | # This determines the depth of the holes, 5mm extra diameter = 2.5mm depth 13 | outer_diameters = [ 5.0, 5.0 ] 14 | 15 | # Limits on finger holes sizes 16 | min_hole_diameters = [ 2.0 ]*6 17 | max_hole_diameters = [ 15.0 ]*6 18 | 19 | # Limit how close successive holes can be to each other 20 | #min_hole_spacing = [ 5,5,5,20,20 ] 21 | # see also max_hole_spacing 22 | # balance 23 | 24 | # Very simple fingering system 25 | initial_length = demakein.design.wavelength('D4') * 0.4 26 | fingerings = [ 27 | ('D4', [1,1,1,1,1,1]), 28 | ('E4', [0,1,1,1,1,1]), 29 | ('F#4', [0,0,1,1,1,1]), 30 | ('G4', [0,0,0,1,1,1]), 31 | ('A4', [0,0,0,0,1,1]), 32 | ('B4', [0,0,0,0,0,1]), 33 | ('C#5', [0,0,0,0,0,0]), 34 | ('D5', [1,1,1,1,1,1]), 35 | ('E5', [0,1,1,1,1,1]), 36 | ('F#5', [0,0,1,1,1,1]), 37 | ('G5', [0,0,0,1,1,1]), 38 | ('A5', [0,0,0,0,1,1]), 39 | ('B5', [0,0,0,0,0,1]), 40 | ('C#6', [0,0,0,0,0,0]), 41 | ] 42 | 43 | if __name__ == '__main__': 44 | demakein.run_tool(Design_simple_shawm) 45 | 46 | # or 47 | #nesoni.run_toolbox([ 48 | # Design_simple_shawm, 49 | # #... 50 | # ]) -------------------------------------------------------------------------------- /examples/stepped_shawm.py: -------------------------------------------------------------------------------- 1 | 2 | import demakein 3 | 4 | class Design_stepped_shawm(demakein.design.Instrument_designer): 5 | closed_top = True 6 | 7 | # Bore will be 35mm at end, 4mm at top 8 | inner_diameters = [ 9 | 35.0, 10 | (35.0, 30.0), 11 | (30.0, 25.0), 12 | (25.0, 20.0), 13 | (20.0, 15.0), 14 | (15.0, 10.0), 15 | (10.0, 7.0), 16 | (7.0, 4.0), 17 | 4.0 18 | ] 19 | 20 | outer_diameters = [ 50.0, 15.0 ] 21 | 22 | # Limits on finger holes sizes 23 | min_hole_diameters = [ 2.0 ]*6 24 | max_hole_diameters = [ 15.0 ]*6 25 | 26 | # Limit how close successive holes can be to each other 27 | #min_hole_spacing = [ 5,5,5,20,20 ] 28 | # see also max_hole_spacing 29 | # balance 30 | 31 | # Very simple fingering system 32 | initial_length = demakein.design.wavelength('D4') * 0.4 33 | fingerings = [ 34 | ('D4', [1,1,1,1,1,1]), 35 | ('E4', [0,1,1,1,1,1]), 36 | ('F#4', [0,0,1,1,1,1]), 37 | ('G4', [0,0,0,1,1,1]), 38 | ('A4', [0,0,0,0,1,1]), 39 | ('B4', [0,0,0,0,0,1]), 40 | ('C#5', [0,0,0,0,0,0]), 41 | ('D5', [1,1,1,1,1,1]), 42 | ('E5', [0,1,1,1,1,1]), 43 | ('F#5', [0,0,1,1,1,1]), 44 | ('G5', [0,0,0,1,1,1]), 45 | ('A5', [0,0,0,0,1,1]), 46 | ('B5', [0,0,0,0,0,1]), 47 | ('C#6', [0,0,0,0,0,0]), 48 | ] 49 | 50 | if __name__ == '__main__': 51 | demakein.run_tool(Design_stepped_shawm) 52 | 53 | # or 54 | #nesoni.run_toolbox([ 55 | # Design_simple_shawm, 56 | # #... 57 | # ]) -------------------------------------------------------------------------------- /examples/drinking_straw.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Note: 4 | 5 | The "reed" has some virtual length. 6 | 7 | Drinking straw needs to be 0.85 of simulated length. 8 | 9 | """ 10 | 11 | import copy 12 | 13 | import demakein 14 | 15 | class Reedpipe(demakein.design.Instrument_designer): 16 | def patch_instrument(self, inst): 17 | inst = copy.copy(inst) 18 | inst.length /= 0.85 19 | return inst 20 | 21 | 22 | class Diatonic(Reedpipe): 23 | closed_top = True 24 | 25 | inner_diameters = [ 6.0, 6.0 ] 26 | outer_diameters = [ 6.0, 6.0 ] 27 | 28 | min_hole_diameters = [ 4.0 ]*7 29 | max_hole_diameters = [ 4.0 ]*7 30 | 31 | initial_length = demakein.design.wavelength('C4') * 0.25 * 0.85 32 | fingerings = [ 33 | ('C4', [1,1,1,1,1,1,1]), 34 | ('D4', [0,1,1,1,1,1,1]), 35 | ('E4', [0,0,1,1,1,1,1]), 36 | ('F4', [0,0,0,1,1,1,1]), 37 | ('G4', [0,0,0,0,1,1,1]), 38 | ('A4', [0,0,0,0,0,1,1]), 39 | ('B4', [0,0,0,0,0,0,1]), 40 | ('C5', [0,0,0,0,0,1,0]), 41 | ] 42 | 43 | 44 | class Pentatonic(Reedpipe): 45 | closed_top = True 46 | 47 | inner_diameters = [ 6.0, 6.0 ] 48 | outer_diameters = [ 6.0, 6.0 ] 49 | 50 | min_hole_diameters = [ 4.0 ]*5 51 | max_hole_diameters = [ 4.0 ]*5 52 | 53 | initial_length = demakein.design.wavelength('C4') * 0.25 * 0.85 54 | fingerings = [ 55 | ('C4', [1,1,1,1,1]), 56 | ('D4', [0,1,1,1,1]), 57 | ('E4', [0,0,1,1,1]), 58 | ('G4', [0,0,0,1,1]), 59 | ('A4', [0,0,0,0,1]), 60 | ('C5', [0,0,0,0,0]), 61 | ] 62 | 63 | 64 | if __name__ == '__main__': 65 | demakein.run_toolbox([ Diatonic, Pentatonic ]) 66 | 67 | -------------------------------------------------------------------------------- /examples/pentatonic_flute.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This example demonstrates subclassing of Design_flute. 4 | 5 | The resultant design can be used with Make_flute. 6 | 7 | Note that Make_flute must be executed using this script, 8 | so that it can unpickle the flute design. 9 | 10 | """ 11 | 12 | import demakein 13 | 14 | from demakein import design, design_flute 15 | 16 | class Design_pentatonic_flute(design_flute.Tapered_flute): 17 | # I like to give the designs as tenor-recorder sized 18 | # We'll default to actually making a smaller instrument 19 | transpose = 12 20 | 21 | 22 | n_holes = 5 # including embouchure hole 23 | 24 | # The fingerings need to include the open embouchure hole. 25 | fingerings = [ 26 | ('D4', [1,1,1,1, 0]), 27 | ('E4', [0,1,1,1, 0]), 28 | ('G4', [0,0,1,1, 0]), 29 | ('A4', [0,0,0,1, 0]), 30 | ('B4', [0,0,0,0, 0]), 31 | ('D5', [1,1,1,1, 0]), 32 | ('E5', [0,1,1,1, 0]), 33 | ('G5', [0,0,1,1, 0]), 34 | ('A5', [0,0,0,1, 0]), 35 | ('B5', [0,0,0,0, 0]), 36 | ] 37 | 38 | initial_length = design.wavelength('D4') * 0.5 39 | 40 | # Holes 1, 2 and 3 should be fairly evenly spaced 41 | balance = [ None, 0.1, None ] 42 | #Spacing of holes 43 | # 0,1,2 1,2,3 2,3,4 44 | 45 | hole_horiz_angles = [ 0.0, 5.0, 0.0, 0.0, 0.0 ] 46 | 47 | # Ways in which the flute may be jointed 48 | divisions = [ 49 | # A single joint above hole 3 50 | [ (3, 0.0) ], 51 | 52 | # A joint halfway between hole 0 and hole 1, 53 | # and a joint above hole 3 54 | [ (0, 0.5), (3, 0.0) ], 55 | ] 56 | 57 | 58 | if __name__ == '__main__': 59 | demakein.run_toolbox( 60 | [ Design_pentatonic_flute, demakein.Make_flute ], 61 | show_make_flags=False) 62 | -------------------------------------------------------------------------------- /backyard/math.txt: -------------------------------------------------------------------------------- 1 | 2 | Junction 3 | 4 | n tubes, 0,1,..n-1 5 | 6 | Pressure 7 | 8 | pin[i] of form x exp(im*d*t) where d = 2*pi/frequency 9 | pout[i] of form x exp(-im**dt) 10 | 11 | im = sqrt(-1) 12 | 13 | For simplicity 14 | pin0 = 1 15 | 16 | Area 17 | 18 | a[i] 19 | 20 | For simplicity 21 | a0 = 1 22 | 23 | Replies 24 | 25 | pin[i] = r[i] * pout[i] 26 | 27 | i in 1..n-1 28 | 29 | Velocity 30 | 31 | vin[i] = im*d*pin[i] 32 | vout[i] = -im*d*pout[i] 33 | 34 | Junction pressure 35 | 36 | For all i 37 | pjunc = pin[i] + pout[i] 38 | 39 | Air is not created or destroyed 40 | 41 | 0 = sum a[i]*(vin[i]+vout[i]) 42 | 43 | 44 | n=2 case 45 | 46 | 0 = pin0-pout0+a1*(pin1-pout1) 47 | pin0 = 1 48 | pjunc = pin0+pout0 = pin1+pout1 49 | pin1 = r1 * pout1 50 | 51 | 52 | 53 | 0 = 1-pout0+a1*pin1-a1*pout1 54 | 55 | 0 = 1-pout0+a1*r1*pout1-a1*pout1 56 | 57 | pjunc = 1+pout0 58 | pjunc = r1*pout1+pout1 59 | pout0 = pjunc-1 = r1*pout1+pout1-1 60 | 61 | 0 = 1-r1*pout1-pout1+1+a1*r1*pout1-a1*pout1 62 | 0 = 2 + (-r1-1+a1*r1-a1)*pout1 63 | pout1 = -2 / (-r1-1+a1*r1-a1) 64 | pout1 = 2/(r1+1-a1*r1+a1) 65 | 66 | This is a bit roundabout, see below. 67 | 68 | 69 | n=3 case 70 | 71 | 0 = pin0-pout0+a1*(pin1-pout1)+a2*(pin2-pout2) 72 | pjunc = pin0+pout0 = pin1+pout1 = pin2+pout2 73 | pin0 = 1 74 | pin1 = r1 * pout1 75 | pin2 = r2 * pout2 76 | 77 | 0 = 1-pout0+a1*(r1-1)*pout1+a2*(r2-1)*pout2 78 | pjunc = 1+pout0 = (r1+1)*pout1 = (r2+1)*pout2 79 | 80 | We can put everthing in terms of pjunc. 81 | 82 | pout0 = pjunc-1 83 | pout1 = pjunc/(r1+1) 84 | pout2 = pjunc/(r2+1) 85 | 0 = 2-pjunc+a1*(r1-1)/(r1+1)*pjunc+a2*(r2-1)/(r2+1)*pjunc 86 | pjunc*(1-a1*(r1-1)/(r1+1)-a2*(r2-1)/(r2+1)) = 2 87 | pjunc = 2 / (1-a1*(r1-1)/(r1+1)-a2*(r2-1)/(r2+1)) 88 | 89 | It follows quite a simple pattern. 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /demakein/raphs_curves/polymat-bad.py: -------------------------------------------------------------------------------- 1 | from Numeric import * 2 | import LinearAlgebra as la 3 | import sys 4 | 5 | n = 15 6 | m = zeros(((n + 1) * 4, (n + 1) * 4), Float) 7 | for i in range(n): 8 | m[4 * i + 2][4 * i + 0] = .5 9 | m[4 * i + 2][4 * i + 1] = -1./12 10 | m[4 * i + 2][4 * i + 2] = 1./48 11 | m[4 * i + 2][4 * i + 3] = -1./480 12 | m[4 * i + 2][4 * i + 4] = .5 13 | m[4 * i + 2][4 * i + 5] = 1./12 14 | m[4 * i + 2][4 * i + 6] = 1./48 15 | m[4 * i + 2][4 * i + 7] = 1./480 16 | 17 | m[4 * i + 3][4 * i + 0] = 1 18 | m[4 * i + 3][4 * i + 1] = .5 19 | m[4 * i + 3][4 * i + 2] = .125 20 | m[4 * i + 3][4 * i + 3] = 1./48 21 | m[4 * i + 3][4 * i + 4] = -1 22 | m[4 * i + 3][4 * i + 5] = .5 23 | m[4 * i + 3][4 * i + 6] = -.125 24 | m[4 * i + 3][4 * i + 7] = 1./48 25 | 26 | m[4 * i + 4][4 * i + 0] = 0 27 | m[4 * i + 4][4 * i + 1] = 1 28 | m[4 * i + 4][4 * i + 2] = .5 29 | m[4 * i + 4][4 * i + 3] = .125 30 | m[4 * i + 4][4 * i + 4] = 0 31 | m[4 * i + 4][4 * i + 5] = -1 32 | m[4 * i + 4][4 * i + 6] = .5 33 | m[4 * i + 4][4 * i + 7] = -.125 34 | 35 | m[4 * i + 5][4 * i + 0] = 0 36 | m[4 * i + 5][4 * i + 1] = 0 37 | m[4 * i + 5][4 * i + 2] = 1 38 | m[4 * i + 5][4 * i + 3] = .5 39 | m[4 * i + 5][4 * i + 4] = 0 40 | m[4 * i + 5][4 * i + 5] = 0 41 | m[4 * i + 5][4 * i + 6] = -1 42 | m[4 * i + 5][4 * i + 7] = .5 43 | 44 | m[n * 4 + 2][2] = 1 45 | m[n * 4 + 3][3] = 1 46 | 47 | m[0][n * 4 + 2] = 1 48 | m[1][n * 4 + 3] = 1 49 | 50 | def printarr(m): 51 | for j in range(n * 4 + 4): 52 | for i in range(n * 4 + 4): 53 | print('%6.1f' % m[j][i], end=' ') 54 | print('') 55 | 56 | sys.output_line_width = 160 57 | #print array2string(m, precision = 3) 58 | mi = la.inverse(m) 59 | #printarr(mi) 60 | print('') 61 | for j in range(n + 1): 62 | for k in range(4): 63 | print('%7.2f' % mi[j * 4 + k][(n / 2) * 4 + 2], end=' ') 64 | print('') 65 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2 | 1.1 - 2025-07-27 3 | Update to require Python version 3. 4 | Build system is now "hatch". 5 | Use trimesh package for 3D models, with manifold3d as underlying boolean ops engine (rather than CGAL). 6 | Add testing shell-script. 7 | Optimizer now runs in a single process by default as multi-processing didn't seem to help. 8 | Small change to Six_hole_whistle_designer, prevent inner bore bulge impacting whistle. 9 | Small change to Make_whistle, cut a tiny bit below the head to avoid boolean ops problems. 10 | 11 | 0.18 - 2024-04-27 12 | Add long description. 13 | 14 | 0.17 - 2024-04-27 15 | Changed kernel in cgal.py to `CGAL::Simple_cartesian` 16 | to get rid of compilation errors. 17 | 18 | 0.16 - Design svg now shows where the sound comes out. 19 | (Mostly not from the end of the instrument, 20 | flared ends are largely decorative!) 21 | Added --tweak-emission flag, but it doesn't work. 22 | Add pentatonic_flute.py example. 23 | 24 | 0.15 - Refinements to three hole whistle. 25 | 26 | 0.14 - Fix optimizer bug -- could halt early. 27 | Refinements to folk whistle. 28 | 29 | 0.13 - Refinements to flutes. 30 | 31 | 0.12 - Added more example scripts. 32 | Allow stepped bores. 33 | 34 | 0.11 - Taper amount is now a parameter for flutes. 35 | 36 | 0.10 - OS X support, hopefully. 37 | 38 | 0.9 - Improved recorder design, and improved tuning of whistles. 39 | Added "tune:" tool. 40 | 41 | 0.8 - Refined three-hole pipe. 42 | Added some actual documentation to README. 43 | 44 | 0.7 - Added recorder and three-hole pipe. 45 | Added weld and tapered joins. 46 | 47 | 0.6 - Added some example scripts 48 | 49 | 0.5 - Folk whistle design somewhat refined 50 | 51 | 0.4 - Design tools no longer depend on cffi 52 | 53 | 0.3 - Sketch uses less memory 54 | Add pennywhistles 55 | 56 | 0.2 - Add --embextra parameter to flutes 57 | Shawms 58 | Improved hole geometry 59 | 60 | 0.1 - Initial release 61 | -------------------------------------------------------------------------------- /backyard/whistle_tuning.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | pypy demakein-script design-folk-whistle: --transpose 12 /tmp/fwhistle --tweak-gapextra 0.0 4 | 5 | """ 6 | 7 | import sys, copy, os 8 | 9 | sys.path.insert(0, os.path.normpath(os.path.join(__file__,'..','..'))) 10 | 11 | from demakein import design, design_whistle 12 | 13 | notes = [ 'D5', 'E5', 'F#5', 'G5', 'A5', 'B5', 'C6', 'C#6', 'D6', 'E6', 'F#6', 'G6', 'A6', 'B6'] 14 | 15 | #Small hole 16 | #obtained = [ 555.0, 623.0, 694.0, 734.0, 824.0, 930.0, 995.0, 1045.0, 1136.0, 1275.0, 1433.0, 1511.0, 1696.0, 1899.0] 17 | 18 | obtained = [ 594.0, 682.0, 774.0, 821.5, 928.0, 1045.0, 1109.0, 1169.0, 1224.0, 1366.0, 1533.0, 1632.0, 1830.0, 2060.0 ] 19 | 20 | designer = design.load('/tmp/bigwhistle') #Whistle with embextra 0 21 | 22 | designer.tweak_gapextra = 0.0 23 | base = designer.patch_instrument(designer.instrument) 24 | designer.tweak_gapextra = 0.00 #0.37 25 | #designer.tweak_gapheight = 26 | mod = designer.patch_instrument(designer.instrument) 27 | 28 | base.prepare() 29 | mod.prepare() 30 | 31 | #unpatched = designer.instrument 32 | #patched = designer.patch_instrument(unpatched) 33 | #unpatched.prepare() 34 | #patched.prepare() 35 | # 36 | #edesigner = design.load('/tmp/fwhistle2') #Whistle with candidate embextra 37 | #eunpatched = edesigner.instrument 38 | #epatched = edesigner.patch_instrument(eunpatched) 39 | #eunpatched.prepare() 40 | #epatched.prepare() 41 | 42 | e = 0.0 43 | for note, obtained in zip(notes, obtained): 44 | for note2, fingers in designer.fingerings: 45 | if abs(design.wavelength(note2,designer.transpose) - design.wavelength(note)) < 1e-3: break 46 | else: 47 | assert False, note2 48 | print(fingers, end=' ') 49 | w = design.wavelength(note) 50 | w_real = design.SPEED_OF_SOUND / obtained 51 | w_base = base.true_wavelength_near(w, fingers, designer.max_grad)[0] 52 | w_mod = mod.true_wavelength_near(w, fingers, designer.max_grad)[0] 53 | #print w, w_real, unmod, mod 54 | print(w_real, w_mod, w_mod/w_real) #w, w_base, w_mod, w_real/w_base, w_base/w_mod, (w_real/w_base) / (w_mod/w_base) 55 | e += abs(1.0-w_mod/w_real) 56 | print(e) 57 | 58 | -------------------------------------------------------------------------------- /demakein/raphs_curves/clothoid.py: -------------------------------------------------------------------------------- 1 | from math import * 2 | from . import cornu 3 | 4 | def mod_2pi(th): 5 | u = th / (2 * pi) 6 | return 2 * pi * (u - floor(u + 0.5)) 7 | 8 | # Given clothoid k(s) = k0 + k1 s, compute th1 - th0 of chord from s = -.5 9 | # to .5. 10 | def compute_dth(k0, k1): 11 | if k1 < 0: 12 | return -compute_dth(k0, -k1) 13 | elif k1 == 0: 14 | return 0 15 | sqrk1 = sqrt(2 * k1) 16 | t0 = (k0 - .5 * k1) / sqrk1 17 | t1 = (k0 + .5 * k1) / sqrk1 18 | (y0, x0) = cornu.eval_cornu(t0) 19 | (y1, x1) = cornu.eval_cornu(t1) 20 | chord_th = atan2(y1 - y0, x1 - x0) 21 | return mod_2pi(t1 * t1 - chord_th) - mod_2pi(chord_th - t0 * t0) 22 | 23 | def compute_chord(k0, k1): 24 | if k1 == 0: 25 | if k0 == 0: 26 | return 1 27 | else: 28 | return sin(k0 * .5) / (k0 * .5) 29 | sqrk1 = sqrt(2 * abs(k1)) 30 | t0 = (k0 - .5 * k1) / sqrk1 31 | t1 = (k0 + .5 * k1) / sqrk1 32 | (y0, x0) = cornu.eval_cornu(t0) 33 | (y1, x1) = cornu.eval_cornu(t1) 34 | return hypot(y1 - y0, x1 - x0) / abs(t1 - t0) 35 | 36 | # Given th0 and th1 at endpoints (measured from chord), return k0 37 | # and k1 such that the clothoid k(s) = k0 + k1 s, evaluated from 38 | # s = -.5 to .5, has the tangents given 39 | def solve_clothoid(th0, th1, verbose = False): 40 | k0 = th0 + th1 41 | 42 | # initial guess 43 | k1 = 6 * (th1 - th0) 44 | error = (th1 - th0) - compute_dth(k0, k1) 45 | if verbose: 46 | print((k0, k1, error)) 47 | 48 | k1_old, error_old = k1, error 49 | # second guess based on d(dth)/dk1 ~ 1/6 50 | k1 += 6 * error 51 | error = (th1 - th0) - compute_dth(k0, k1) 52 | if verbose: 53 | print((k0, k1, error)) 54 | 55 | # secant method 56 | for i in range(10): 57 | if abs(error) < 1e-9: break 58 | k1_old, error_old, k1 = k1, error, k1 + (k1_old - k1) * error / (error - error_old) 59 | error = (th1 - th0) - compute_dth(k0, k1) 60 | if verbose: 61 | print((k0, k1, error)) 62 | 63 | return k0, k1 64 | 65 | if __name__ == '__main__': 66 | print((solve_clothoid(.06, .05, True))) 67 | -------------------------------------------------------------------------------- /backyard/math.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | def junction_reply(area, areas, replies): 5 | total_area = area + sum(areas) 6 | 7 | return area / (total_area*(0.5 - sum( a/(total_area*(1.0/r+1.0)) for a,r in zip(areas, replies) ))) - 1.0 8 | 9 | #Equivalently? 10 | #x = area / (total_area*(0.5 - sum( a/(total_area*(r+1.0)) for a,r in zip(areas, replies) ))) - 1.0 11 | #return 1/x 12 | 13 | 14 | def junction2_reply(area, area1, reply1): 15 | total_area = area + area1 16 | 17 | return area / (total_area*(0.5 - area1/(total_area*(1.0/reply1+1.0)))) - 1.0 18 | 19 | 20 | def junction3_reply(area, area1, area2, reply1, reply2): 21 | total_area = area + area1 + area2 22 | 23 | return area / (total_area*( 24 | 0.5 25 | - area1/(total_area*(1.0/reply1+1.0)) 26 | - area2/(total_area*(1.0/reply2+1.0)) 27 | )) - 1.0 28 | 29 | 30 | 31 | 32 | def new2(a0, a1, r1): 33 | #pjunc = 2.0*(reply1+1.0)/(reply1+1.0-area1*reply1) 34 | 35 | #pout1 = 2.0 / (r1+1.0-a1*r1-a1) 36 | #pout1 = -2.0 / (-r1-1.0+a1*r1-a1) 37 | #pout1 = 2.0/(r1+1.0-a1*r1+a1) 38 | #pjunc = pout1 + r1*pout1 39 | 40 | pjunc = 2.0*a0 / (a0-a1*((r1-1.0)/(r1+1.0))) 41 | 42 | pout1 = pjunc / (r1+1.0) 43 | 44 | pin1 = r1 * pout1 45 | pin0 = 1.0 46 | pout0 = pjunc - pin0 47 | print() 48 | print(pout1 + pin1) 49 | print(pjunc) 50 | print() 51 | print(pin0-pout0+a1*(pin1-pout1)) 52 | print() 53 | print(abs(pout1), abs(pout1/r1)) 54 | return pjunc - 1.0 55 | 56 | 57 | def new3(a0, a1, a2, r1, r2): 58 | pjunc = 2.0*a0 / (a0-a1*((r1-1.0)/(r1+1.0))-a2*((r2-1.0)/(r2+1.0))) 59 | 60 | pout1 = pjunc / (r1+1.0) 61 | pout2 = pjunc / (r2+1.0) 62 | print(abs(pjunc)) 63 | print(abs(pout1)) 64 | print(abs(pout2)) 65 | 66 | return pjunc - 1.0 67 | 68 | 69 | area = 1.0 70 | area1 = 5.0 71 | reply1 = -1j 72 | area2 = 7.0 73 | reply2 = 1j 74 | 75 | print(junction2_reply(area, area1, reply1)) 76 | print(new2(area, area1, reply1)) 77 | print('...') 78 | 79 | print(junction3_reply(area,area1,area2, reply1,reply2)) 80 | print(new3(area, area1, area2, reply1, reply2)) 81 | 82 | 83 | -------------------------------------------------------------------------------- /demakein/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '1.1' 2 | 3 | from .design_flute import Design_pflute, Design_folk_flute 4 | from .make_flute import Make_flute 5 | from .make_cork import Make_cork 6 | 7 | from .design_shawm import Design_reed_drone, Design_reedpipe, Design_shawm, Design_folk_shawm 8 | from .make_shawm import Make_reed_instrument, Make_dock_extender 9 | from .make_mouthpiece import Make_mouthpiece 10 | from .make_bauble import Make_bauble 11 | from .make_windcap import Make_windcap 12 | 13 | from .design_whistle import \ 14 | Design_folk_whistle, Design_dorian_whistle, Design_recorder, Design_three_hole_whistle 15 | from .make_whistle import Make_whistle 16 | 17 | from .make_panpipe import Make_panpipe 18 | 19 | from .make_reed import Make_reed, Make_reed_shaper 20 | 21 | from .tune import Tune 22 | 23 | from .all import All 24 | 25 | from .legion import run_tool, run_toolbox 26 | 27 | def main(): 28 | """ Command line interface. """ 29 | from . import legion 30 | legion.run_toolbox([ 31 | 'Demakein '+VERSION, 32 | 'Flutes', 33 | Design_pflute, 34 | Design_folk_flute, 35 | Make_flute, 36 | Make_cork, 37 | 38 | 'Whistles', 39 | Design_folk_whistle, 40 | Design_dorian_whistle, 41 | Design_recorder, 42 | Design_three_hole_whistle, 43 | Make_whistle, 44 | 45 | 'Reed instruments', 46 | Design_reed_drone, 47 | #Design_reedpipe, 48 | Design_shawm, 49 | Design_folk_shawm, 50 | Make_reed_instrument, 51 | Make_dock_extender, 52 | Make_mouthpiece, 53 | Make_windcap, 54 | Make_bauble, 55 | #Make_reed, 56 | #Make_reed_shaper, 57 | 58 | 'Panpipes', 59 | Make_panpipe, 60 | 61 | 'Utilities', 62 | Tune, 63 | 64 | 'Everything', 65 | All, 66 | #'"demakein all:" uses the nesoni make system, see flags below.', 67 | ], 68 | show_make_flags=False, 69 | ) 70 | 71 | -------------------------------------------------------------------------------- /backyard/one_hole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S uv run --script 2 | # /// script 3 | # requires-python = ">=3.8" 4 | # dependencies = [ 5 | # "polars", 6 | # "altair[all]", 7 | # "demakein" 8 | # ] 9 | # 10 | # [tool.uv.sources] 11 | # demakein = { path = ".." } 12 | # 13 | # /// 14 | 15 | """ 16 | Some parameter scanning with a simple instrument. 17 | """ 18 | 19 | 20 | import math, copy 21 | 22 | import polars as pl 23 | import altair as alt 24 | 25 | from demakein import design, profile 26 | 27 | inst = design.Instrument() 28 | inst.length = design.wavelength("C4") 29 | inst.closed_top = False 30 | inst.inner = profile.Profile([0,inst.length], [6,6]) 31 | inst.outer = profile.Profile([0,inst.length], [10,10]) 32 | 33 | inst.inner_hole_positions = [ inst.length * 2/3 ] 34 | inst.hole_lengths = [ 0.0 ] 35 | inst.hole_diameters = [ 0.1 ] 36 | 37 | inst.cone_step = 0.125 38 | 39 | inst.prepare_phase() 40 | 41 | def interpret(w): 42 | desc = design.describe(w) 43 | error = design.wavelength(desc) / w 44 | cents = math.log(error) / math.log(2) * 1200.0 45 | return f"{desc} {cents} {w}" 46 | 47 | print( interpret( inst.length ) ) 48 | print( interpret( inst.true_nth_wavelength_near(inst.length * 0.5, [ 1 ], 2 ) ) ) 49 | print( interpret( inst.true_nth_wavelength_near(inst.length * 0.5, [ 0 ], 2 ) ) ) 50 | print( interpret( inst.true_nth_wavelength_near(inst.length * 0.5, [ 1 ], 3 ) ) ) 51 | print( interpret( inst.true_nth_wavelength_near(inst.length * 0.5, [ 0 ], 3 ) ) ) 52 | 53 | 54 | df = [ {"nth":nth, "finger":finger, "diam":diam} 55 | for diam in [0.1,0.5,1,2,3,4,5,6,12] 56 | for nth in [2,3,4,5,6] 57 | for finger in [0,1] ] 58 | 59 | for row in df: 60 | inst2 = copy.copy(inst) 61 | inst2.hole_diameters = [ row["diam"] ] 62 | inst2.prepare_phase() 63 | row["w"] = inst2.true_nth_wavelength_near(inst2.length*0.5, [row["finger"]], row["nth"]) 64 | row["desc"] = interpret(row["w"]) 65 | 66 | cols = {k: [d[k] for d in df] for k in df[0]} 67 | 68 | chart = alt.Chart(pl.DataFrame(cols)).mark_point().encode(x="diam",y="w",color="nth",column="finger") 69 | chart.save("output/chart.html") 70 | 71 | print(pl.DataFrame(cols)) 72 | 73 | 74 | # Even simpler would be to look at what a stepped bore does 75 | 76 | -------------------------------------------------------------------------------- /demakein/raphs_curves/band.py: -------------------------------------------------------------------------------- 1 | # A little solver for band-diagonal matrices. Based on NR Ch 2.4. 2 | 3 | from math import * 4 | 5 | from Numeric import * 6 | 7 | do_pivot = True 8 | 9 | def bandec(a, m1, m2): 10 | n, m = a.shape 11 | mm = m1 + m2 + 1 12 | if m != mm: 13 | raise ValueError('Array has width %d expected %d' % (m, mm)) 14 | al = zeros((n, m1), Float) 15 | indx = zeros(n, Int) 16 | 17 | for i in range(m1): 18 | l = m1 - i 19 | for j in range(l, mm): a[i, j - l] = a[i, j] 20 | for j in range(mm - l, mm): a[i, j] = 0 21 | 22 | d = 1. 23 | 24 | l = m1 25 | for k in range(n): 26 | dum = a[k, 0] 27 | pivot = k 28 | if l < n: l += 1 29 | if do_pivot: 30 | for j in range(k + 1, l): 31 | if abs(a[j, 0]) > abs(dum): 32 | dum = a[j, 0] 33 | pivot = j 34 | indx[k] = pivot 35 | if dum == 0.: a[k, 0] = 1e-20 36 | if pivot != k: 37 | d = -d 38 | for j in range(mm): 39 | tmp = a[k, j] 40 | a[k, j] = a[pivot, j] 41 | a[pivot, j] = tmp 42 | for i in range(k + 1, l): 43 | dum = a[i, 0] / a[k, 0] 44 | al[k, i - k - 1] = dum 45 | for j in range(1, mm): 46 | a[i, j - 1] = a[i, j] - dum * a[k, j] 47 | a[i, mm - 1] = 0. 48 | return al, indx, d 49 | 50 | def banbks(a, m1, m2, al, indx, b): 51 | n, m = a.shape 52 | mm = m1 + m2 + 1 53 | l = m1 54 | for k in range(n): 55 | i = indx[k] 56 | if i != k: 57 | tmp = b[k] 58 | b[k] = b[i] 59 | b[i] = tmp 60 | if l < n: l += 1 61 | for i in range(k + 1, l): 62 | b[i] -= al[k, i - k - 1] * b[k] 63 | l = 1 64 | for i in range(n - 1, -1, -1): 65 | dum = b[i] 66 | for k in range(1, l): 67 | dum -= a[i, k] * b[k + i] 68 | b[i] = dum / a[i, 0] 69 | if l < mm: l += 1 70 | 71 | if __name__ == '__main__': 72 | a = zeros((10, 3), Float) 73 | for i in range(10): 74 | a[i, 0] = 1 75 | a[i, 1] = 2 76 | a[i, 2] = 1 77 | print(a) 78 | al, indx, d = bandec(a, 1, 1) 79 | print(a) 80 | print(al) 81 | print(indx) 82 | b = zeros(10, Float) 83 | b[5] = 1 84 | banbks(a, 1, 1, al, indx, b) 85 | print(b) 86 | -------------------------------------------------------------------------------- /demakein/make_windcap.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from . import config, make, shape, profile 4 | 5 | 6 | @config.help('Make a windcap for a reed instrument.') 7 | @config.Float_flag('dock_diameter', 8 | 'Diameter windcap should fit on top of.' 9 | ) 10 | @config.Float_flag('dock_length', 11 | 'Length of socket.' 12 | ) 13 | class Make_windcap(make.Make): 14 | dock_diameter = 10.0 15 | dock_length = 15.0 16 | 17 | def run(self): 18 | wall = 2.0 19 | 20 | scale = self.dock_diameter 21 | 22 | z_dock_low = 0.0 23 | z_dock_high = self.dock_length 24 | z_top_low = z_dock_high + scale * 2.5 25 | z_top_high = z_top_low + wall 26 | z_pinch = z_top_low - scale*0.5 27 | z_mouth_high = z_top_high + scale * 0.75 28 | 29 | d_dock = self.dock_diameter 30 | d_inner = d_dock - wall 31 | d_outer = d_dock + wall 32 | d_dock_outer = d_outer + wall 33 | d_mouth_inner = wall 34 | d_mouth_outer = wall + wall*1.5 35 | 36 | outer_profile = profile.make_profile([ 37 | (z_dock_low, d_dock_outer), 38 | (z_top_high-wall*0.5, d_outer), 39 | (z_top_high, d_outer-wall, d_mouth_outer+wall), 40 | (z_top_high+wall*0.5, d_mouth_outer), 41 | (z_mouth_high, d_mouth_outer), 42 | ]) 43 | 44 | inner_profile = profile.make_profile([ 45 | (z_dock_low, d_dock), 46 | (z_dock_high, d_dock, d_inner), 47 | (z_pinch, d_inner), 48 | (z_top_low, d_mouth_inner), 49 | (z_mouth_high, d_mouth_inner), 50 | ]) 51 | 52 | stretch = (d_outer-d_mouth_outer)*0.5 53 | inner_stretch = profile.make_profile([ 54 | (z_pinch, 0.0), 55 | (z_top_low, stretch) 56 | ]) 57 | outer_stretch = profile.make_profile([ 58 | (z_top_high, 0.0, stretch) 59 | ]) 60 | 61 | cross_section = lambda d,s: shape.rounded_rectangle(-(d+s)*0.5,(d+s)*0.5,-d*0.5,d*0.5,d) 62 | 63 | outside = shape.extrude_profile(outer_profile, outer_stretch, cross_section=cross_section) 64 | inside = shape.extrude_profile(inner_profile, inner_stretch, cross_section=cross_section) 65 | 66 | thing = outside.copy() 67 | thing.remove(inside) 68 | self.save(thing, 'windcap') 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /demakein/make_bauble.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | from . import config, make, shape, profile 5 | 6 | 7 | def wobble(diameter=1.0, wobble=0.5, spin=0.0, period=16, n=256): 8 | radius = diameter * 0.5 9 | result = [ ] 10 | for i in range(n): 11 | a = (i+0.5)*math.pi*2.0/n 12 | s = spin*math.pi / 180.0 13 | b = math.cos((a+s)*period) 14 | c = math.cos((a-s*0.5)*period)*0.75 15 | d = 1-(1-b)*(1-c) 16 | r2 = radius*(1.0 + wobble*( d -0.5) ) 17 | result.append( (math.cos(a)*r2, math.sin(a)*r2) ) 18 | return shape.Loop(result) 19 | 20 | 21 | @config.help( 22 | 'Make a gratuitous bauble.', 23 | """\ 24 | This tool is used when you use "make-shawm: --bauble yes". 25 | 26 | Make your own gratuitous baubles to fit the cylindrical object of your choice. 27 | """ 28 | ) 29 | @config.Float_flag('dock_diameter','Diameter of cylinder to fit.') 30 | @config.Float_flag('dock_length','Length of cylinder to fit.') 31 | class Make_bauble(make.Make): 32 | dock_diameter = 40.0 33 | dock_length = 5.0 34 | 35 | def run(self): 36 | length = self.dock_diameter * 1.5 37 | inlength = length * 0.9 38 | pos_outer = [ 0.0, length ] 39 | diam_outer = [ self.dock_diameter+2.0, 0.0 ] 40 | angle = [ 20.0, -10.0 ] 41 | p_outer = profile.curved_profile( 42 | pos_outer, diam_outer, diam_outer, angle, angle) 43 | 44 | pos_inner = [ 0.0, inlength ] 45 | diam_inner = [ self.dock_diameter, 0.0 ] 46 | p_inner = profile.curved_profile( 47 | pos_inner, diam_inner, diam_inner, angle, angle) 48 | 49 | spin = profile.make_profile([(0.0,0.0),(length, 120.0)]) 50 | 51 | wob = profile.make_profile([(self.dock_length*0.5,0.0),(self.dock_length,1.0),(length,0.0)]) 52 | 53 | bauble = shape.extrude_profile(p_outer,spin,wob, cross_section=lambda d,s,w: wobble(d,w*0.1,s,12)) 54 | inside = shape.extrude_profile( 55 | p_inner.clipped(self.dock_length+1.0,inlength),spin.clipped(self.dock_length+1.0,inlength),wob.clipped(self.dock_length+1.0,inlength), 56 | cross_section=lambda d,s,w: wobble(d,w*-0.1,s,12)) 57 | bauble.remove(inside) 58 | 59 | dock_profile = profile.make_profile([(0.0,self.dock_diameter),(self.dock_length,self.dock_diameter),(self.dock_length+self.dock_diameter*0.5,0.0)]) 60 | dock = shape.extrude_profile(dock_profile) 61 | #bauble.add(shape.extrude_profile(p_outer.clipped(0.0,self.dock_length+1.0))) 62 | bauble.remove(dock) 63 | 64 | self.save(bauble, 'bauble') 65 | return bauble 66 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Test demakein on the command line 4 | 5 | ## To clean-up first: 6 | # 7 | # rm -rf output 8 | # 9 | 10 | set -xeu 11 | 12 | mkdir -p output 13 | 14 | demakein make-cork: output/cork 15 | 16 | DEMAKEIN_DRAFT=1 demakein make-cork: output/cork-draft 17 | 18 | demakein make-panpipe: output/panpipes 19 | 20 | demakein make-bauble: output/bauble 21 | 22 | 23 | # Test example scripts 24 | 25 | 26 | python examples/simple_flute.py output/simple-flute 27 | 28 | python examples/simple_reedpipe.py output/simple-reedpipe 29 | 30 | python examples/simple_shawm.py output/simple-shawm 31 | 32 | python examples/stepped_shawm.py output/stepped-shawm 33 | 34 | python examples/drinking_straw.py pentatonic: output/drinking-straw-pentatonic 35 | python examples/drinking_straw.py diatonic: output/drinking-straw-diatatonic 36 | 37 | python examples/mywhistle.py design-my-whistle: output/mywhistle 38 | python examples/mywhistle.py make-whistle: output/mywhistle 39 | 40 | python examples/pentatonic_flute.py design-pentatonic-flute: output/pentatonic-flute 41 | python examples/pentatonic_flute.py make-flute: output/pentatonic-flute 42 | 43 | 44 | # Test tools 45 | 46 | demakein design-folk-flute: output/folk-flute 47 | demakein make-flute: output/folk-flute 48 | demakein make-flute: output/folk-flute --mill yes 49 | 50 | demakein design-pflute: output/pflute 51 | demakein make-flute: output/pflute 52 | demakein make-flute: output/pflute --mill yes 53 | 54 | demakein design-folk-whistle: output/folk-whistle 55 | demakein make-whistle: output/folk-whistle 56 | demakein make-whistle: output/folk-whistle --mill yes 57 | 58 | demakein design-dorian-whistle: output/dorian-whistle 59 | demakein make-whistle: output/dorian-whistle 60 | demakein make-whistle: output/dorian-whistle --mill yes 61 | 62 | demakein design-recorder: output/recorder 63 | demakein make-whistle: output/recorder 64 | demakein make-whistle: output/recorder --mill yes 65 | 66 | demakein design-three-hole-whistle: output/three-hole-whistle 67 | demakein make-whistle: output/three-hole-whistle 68 | demakein make-whistle: output/three-hole-whistle --mill yes 69 | 70 | 71 | # Currently broken 72 | #demakein design-reedpipe: output/reedpipe 73 | #demakein make-reed-instrument: output/reedpipe 74 | #demakein make-reed-instrument: output/reedpipe --mill yes 75 | 76 | demakein design-shawm: output/shawm 77 | demakein make-reed-instrument: output/shawm 78 | demakein make-reed-instrument: output/shawm --mill yes 79 | 80 | demakein design-folk-shawm: output/folk-shawm 81 | demakein make-reed-instrument: output/folk-shawm 82 | demakein make-reed-instrument: output/folk-shawm --mill yes 83 | 84 | demakein design-reed-drone: output/drone 85 | demakein make-reed-instrument: output/drone 86 | 87 | demakein make-dock-extender: output/dock-extender 88 | 89 | demakein make-mouthpiece: output/mouthpiece 90 | 91 | # Currently broken. Not sure what this was for. 92 | #demakein make-reed-shaper: output/reed-shaper 93 | 94 | 95 | # Omnibus collection 96 | 97 | demakein all: output/all 98 | -------------------------------------------------------------------------------- /demakein/make_reed.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | from . import config, design, make, shape, profile 5 | 6 | 7 | def grow(loop,thickness): 8 | extent = loop.extent() 9 | xsize = extent.xmax-extent.xmin 10 | ysize = extent.ymax-extent.ymin 11 | return loop.scale2( 12 | (xsize+thickness*2.0)/xsize, 13 | (ysize+thickness*2.0)/ysize 14 | ) 15 | 16 | 17 | @config.help( 18 | '[Experimental, not yet working] Make a printable reed.' 19 | ) 20 | class Make_reed(make.Make): 21 | def run(self): 22 | diameter = 8.0 23 | flare = 2.0 24 | length = 30.0 25 | 26 | stem_length = 5.0 27 | stem_diameter = diameter + 6.0 28 | 29 | thickness0 = 1.0 30 | thickness1 = 0.05 31 | 32 | bottom = shape.circle(diameter) 33 | top = shape.lens(0.99).with_circumpherence(math.pi*diameter*flare) 34 | 35 | stem = shape.circle(stem_diameter) 36 | 37 | reed = shape.extrusion( 38 | [0.0,stem_length,stem_length,stem_length+length], 39 | [stem,stem,grow(bottom,thickness0),grow(top,thickness1)] 40 | ) 41 | inside = shape.extrusion( 42 | [0.0,stem_length,stem_length+length], 43 | [bottom,bottom,top] 44 | ) 45 | 46 | reed.remove(inside) 47 | 48 | #wedge_thickness = 0.2 49 | #wedge_loop = shape.Loop([ 50 | # (length*0.5, 0.0), 51 | # (length*1.01, wedge_thickness*0.5), 52 | # (length*1.01, -wedge_thickness*0.5) 53 | # ]) 54 | #wedge = shape.extrusion([-diameter*4,diameter*4],[wedge_loop,wedge_loop]) 55 | #wedge.rotate(0,1,0,-90) 56 | #reed.remove(wedge) 57 | 58 | self.save(reed, 'reed') 59 | 60 | 61 | @config.help( 62 | '[Experimental] Make a reed-shaping doodad.' 63 | ) 64 | class Make_reed_shaper(make.Make): 65 | def run(self): 66 | diameter = 7.0 67 | length = 15.0 68 | 69 | thickness0 = 2.0 70 | thickness1 = 2.0 71 | 72 | outside = shape.empty_shape() 73 | inside = shape.empty_shape() 74 | 75 | for lens_amount in [ 0.85 ]: 76 | #bottom = shape.circle(diameter) 77 | top = shape.lens(lens_amount).with_circumpherence(math.pi*diameter) 78 | bottom = top 79 | 80 | outside.add(shape.extrusion( 81 | [0.0,length], 82 | [grow(bottom,thickness0),grow(top,thickness1)] 83 | )) 84 | inside.add(shape.extrusion( 85 | [0.0,length], 86 | [bottom,top] 87 | )) 88 | 89 | thing = outside.copy() 90 | thing.remove(inside) 91 | 92 | wedge_thickness = diameter*0.2 93 | wedge_loop = shape.Loop([ 94 | (length*0.333, 0.0), 95 | (length*1.01, wedge_thickness*0.5), 96 | (length*1.01, -wedge_thickness*0.5) 97 | ]) 98 | wedge = shape.extrusion([-diameter*4,diameter*4],[wedge_loop,wedge_loop]) 99 | wedge.rotate(0,1,0,-90) 100 | #wedge.rotate(0,0,1,90) 101 | thing.remove(wedge) 102 | 103 | self.save(thing, 'shaper') 104 | 105 | -------------------------------------------------------------------------------- /demakein/selection.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | 4 | from . import grace 5 | 6 | def term_specification(term): 7 | if '=' not in term: return term 8 | return term.split('=',1)[0] 9 | 10 | def term_name(term): 11 | if '=' not in term: return term 12 | return term.split('=',1)[1] 13 | 14 | 15 | def matches(expression, tags): 16 | tokens = list('[]-:/^') 17 | 18 | def parse2(expression): 19 | assert expression, 'unexpected end of expression' 20 | if expression[0] == '[': 21 | value, expression = parse(expression[1:]) 22 | assert expression.startswith(']'), 'expected a closing ]' 23 | return value, expression[1:] 24 | 25 | i = 0 26 | while i < len(expression) and expression[i] not in '[]:/^': 27 | i += 1 28 | assert i > 0, 'unexpected '+expression[0] 29 | return expression[:i] == 'all' or expression[:i] in tags, expression[i:] 30 | 31 | def parse1(expression): 32 | assert expression, 'unexpected end of expression' 33 | if expression.startswith('-'): 34 | value, expression = parse2(expression[1:]) 35 | return not value, expression 36 | else: 37 | value, expression = parse2(expression) 38 | return value, expression 39 | 40 | def parse(expression): 41 | value, expression = parse1(expression) 42 | while expression and expression[0] in ':/^': 43 | operator, expression = expression[0], expression[1:] 44 | value2, expression = parse1(expression) 45 | if operator == ':': 46 | value = value and value2 47 | elif operator == '/': 48 | value = value or value2 49 | else: 50 | value = (not value2 and value) or (not value and value2) 51 | return value, expression 52 | 53 | if expression == '': 54 | return False 55 | try: 56 | value, expression = parse(expression) 57 | assert not expression, 'don\'t know what to do with: '+expression 58 | except AssertionError as e: 59 | raise grace.Error('Could not parse: '+expression+', '+e.args[0]) 60 | return value 61 | 62 | 63 | def select_and_sort(select_expression, sort_expression, items, get_tags=lambda item: item.get_tags()): 64 | """ Select items based on select_expression then sort by sort_expression. 65 | If group=True, return a list of lists being the distinct groups created by the sort expression. 66 | Otherwise return a list. 67 | """ 68 | items = [ item for item in items 69 | if matches(select_expression, get_tags(item)) ] 70 | 71 | if not sort_expression: 72 | parts = [] 73 | else: 74 | parts = sort_expression.split(',') 75 | 76 | def key(item): 77 | tags = get_tags(item) 78 | return [ 0 if matches(part, tags) else 1 79 | for part in parts ] 80 | 81 | items.sort(key=key) 82 | 83 | return items 84 | 85 | 86 | def weight(expression, tags): 87 | parts = expression.split(',') 88 | total = 0.0 89 | for part in parts: 90 | weight = 1.0 91 | match = re.match('^{(.*)}(.*)$',part) 92 | if match: 93 | weight = float(match.group(1)) 94 | part = match.group(2) 95 | if matches(part, tags): 96 | total += weight 97 | return total 98 | 99 | 100 | class Matchable_set(set): 101 | def matches(self, expression): 102 | return matches(expression, self) 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /demakein/make_mouthpiece.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | from . import config, shape, profile, make 5 | 6 | 7 | """ 8 | 9 | A typical trumpet mouthpiece (Kelly 5C): 10 | 11 | cup 12 | 16mm inner diameter 13 | 26.5mm outer diameter 14 | depth of about 9mm 15 | 16 | 3.75mm entrance to bore 17 | flares over ~75mm to 8mm 18 | 19 | 20 | 21 | Cup diam 11 depth 4 bore 5 almost works with 4mm tenor shawm. 22 | 23 | """ 24 | 25 | 26 | def make_widget( 27 | tube_length, 28 | tube_diameter, 29 | cup_length, 30 | final_diameter, 31 | french=False, 32 | ): 33 | a = 0.0 34 | b = a + tube_length 35 | c = b + cup_length 36 | 37 | initial_outer_diameter = final_diameter 38 | final_outer_diameter = final_diameter + 14.0 39 | 40 | outer_profile = profile.make_profile([ 41 | (a, initial_outer_diameter), 42 | (c, final_outer_diameter), 43 | ]) 44 | 45 | inner_spec = [ 46 | (a, tube_diameter), 47 | ] 48 | n = 1 if french else 20 49 | for i in range(n): 50 | angle = i*math.pi/2/n 51 | inner_spec.append(( 52 | b+cup_length*(1-math.cos(angle)), 53 | tube_diameter+(final_diameter-tube_diameter)*math.sin(angle) 54 | )) 55 | inner_spec.append((c,final_diameter)) 56 | inner_profile = profile.make_profile(inner_spec).clipped(-10.0, c+10.0) 57 | 58 | bevel = profile.make_profile([ 59 | (c-1.0, 0.0), 60 | (c, 2.0) 61 | ]) 62 | outer_profile = outer_profile - bevel 63 | inner_profile = inner_profile + bevel 64 | 65 | outer = shape.extrude_profile(outer_profile) 66 | inner = shape.extrude_profile(inner_profile) 67 | widget = outer.copy() 68 | widget.remove(inner) 69 | 70 | return outer, inner, widget 71 | 72 | 73 | def make_horn(straight_length, length, d1, d2, power): 74 | k = (d1/d2)**(1.0/power) 75 | a = k*length/(1.0-k) 76 | print(a, k) 77 | b = d1/(abs(a)**power) 78 | f = lambda x: b*(abs(x+a)**power) 79 | 80 | print(f(0.0), f(length)) 81 | 82 | outer_profile = profile.make_profile([ 83 | (0.0, d2+1.5), 84 | (straight_length+length-0.5, d2+1.5), 85 | (straight_length+length, d2+0.5), 86 | ]) 87 | 88 | steps = 16 89 | inner_profile = profile.make_profile( 90 | [ (0.0, d1) ] + [ 91 | (straight_length+i*length/steps, f(i*length/steps)) 92 | for i in range(steps) 93 | ] + [ (straight_length+length, d2) ]) 94 | 95 | outer = shape.extrude_profile(outer_profile) 96 | inner = shape.extrude_profile(inner_profile) 97 | widget = outer.copy() 98 | widget.remove(inner) 99 | 100 | return outer, inner, widget 101 | 102 | 103 | @config.help("""\ 104 | [Experimental] Make a cornett mouthpiece for a shawm. 105 | ""","""\ 106 | Glue on or attach using a short length of drinking straw. 107 | """) 108 | @config.Float_flag('bore','Bore diameter mouthpiece connects to.') 109 | class Make_mouthpiece(make.Make, make.Miller): 110 | bore = 5.0 111 | 112 | def run(self): 113 | #outer, inner, widget = make_widget( 114 | # tube_length = 8.0, 115 | # tube_diameter = self.bore, 116 | # cup_length = 4.0, #5.0 #6.0, 117 | # final_diameter = 9.0, #12.0 #15.0, 118 | # ) 119 | outer,inner,widget = make_horn( 120 | 4.0, 8.0, 121 | self.bore, 122 | 20.0, 123 | -1.0 124 | ) 125 | self.save(widget, 'mouthpiece') 126 | 127 | #Ripped a strand of wood removing 128 | #mill = self.miller(widget, [0.0],[0.0]) 129 | 130 | mill = self.miller(widget, [-5.0,5.0],[]) 131 | self.save(mill, 'mill-mouthpiece') 132 | 133 | -------------------------------------------------------------------------------- /demakein/raphs_curves/cornu.py: -------------------------------------------------------------------------------- 1 | from math import * 2 | 3 | # implementation adapted from cephes 4 | 5 | def polevl(x, coef): 6 | ans = coef[-1] 7 | for i in range(len(coef) - 2, -1, -1): 8 | ans = ans * x + coef[i] 9 | return ans 10 | 11 | sn = [ 12 | -2.99181919401019853726E3, 13 | 7.08840045257738576863E5, 14 | -6.29741486205862506537E7, 15 | 2.54890880573376359104E9, 16 | -4.42979518059697779103E10, 17 | 3.18016297876567817986E11 18 | ] 19 | sn.reverse() 20 | sd = [ 21 | 1.00000000000000000000E0, 22 | 2.81376268889994315696E2, 23 | 4.55847810806532581675E4, 24 | 5.17343888770096400730E6, 25 | 4.19320245898111231129E8, 26 | 2.24411795645340920940E10, 27 | 6.07366389490084639049E11 28 | ] 29 | sd.reverse() 30 | cn = [ 31 | -4.98843114573573548651E-8, 32 | 9.50428062829859605134E-6, 33 | -6.45191435683965050962E-4, 34 | 1.88843319396703850064E-2, 35 | -2.05525900955013891793E-1, 36 | 9.99999999999999998822E-1 37 | ] 38 | cn.reverse() 39 | cd = [ 40 | 3.99982968972495980367E-12, 41 | 9.15439215774657478799E-10, 42 | 1.25001862479598821474E-7, 43 | 1.22262789024179030997E-5, 44 | 8.68029542941784300606E-4, 45 | 4.12142090722199792936E-2, 46 | 1.00000000000000000118E0 47 | ] 48 | cd.reverse() 49 | 50 | fn = [ 51 | 4.21543555043677546506E-1, 52 | 1.43407919780758885261E-1, 53 | 1.15220955073585758835E-2, 54 | 3.45017939782574027900E-4, 55 | 4.63613749287867322088E-6, 56 | 3.05568983790257605827E-8, 57 | 1.02304514164907233465E-10, 58 | 1.72010743268161828879E-13, 59 | 1.34283276233062758925E-16, 60 | 3.76329711269987889006E-20 61 | ] 62 | fn.reverse() 63 | fd = [ 64 | 1.00000000000000000000E0, 65 | 7.51586398353378947175E-1, 66 | 1.16888925859191382142E-1, 67 | 6.44051526508858611005E-3, 68 | 1.55934409164153020873E-4, 69 | 1.84627567348930545870E-6, 70 | 1.12699224763999035261E-8, 71 | 3.60140029589371370404E-11, 72 | 5.88754533621578410010E-14, 73 | 4.52001434074129701496E-17, 74 | 1.25443237090011264384E-20 75 | ] 76 | fd.reverse() 77 | gn = [ 78 | 5.04442073643383265887E-1, 79 | 1.97102833525523411709E-1, 80 | 1.87648584092575249293E-2, 81 | 6.84079380915393090172E-4, 82 | 1.15138826111884280931E-5, 83 | 9.82852443688422223854E-8, 84 | 4.45344415861750144738E-10, 85 | 1.08268041139020870318E-12, 86 | 1.37555460633261799868E-15, 87 | 8.36354435630677421531E-19, 88 | 1.86958710162783235106E-22 89 | ] 90 | gn.reverse() 91 | gd = [ 92 | 1.00000000000000000000E0, 93 | 1.47495759925128324529E0, 94 | 3.37748989120019970451E-1, 95 | 2.53603741420338795122E-2, 96 | 8.14679107184306179049E-4, 97 | 1.27545075667729118702E-5, 98 | 1.04314589657571990585E-7, 99 | 4.60680728146520428211E-10, 100 | 1.10273215066240270757E-12, 101 | 1.38796531259578871258E-15, 102 | 8.39158816283118707363E-19, 103 | 1.86958710162783236342E-22 104 | ] 105 | gd.reverse() 106 | 107 | 108 | def fresnel(xxa): 109 | x = abs(xxa) 110 | x2 = x * x 111 | if x2 < 2.5625: 112 | t = x2 * x2 113 | ss = x * x2 * polevl(t, sn) / polevl(t, sd) 114 | cc = x * polevl(t, cn) / polevl(t, cd) 115 | elif x > 36974.0: 116 | ss = 0.5 117 | cc = 0.5 118 | else: 119 | t = pi * x2 120 | u = 1.0 / (t * t) 121 | t = 1.0 / t 122 | f = 1.0 - u * polevl(u, fn) / polevl(u, fd) 123 | g = t * polevl(u, gn) / polevl(u, gd) 124 | t = pi * .5 * x2 125 | c = cos(t) 126 | s = sin(t) 127 | t = pi * x 128 | cc = 0.5 + (f * s - g * c) / t 129 | ss = 0.5 - (f * c + g * s) / t 130 | if xxa < 0: 131 | cc = -cc 132 | ss = -ss 133 | return ss, cc 134 | 135 | def eval_cornu(t): 136 | spio2 = sqrt(pi * .5) 137 | s, c = fresnel(t / spio2) 138 | s *= spio2 139 | c *= spio2 140 | return s, c 141 | -------------------------------------------------------------------------------- /demakein/svg.py: -------------------------------------------------------------------------------- 1 | 2 | PREAMBLE = """\ 3 | 4 | 6 | 7 | 13 | 14 | 15 | """ 16 | 17 | POSTAMBLE = """\ 18 | 19 | """ 20 | 21 | class SVG: 22 | def __init__(self): 23 | self.min_x = None 24 | self.max_x = None 25 | self.min_y = None 26 | self.max_y = None 27 | self.commands = [ ] 28 | 29 | def save(self, filename): 30 | # Assume 90dpi (inkscape default 31 | scale = 90 / 25.4 32 | pad = max(self.max_x-self.min_x,self.max_y-self.min_y) * 0.1 33 | width = (self.max_x-self.min_x+pad*2) 34 | height = (self.max_y-self.min_y+pad*2) 35 | trans_x = -self.min_x+pad 36 | trans_y = -self.min_y+pad 37 | neg_trans_x = -trans_x 38 | neg_trans_y = -trans_y 39 | 40 | with open(filename, 'w') as f: 41 | f.write(PREAMBLE % locals()) 42 | for item in self.commands: 43 | f.write(item + '\n') 44 | f.write(POSTAMBLE) 45 | 46 | def require(self, x,y): 47 | if self.min_x is None: 48 | self.min_x = self.max_x = x 49 | self.min_y = self.max_y = y 50 | else: 51 | self.min_x = min(self.min_x,x) 52 | self.max_x = max(self.max_x,x) 53 | self.min_y = min(self.min_y,y) 54 | self.max_y = max(self.max_y,y) 55 | 56 | def circle(self, x,y,diameter, stroke='#000000'): 57 | radius = diameter * 0.5 58 | self.require(x-radius,y-radius) 59 | self.require(x+radius,y+radius) 60 | self.commands.append( 61 | '' % locals() 62 | ) 63 | 64 | def line(self, points, color='#000000', width=0.25): 65 | for x,y in points: self.require(x,y) 66 | self.commands.append( 67 | '' % ( 68 | ' '.join( '%f,%f'%item for item in points ), 69 | color, 70 | width 71 | ) 72 | ) 73 | 74 | def polygon(self, points, color='#000000', width=0.25): 75 | for x,y in points: self.require(x,y) 76 | self.commands.append( 77 | '' % ( 78 | ' '.join( '%f,%f'%item for item in points ), 79 | color, 80 | width 81 | ) 82 | ) 83 | 84 | def profile(self, profile, color='#000000'): 85 | points = [ ] 86 | for i, pos in enumerate(profile.pos): 87 | if i: 88 | points.append( (profile.low[i], pos) ) 89 | if not i or (profile.low[i] != profile.high[i] and i < len(profile.pos)-1): 90 | points.append( (profile.high[i], pos) ) 91 | self.line([ ( 0.5*x,-y) for x,y in points ], color) 92 | self.line([ (-0.5*x,-y) for x,y in points ], color) 93 | 94 | def text(self, x,y, text, color='#666666'): 95 | font_height = 8 96 | yy = y + font_height * 0.5 97 | self.require(x,yy-font_height) 98 | self.require(x+len(text)*font_height*0.8,yy) 99 | self.commands.append( 100 | '%(text)s' % locals() 101 | ) 102 | return y-font_height 103 | 104 | -------------------------------------------------------------------------------- /demakein/tune.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | from . import config, design, optimize 5 | 6 | 7 | class Working(object): pass 8 | 9 | @config.help( 10 | 'Modelling the mouthpiece is more difficult than modelling the body of an ' 11 | 'instrument. Some parameters are most easily determined empirically.', 12 | 'This tool tries to explain observed frequencies obtained from an instrument ' 13 | 'by tweaking parameters to do with the mouthpiece. ' 14 | 'Resultant parameters should then result in a correctly tuned instrument ' 15 | 'when the design tool is run again.', 16 | ) 17 | @config.Positional( 18 | 'tweak', 19 | 'Comma separated list of parameters to tweak.' 20 | ) 21 | @config.Main_section( 22 | 'observations', 23 | 'Comma separated lists of frequency followed by ' 24 | 'whether each finger hole is open (0) or closed (1) ' 25 | '(from bottom to top).' 26 | ) 27 | class Tune(config.Action_with_working_dir): 28 | tweak = None 29 | observations = [ ] 30 | 31 | def _constraint_score(self, state): 32 | #All positive 33 | return sum( max(-item,0.0) for item in state ) 34 | 35 | def _errors(self, state): 36 | mod = self.working.designer( 37 | **dict(list(zip(self.working.parameters,state))) 38 | ) 39 | 40 | instrument = mod.patch_instrument( 41 | mod.unpack(self.working.designer.state_vec) 42 | ) 43 | instrument.prepare_phase() 44 | 45 | errors = [ ] 46 | 47 | s = 1200.0/math.log(2) 48 | for item in self.observations: 49 | parts = item.split(',') 50 | assert len(parts) == (mod.n_holes+1) 51 | fingers = [ int(item2) for item2 in parts[1:] ] 52 | w_obtained = design.SPEED_OF_SOUND / float(parts[0]) 53 | w_expected = instrument.true_wavelength_near(w_obtained, fingers) 54 | 55 | errors.append( (math.log(w_obtained)-math.log(w_expected))*s ) 56 | 57 | return errors 58 | 59 | def _score(self, state): 60 | errors = self._errors(state) 61 | p = 2 62 | return (sum( abs(item**p) for item in errors ) / max(1,len(errors)))**(1.0/p) 63 | 64 | def _report(self, state, etc=[]): 65 | print() 66 | for name, value in zip(self.working.parameters, state): 67 | print('%s %.3f' % (name, value)) 68 | print() 69 | for error, observation in zip(self._errors(state),self.observations): 70 | print('%6.1f cents %s' % (error, observation)) 71 | print('--------------') 72 | print('%6.1f score' % self._score(state)) 73 | print() 74 | 75 | def run(self): 76 | self.working = Working() 77 | self.working.designer = design.load(self.working_dir) 78 | self.working.parameters = [ ] 79 | 80 | for item in self.tweak.split(','): 81 | fixed = '=' in item 82 | if fixed: 83 | item, value = item.split('=') 84 | value = float(value) 85 | 86 | for item2 in self.working.designer.parameters: 87 | if item2.shell_name().lstrip('-') == item.lstrip('-'): 88 | if fixed: 89 | self.working.designer = self.working.designer(**{item2.name:value}) 90 | else: 91 | self.working.parameters.append(item2.name) 92 | break 93 | else: 94 | assert False, 'Unknown parameter: %s' % item 95 | 96 | initial = [ 97 | getattr(self.working.designer,item) 98 | for item in self.working.parameters 99 | ] 100 | 101 | print('Current model and errors:') 102 | self._report(initial) 103 | 104 | if self.parameters: 105 | state = optimize.improve( 106 | self.shell_name(), 107 | self._constraint_score, 108 | self._score, 109 | initial, 110 | #monitor=self._report 111 | ) 112 | 113 | print('Optimized model and errors:') 114 | self._report(state) 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /backyard/mill/send.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | Z command units are 0.025mm (1./40 mm) 5 | 6 | V command units are mm/sec 7 | 8 | """ 9 | 10 | import sys, os, math, time, random 11 | 12 | import serial 13 | 14 | import nesoni 15 | from nesoni import config 16 | 17 | MAX_X = 200 * 40 #... 18 | MAX_Y = 150 * 40 #... slightly more? 19 | MAX_Z = 2420 20 | 21 | def shift(commands, x,y,v): 22 | result = [ ] 23 | #print '(height hack)' 24 | for item in commands: 25 | if item.startswith('Z'): 26 | pos = list(map(int,item[1:].split(','))) 27 | #if pos[2] >= 40: pos[2] = 2420 28 | pos[0] += x 29 | pos[1] += y 30 | assert 0 <= pos[0] < MAX_X, 'outside work area on x axis' 31 | assert 0 <= pos[1] < MAX_Y, 'outside work area on y axis' 32 | result.append('Z%d,%d,%d' % tuple(pos)) 33 | elif item.startswith('V') and v != 1.0: 34 | vel = float(item[1:]) 35 | vel *= v 36 | result.append('V%.1f' % vel) 37 | else: 38 | result.append(item) 39 | return result 40 | 41 | def execute(commands, port_name, start_command=0): 42 | port = serial.Serial( 43 | port = port_name, 44 | baudrate = 9600, 45 | timeout = 0, #Read timeout 46 | ) 47 | 48 | def check_dsr(): 49 | t = 10.0 50 | while True: 51 | try: 52 | return port.getDSR() 53 | except IOError: 54 | print(' IOError ') 55 | time.sleep(t) 56 | t *= 2 57 | 58 | start = time.time() 59 | #for i, command in enumerate(commands): 60 | for i in range(start_command,len(commands)): 61 | command = commands[i] 62 | 63 | #char = port.read(1) 64 | #if char: 65 | # sys.stdout.write('\nRead: ') 66 | # while char: 67 | # sys.stdout.write(char) 68 | # sys.stdout.flush() 69 | # char = port.read(1) 70 | # sys.stdout.write('\n') 71 | # sys.stdout.flush() 72 | 73 | command = command.strip() + ';\n' 74 | 75 | #Paranoia 76 | for j in range(3): 77 | while not check_dsr(): 78 | time.sleep(0.01) 79 | port.write(';\n') 80 | 81 | for char in command: 82 | while not check_dsr(): 83 | time.sleep(0.01) 84 | port.write(char) 85 | 86 | delta = int(time.time()-start) 87 | sys.stdout.write('\r%3dmin %3.1f%%' % ( 88 | delta // 60, 89 | (i+1.0)/len(commands)*100.0 90 | )) 91 | sys.stdout.flush() 92 | 93 | port.close() 94 | print() 95 | 96 | 97 | 98 | @config.Positional('filename', '.prn file to send to mill.') 99 | @config.String_flag('port') 100 | @config.Float_flag('x', 'X offset') 101 | @config.Float_flag('y', 'Y offset') 102 | @config.Float_flag('v', 'Velocity multiplier') 103 | @config.Float_flag('percent', 'Start from percent-done') 104 | class Send(config.Action): 105 | filename = None 106 | port = '/dev/ttyUSB0' 107 | x = 0.0 108 | y = 0.0 109 | v = 1.0 110 | percent = 0.0 111 | 112 | def run(self): 113 | commands = open(self.filename,'rb').read().strip().rstrip(';').split(';') 114 | commands = [ item.strip() for item in commands ] 115 | 116 | print(len(commands), 'commands') 117 | 118 | body_start = commands.index('!MC1') + 1 119 | 120 | body_end = body_start 121 | while body_end < len(commands) and commands[body_end][:1] != '!': 122 | body_end += 1 123 | 124 | #commands = ( 125 | # commands[:body_start_1] + 126 | # shift(commands[body_start_1:], int(self.x*40),int(self.y*40)) 127 | # ) 128 | 129 | start = body_start + int(self.percent/100.0*(body_end-body_start)) 130 | 131 | while True: 132 | if start == body_start: break 133 | if commands[start].startswith('Z'): 134 | pos = list(map(int,commands[start][1:].split(','))) 135 | if pos[2] >= 2400: 136 | break 137 | start -= 1 138 | 139 | print('commands ', len(commands)) 140 | print('body_start', body_start) 141 | print('start ', start) 142 | print('body_end ', body_end) 143 | 144 | execute(commands[:body_start], self.port) 145 | 146 | execute(shift(commands[body_start:],int(self.x*40),int(self.y*40),self.v), 147 | self.port, start-body_start) 148 | 149 | 150 | 151 | if __name__ == '__main__': 152 | nesoni.run_tool(Send) 153 | 154 | 155 | -------------------------------------------------------------------------------- /demakein/sketch.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, collections, math 3 | 4 | from . import shape, svg 5 | 6 | def sub(a,b): 7 | return tuple( aa-bb for aa,bb in zip(a,b) ) 8 | 9 | def dot(a,b): 10 | return sum( aa*bb for aa,bb in zip(a,b) ) 11 | 12 | def cross(a,b): 13 | return (a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]) 14 | 15 | def sketch(thing, outname): 16 | print(outname, end=' ') 17 | sys.stdout.flush() 18 | 19 | ox = 0 20 | oy = 0 21 | 22 | pic = svg.SVG() 23 | pic.require(0,0) 24 | 25 | triangles = thing.triangles() 26 | 27 | def rot(triangles, x,y,z,angle): 28 | mat = shape.rotation_matrix(x,y,z,angle) 29 | return [ [ shape.transform_point_3(mat,item2) for item2 in item ] for item in triangles ] 30 | 31 | def iterator(): 32 | #yield thing, 1 33 | yield triangles, 1 34 | 35 | #item_rot = thing.copy() 36 | #item_rot.rotate(1,0,0,-90) 37 | #yield item_rot, 1 38 | #del item_rot 39 | yield rot(triangles, 1,0,0,-90), 1 40 | 41 | #item_iso = thing.copy() 42 | #item_iso.rotate(0,0,1,-45, 256) 43 | #item_iso.rotate(1,0,0,-45, 256) 44 | #yield item_iso, 0 45 | #del item_iso 46 | yield rot(rot(triangles, 0,0,1,-45), 1,0,0,-45), 0 47 | 48 | 49 | # for (item,showdim) in iterator(): 50 | # extent = item.extent() 51 | for (this_triangles,showdim) in iterator(): 52 | extent = shape.extent_3(item2 for item in this_triangles for item2 in item) 53 | 54 | if extent.xmax-extent.xmin < extent.ymax-extent.ymin: 55 | ox = pic.max_x + 10 56 | oy = -pic.max_y 57 | else: 58 | ox = 0.0 59 | oy = -(extent.ymax-extent.ymin)-pic.max_y - 20 60 | 61 | ox -= extent.xmin 62 | oy -= extent.ymin 63 | 64 | xmid = (extent.xmin+extent.xmax)*0.5 65 | ymid = (extent.ymin+extent.ymax)*0.5 66 | zmid = (extent.zmin+extent.zmax)*0.5 67 | if showdim: 68 | pic.text(ox+xmid, -oy-extent.ymax-10, '%.1fmm' % (extent.xmax-extent.xmin)) 69 | pic.text(ox+extent.xmax + 5, -oy-ymid, '%.1fmm' % (extent.ymax-extent.ymin)) 70 | 71 | lines = collections.defaultdict(list) 72 | for tri in this_triangles: 73 | #a = numpy.array(tri) 74 | #normal = numpy.cross(a[1]-a[0],a[2]-a[0]) 75 | normal = cross(sub(tri[1],tri[0]),sub(tri[2],tri[0])) 76 | length = math.sqrt(normal[0]**2+normal[1]**2+normal[2]**2) 77 | 78 | if length == 0.0: 79 | print("Warning: Zero area triangle in sketch.") 80 | continue 81 | 82 | normal = (normal[0]/length,normal[1]/length,normal[2]/length) 83 | 84 | 85 | lines[(tri[0],tri[1])].append( normal ) 86 | lines[(tri[1],tri[2])].append( normal ) 87 | lines[(tri[2],tri[0])].append( normal ) 88 | 89 | for a,b in lines: 90 | if (b,a) not in lines: 91 | weight = -1.0 92 | normals = lines[(a,b)] 93 | else: 94 | if (b,a) < (a,b): continue 95 | normals = lines[(a,b)] + lines[(b,a)] 96 | 97 | weight = -1.0 98 | for n1 in lines[(a,b)]: 99 | for n2 in lines[(b,a)]: 100 | if n1[2]*n2[2] > 0.0: 101 | weight = max(weight, dot(n1,n2)) 102 | 103 | weight = (1.0-weight)*0.5 104 | if weight < 0.01: continue 105 | weight = min(1.0,weight*2) 106 | 107 | outs = 0 108 | ins = 0 109 | for n in normals: 110 | if n[2] <= 1e-6: 111 | ins += 1 112 | if n[2] >= -1e-6: 113 | outs += 1 114 | 115 | if ins >= 2 and not outs: 116 | weight *= 0.25 117 | #elif outs >= 2 and not ins: 118 | # weight *= 2.0 119 | 120 | pic.line([ (ox+x,-oy-y) for x,y,z in [a,b] ],width=0.2 * weight) 121 | 122 | del lines 123 | 124 | pic.save(outname) 125 | print() 126 | 127 | 128 | def run(args): 129 | for filename in args: 130 | prefix = filename 131 | if prefix[-4:].lower() == '.stl': 132 | prefix = prefix[:-4] 133 | outname = prefix + '-sketch.svg' 134 | 135 | item = shape.load_stl(filename) 136 | 137 | sketch(item, outname) 138 | 139 | 140 | 141 | if __name__ == '__main__': 142 | shape.main(run) -------------------------------------------------------------------------------- /demakein/all.py: -------------------------------------------------------------------------------- 1 | 2 | import demakein 3 | from . import config, legion 4 | 5 | @config.help("""\ 6 | Design and make a selection of instruments, in a variety of sizes. 7 | ""","""\ 8 | Note that I use the terms "soprano", "alto", "tenor", and "bass" to \ 9 | refer to the sizes of instruments. Flutes and shawms I name this way are actually \ 10 | an octave above the singing voices of the same name. 11 | """) 12 | @config.Bool_flag('panpipes', 'Do panpipes.') 13 | @config.Bool_flag('flutes', 'Do flutes.') 14 | @config.Bool_flag('whistles', 'Do whistles.') 15 | @config.Bool_flag('shawms', 'Do shawms. May be broken.') 16 | @config.String_flag('version', 'version code, for file names') 17 | class All(config.Action_with_output_dir): 18 | panpipes = True 19 | flutes = True 20 | whistles = True 21 | shawms = False 22 | version = 'v'+demakein.VERSION.lstrip('0.') 23 | 24 | def _do_flute(self, model_name, model_code, size_name, size_code, designer, transpose): 25 | workspace = self.get_workspace() 26 | outdir = workspace / (model_name+'-'+size_name) 27 | 28 | designer( 29 | outdir, 30 | transpose=transpose 31 | ).make() 32 | 33 | demakein.Make_flute( 34 | outdir, 35 | prefix = model_code+size_code+'-'+self.version+'-', 36 | decorate=True, 37 | ).make() 38 | 39 | def _do_shawm(self, model_name, model_code, size_name, size_code, designer, transpose, bore): 40 | workspace = self.get_workspace() 41 | outdir = workspace / (model_name+'-'+size_name) 42 | 43 | designer( 44 | outdir, 45 | transpose=transpose, 46 | bore=bore, 47 | ).make() 48 | 49 | demakein.Make_reed_instrument( 50 | outdir, 51 | prefix=model_code+size_code+'-'+self.version+'-', 52 | decorate=True, 53 | ).make() 54 | 55 | def _do_folk_whistle(self, model_name, model_code, size_name, size_code, transpose): 56 | workspace = self.get_workspace() 57 | outdir = workspace / (model_name+'-'+size_name) 58 | 59 | demakein.Design_folk_whistle( 60 | outdir, 61 | transpose=transpose, 62 | ).make() 63 | 64 | demakein.Make_whistle( 65 | outdir, 66 | prefix=model_code+size_code+'-'+self.version+'-', 67 | ).make() 68 | 69 | def run(self): 70 | workspace = self.get_workspace() 71 | 72 | if self.panpipes: 73 | if self.make: 74 | demakein.Make_panpipe( 75 | workspace/'panpipe' 76 | ).make() 77 | 78 | if self.flutes: 79 | for model_name, model_code, designer in [ 80 | ('folk-flute', 'FF', demakein.Design_folk_flute), 81 | ('pflute', 'PF', demakein.Design_pflute), 82 | #('folk-flute-straight', 'FFS', demakein.Design_straight_folk_flute), 83 | #('folk-flute-tapered', 'FFT', demakein.Design_tapered_folk_flute), 84 | #('pflute-straight', 'PFS', demakein.Design_straight_pflute), 85 | #('pflute-tapered', 'PFT', demakein.Design_tapered_pflute), 86 | ]: 87 | for size_name, size_code, transpose in [ 88 | ('tenor', 't', 0), 89 | ('alto', 'a', 5), 90 | ('soprano', 's', 12), 91 | ]: 92 | self._do_flute(model_name,model_code,size_name,size_code,designer,transpose) 93 | 94 | if self.whistles: 95 | for model_name, model_code in [ 96 | ('folk-whistle', 'FW'), 97 | ]: 98 | for size_name, size_code, transpose in [ 99 | ('tenor', 't', 0), 100 | ('alto', 'a', 5), 101 | ('soprano', 's', 12), 102 | ('sopranino', 'ss', 17), 103 | ]: 104 | self._do_folk_whistle(model_name,model_code,size_name,size_code,transpose) 105 | 106 | if self.shawms: 107 | for model_name, model_code, designer in [ 108 | ('shawm', 'SH', demakein.Design_shawm), 109 | ('folk-shawm', 'FSH', demakein.Design_folk_shawm), 110 | ]: 111 | for size_name, size_code, transpose, bore in [ 112 | ('4mm-alto', '4a', 5, 4.0), 113 | ('4mm-tenor', '4t', 0, 4.0), 114 | ('6mm-tenor', '6t', 0, 6.0), 115 | ('6mm-bass', '6b', -7, 6.0), 116 | ]: 117 | self._do_shawm(model_name,model_code,size_name,size_code,designer,transpose,bore) 118 | 119 | 120 | -------------------------------------------------------------------------------- /demakein/workspace.py: -------------------------------------------------------------------------------- 1 | 2 | import os, pickle, json, tempfile, contextlib, shutil 3 | 4 | class Workspace(object): 5 | """ Directory containing pickled objects, etc 6 | 7 | Properties: 8 | name - directory base name 9 | param - a dictionary of parameters stored in a file 10 | called "parameters". 11 | Note: update this with .update_param(...) 12 | 13 | Operator overloading: 14 | workspace / 'filename' 15 | - returns the full path of a file called 'filename' 16 | in the workspace directory. 17 | (Bless me Guido, for I have sinned.) 18 | 19 | """ 20 | 21 | def __init__(self, working_dir, must_exist=False): 22 | self.working_dir = os.path.normpath(working_dir) 23 | if not os.path.exists(self.working_dir): 24 | assert not must_exist, working_dir + ' does not exist' 25 | os.mkdir(self.working_dir) 26 | else: 27 | assert os.path.isdir(self.working_dir), self.working_dir + ' exists and is not a directory' 28 | 29 | self.name = os.path.split(os.path.abspath(working_dir))[1] 30 | 31 | # @property 32 | # def param(self): 33 | # if self.object_exists('parameters'): 34 | # return self.get_object('parameters', plain_text=True) 35 | # else: 36 | # return { } 37 | # 38 | # def update_param(self, remove=[], **updates): 39 | # param = self.param 40 | # for item in remove: 41 | # if item in self.param: 42 | # del param[item] 43 | # param.update(updates) 44 | # self.set_object(param, 'parameters', plain_text=True) 45 | # 46 | # def open(self, path, mode): 47 | # return open(self.object_filename(path), mode) 48 | # 49 | # def object_exists(self, path): 50 | # return os.path.exists(self.object_filename(path)) 51 | # 52 | # def get_object(self, path, plain_text=False): 53 | # from nesoni import io 54 | # f = io.open_possibly_compressed_file(self._object_filename(path)) 55 | # if plain_text: 56 | # data = f.read() 57 | # try: 58 | # result = json.loads(data) 59 | # except ValueError: #Older versions used repr instead of json.dump 60 | # result = eval(data) 61 | # else: 62 | # result = cPickle.load(f) 63 | # f.close() 64 | # return result 65 | # 66 | # def set_object(self, obj, path, plain_text=False): 67 | # from nesoni import io 68 | # temp_filename = self._object_filename('tempfile') 69 | # if plain_text: 70 | # f = open(temp_filename, 'wb') 71 | # json.dump(obj, f) 72 | # f.close() 73 | # else: 74 | # f = io.Pipe_writer(temp_filename, ['gzip']) 75 | # cPickle.dump(obj, f, 2) 76 | # f.close() 77 | # 78 | # os.rename(temp_filename, self._object_filename(path)) 79 | 80 | def path_as_relative_path(self, path): 81 | #Breaks thumbnails if outputing to absolute output dir 82 | #if os.path.isabs(path): 83 | # return path 84 | 85 | #assert os.path.sep == '/' #Someone else can work this out on windows and mac 86 | sep = os.path.sep 87 | me = os.path.abspath(self.working_dir).strip(sep).split(sep) 88 | it = os.path.abspath(path).strip(sep).split(sep) 89 | n_same = 0 90 | while n_same < len(me) and n_same < len(it) and me[n_same] == it[n_same]: 91 | n_same += 1 92 | return os.path.normpath( sep.join( ['..']*(len(me)-n_same) + it[n_same:] ) ) 93 | 94 | def relative_path_as_path(self, path): 95 | if os.path.isabs(path): 96 | return path 97 | 98 | return os.path.normpath(os.path.join(self.working_dir,path)) 99 | object_filename = relative_path_as_path 100 | _object_filename = object_filename 101 | 102 | # A single step down the path of darkness, what's the worst that can happen? 103 | def __truediv__(self, path): 104 | if not isinstance(path, str): 105 | path = os.path.join(*path) 106 | return self.relative_path_as_path(path) 107 | 108 | 109 | @contextlib.contextmanager 110 | def tempspace(dir=None): 111 | """ Use this in a "with" statement to create a temporary workspace. 112 | 113 | Example: 114 | 115 | with workspace.tempspace() as temp: 116 | with open(temp/'hello.c','wb') as f: 117 | f.write('#include \nint main() { printf("Hello world\\n"); }') 118 | os.system('cd '+temp.working_dir+' && gcc hello.c && ./a.out') 119 | """ 120 | path = tempfile.mkdtemp(dir=dir) 121 | try: 122 | yield Workspace(path, must_exist=True) 123 | finally: 124 | shutil.rmtree(path) 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /demakein/raphs_curves/poly3.py: -------------------------------------------------------------------------------- 1 | # Numerical techniques for solving 3rd order polynomial spline systems 2 | 3 | # The standard representation is the vector of derivatives at s=0, 4 | # with -.5 <= s <= 5. 5 | # 6 | # Thus, \kappa(s) = k0 + k1 s + 1/2 k2 s^2 + 1/6 k3 s^3 7 | 8 | from math import * 9 | 10 | def eval_cubic(a, b, c, d, x): 11 | return ((d * x + c) * x + b) * x + a 12 | 13 | # integrate over s = [0, 1] 14 | def int_3spiro_poly(ks, n): 15 | x, y = 0, 0 16 | th = 0 17 | ds = 1.0 / n 18 | th1, th2, th3, th4 = ks[0], .5 * ks[1], (1./6) * ks[2], (1./24) * ks[3] 19 | k0, k1, k2, k3 = ks[0] * ds, ks[1] * ds, ks[2] * ds, ks[3] * ds 20 | s = 0 21 | result = [(x, y)] 22 | for i in range(n): 23 | sm = s + 0.5 * ds 24 | th = sm * eval_cubic(th1, th2, th3, th4, sm) 25 | cth = cos(th) 26 | sth = sin(th) 27 | 28 | km0 = ((1./6 * k3 * sm + .5 * k2) * sm + k1) * sm + k0 29 | km1 = ((.5 * k3 * sm + k2) * sm + k1) * ds 30 | km2 = (k3 * sm + k2) * ds * ds 31 | km3 = k3 * ds * ds * ds 32 | #print km0, km1, km2, km3 33 | u = 1 - km0 * km0 / 24 34 | v = km1 / 24 35 | 36 | u = 1 - km0 * km0 / 24 + (km0 ** 4 - 4 * km0 * km2 - 3 * km1 * km1) / 1920 37 | v = km1 / 24 + (km3 - 6 * km0 * km0 * km1) / 1920 38 | 39 | x += cth * u - sth * v 40 | y += cth * v + sth * u 41 | result.append((ds * x, ds * y)) 42 | 43 | s += ds 44 | 45 | return result 46 | 47 | def integ_chord(k, n = 64): 48 | ks = (k[0] * .5, k[1] * .25, k[2] * .125, k[3] * .0625) 49 | xp, yp = int_3spiro_poly(ks, n)[-1] 50 | ks = (k[0] * -.5, k[1] * .25, k[2] * -.125, k[3] * .0625) 51 | xm, ym = int_3spiro_poly(ks, n)[-1] 52 | dx, dy = .5 * (xp + xm), .5 * (yp + ym) 53 | return hypot(dx, dy), atan2(dy, dx) 54 | 55 | # Return th0, th1, k0, k1 for given params 56 | def calc_thk(ks): 57 | chord, ch_th = integ_chord(ks) 58 | th0 = ch_th - (-.5 * ks[0] + .125 * ks[1] - 1./48 * ks[2] + 1./384 * ks[3]) 59 | th1 = (.5 * ks[0] + .125 * ks[1] + 1./48 * ks[2] + 1./384 * ks[3]) - ch_th 60 | k0 = chord * (ks[0] - .5 * ks[1] + .125 * ks[2] - 1./48 * ks[3]) 61 | k1 = chord * (ks[0] + .5 * ks[1] + .125 * ks[2] + 1./48 * ks[3]) 62 | #print '%', (-.5 * ks[0] + .125 * ks[1] - 1./48 * ks[2] + 1./384 * ks[3]), (.5 * ks[0] + .125 * ks[1] + 1./48 * ks[2] + 1./384 * ks[3]), ch_th 63 | return th0, th1, k0, k1 64 | 65 | def calc_k1k2(ks): 66 | chord, ch_th = integ_chord(ks) 67 | k1l = chord * chord * (ks[1] - .5 * ks[2] + .125 * ks[3]) 68 | k1r = chord * chord * (ks[1] + .5 * ks[2] + .125 * ks[3]) 69 | k2l = chord * chord * chord * (ks[2] - .5 * ks[3]) 70 | k2r = chord * chord * chord * (ks[2] + .5 * ks[3]) 71 | return k1l, k1r, k2l, k2r 72 | 73 | def plot(ks): 74 | ksp = (ks[0] * .5, ks[1] * .25, ks[2] * .125, ks[3] * .0625) 75 | pside = int_3spiro_poly(ksp, 64) 76 | ksm = (ks[0] * -.5, ks[1] * .25, ks[2] * -.125, ks[3] * .0625) 77 | mside = int_3spiro_poly(ksm, 64) 78 | mside.reverse() 79 | for i in range(len(mside)): 80 | mside[i] = (-mside[i][0], -mside[i][1]) 81 | pts = mside + pside[1:] 82 | cmd = "moveto" 83 | for j in range(len(pts)): 84 | x, y = pts[j] 85 | print(306 + 300 * x, 400 + 300 * y, cmd) 86 | cmd = "lineto" 87 | print("stroke") 88 | x, y = pts[0] 89 | print(306 + 300 * x, 400 + 300 * y, "moveto") 90 | x, y = pts[-1] 91 | print(306 + 300 * x, 400 + 300 * y, "lineto .5 setlinewidth stroke") 92 | print("showpage") 93 | 94 | def solve_3spiro(th0, th1, k0, k1): 95 | ks = [0, 0, 0, 0] 96 | for i in range(5): 97 | th0_a, th1_a, k0_a, k1_a = calc_thk(ks) 98 | dth0 = th0 - th0_a 99 | dth1 = th1 - th1_a 100 | dk0 = k0 - k0_a 101 | dk1 = k1 - k1_a 102 | ks[0] += (dth0 + dth1) * 1.5 + (dk0 + dk1) * -.25 103 | ks[1] += (dth1 - dth0) * 15 + (dk0 - dk1) * 1.5 104 | ks[2] += (dth0 + dth1) * -12 + (dk0 + dk1) * 6 105 | ks[3] += (dth0 - dth1) * 360 + (dk1 - dk0) * 60 106 | #print '% ks =', ks 107 | return ks 108 | 109 | def iter_spline(pts, ths, ks): 110 | pass 111 | 112 | def solve_vee(): 113 | kss = [] 114 | for i in range(10): 115 | kss.append([0, 0, 0, 0]) 116 | thl = [0] * len(kss) 117 | thr = [0] * len(kss) 118 | k0l = [0] * len(kss) 119 | k0r = [0] * len(kss) 120 | k1l = [0] * len(kss) 121 | k1r = [0] * len(kss) 122 | k2l = [0] * len(kss) 123 | k2r = [0] * len(kss) 124 | for i in range(10): 125 | for j in range(len(kss)): 126 | thl[j], thr[j], k0l[j], k0r[j] = calc_thk(kss[j]) 127 | k0l[j], k1r[j], k2l[j], k2r[j] = calc_k1k2(kss[j]) 128 | for j in range(len(kss) - 1): 129 | dth = thl[j + 1] + thr[j] 130 | if j == 5: dth += .1 131 | dk0 = k0l[j + 1] - k0r[j] 132 | dk1 = k1l[j + 1] - k1r[j] 133 | dk2 = k2l[j + 1] - k2r[j] 134 | 135 | 136 | if __name__ == '__main__': 137 | k0 = pi * 3 138 | ks = [0, k0, -2 * k0, 0] 139 | ks = [0, 0, 0, 0.01] 140 | #plot(ks) 141 | thk = calc_thk(ks) 142 | print('%', thk) 143 | 144 | ks = solve_3spiro(0, 0, 0, 0.001) 145 | print('% thk =', calc_thk(ks)) 146 | #plot(ks) 147 | print('%', ks) 148 | print(calc_k1k2(ks)) 149 | -------------------------------------------------------------------------------- /demakein/raphs_curves/plot_solve_clothoid.py: -------------------------------------------------------------------------------- 1 | from . import clothoid 2 | from math import * 3 | 4 | print('%!PS-Adobe') 5 | 6 | def integ_spiro(k0, k1, k2, k3, n = 4): 7 | th1 = k0 8 | th2 = .5 * k1 9 | th3 = (1./6) * k2 10 | th4 = (1./24) * k3 11 | ds = 1. / n 12 | ds2 = ds * ds 13 | ds3 = ds2 * ds 14 | s = .5 * ds - .5 15 | 16 | k0 *= ds 17 | k1 *= ds 18 | k2 *= ds 19 | k3 *= ds 20 | 21 | x = 0 22 | y = 0 23 | 24 | for i in range(n): 25 | if n == 1: 26 | km0 = k0 27 | km1 = k1 * ds 28 | km2 = k2 * ds2 29 | else: 30 | km0 = (((1./6) * k3 * s + .5 * k2) * s + k1) * s + k0 31 | km1 = ((.5 * k3 * s + k2) * s + k1) * ds 32 | km2 = (k3 * s + k2) * ds2 33 | km3 = k3 * ds3 34 | 35 | t1_1 = km0 36 | t1_2 = .5 * km1 37 | t1_3 = (1./6) * km2 38 | t1_4 = (1./24) * km3 39 | t2_2 = t1_1 * t1_1 40 | t2_3 = 2 * (t1_1 * t1_2) 41 | t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2 42 | t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3) 43 | t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3 44 | t2_7 = 2 * (t1_3 * t1_4) 45 | t2_8 = t1_4 * t1_4 46 | t3_4 = t2_2 * t1_2 + t2_3 * t1_1 47 | t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1 48 | t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1 49 | t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2 50 | t4_4 = t2_2 * t2_2 51 | t4_5 = 2 * (t2_2 * t2_3) 52 | t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3 53 | t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4) 54 | t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4 55 | t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5) 56 | t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5 57 | t5_6 = t4_4 * t1_2 + t4_5 * t1_1 58 | t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1 59 | t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1 60 | t6_6 = t4_4 * t2_2 61 | t6_7 = t4_4 * t2_3 + t4_5 * t2_2 62 | t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2 63 | t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2 64 | t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2 65 | t7_8 = t6_6 * t1_2 + t6_7 * t1_1 66 | t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1 67 | t8_8 = t6_6 * t2_2 68 | t8_9 = t6_6 * t2_3 + t6_7 * t2_2 69 | t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2 70 | t9_10 = t8_8 * t1_2 + t8_9 * t1_1 71 | t10_10 = t8_8 * t2_2 72 | u = 1 73 | u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8 74 | u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8 + (1./270336) * t4_10 75 | u -= (1./322560) * t6_6 + (1./1658880) * t6_8 + (1./8110080) * t6_10 76 | u += (1./92897280) * t8_8 + (1./454164480) * t8_10 77 | u -= 2.4464949595157930e-11 * t10_10 78 | v = (1./12) * t1_2 + (1./80) * t1_4 79 | v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8 + (1./67584) * t3_10 80 | v += (1./53760) * t5_6 + (1./276480) * t5_8 + (1./1351680) * t5_10 81 | v -= (1./11612160) * t7_8 + (1./56770560) * t7_10 82 | v += 2.4464949595157932e-10 * t9_10 83 | if n == 1: 84 | x = u 85 | y = v 86 | else: 87 | th = (((th4 * s + th3) * s + th2) * s + th1) * s 88 | cth = cos(th) 89 | sth = sin(th) 90 | 91 | x += cth * u - sth * v 92 | y += cth * v + sth * u 93 | s += ds 94 | return [x * ds, y * ds] 95 | 96 | count_iter = 0 97 | 98 | # Given th0 and th1 at endpoints (measured from chord), return k0 99 | # and k1 such that the clothoid k(s) = k0 + k1 s, evaluated from 100 | # s = -.5 to .5, has the tangents given 101 | def solve_clothoid(th0, th1, verbose = False): 102 | global count_iter 103 | 104 | k1_old = 0 105 | e_old = th1 - th0 106 | k0 = th0 + th1 107 | k1 = 6 * (1 - ((.5 / pi) * k0) ** 3) * e_old 108 | 109 | # secant method 110 | for i in range(10): 111 | x, y = integ_spiro(k0, k1, 0, 0) 112 | e = (th1 - th0) + 2 * atan2(y, x) - .25 * k1 113 | count_iter += 1 114 | if verbose: 115 | print(k0, k1, e) 116 | if abs(e) < 1e-9: break 117 | k1_old, e_old, k1 = k1, e, k1 + (k1_old - k1) * e / (e - e_old) 118 | 119 | return k0, k1 120 | 121 | def plot_by_thp(): 122 | count = 0 123 | for i in range(11): 124 | thp = i * .1 125 | print(.5 + .05 * i, .5, .5, 'setrgbcolor') 126 | print('.75 setlinewidth') 127 | cmd = 'moveto' 128 | for j in range(-40, 41): 129 | thm = j * .02 130 | k0, k1 = solve_clothoid(thp - thm, thp + thm, True) 131 | count += 1 132 | k1 = min(40, max(-40, k1)) 133 | print(306 + 75 * thm, 396 - 10 * k1, cmd) 134 | cmd = 'lineto' 135 | print('stroke') 136 | print('% count_iter = ', count_iter, 'for', count) 137 | 138 | def plot_by_thm(): 139 | print('.75 setlinewidth') 140 | print(36, 396 - 350, 'moveto') 141 | print(0, 700, 'rlineto stroke') 142 | for i in range(-10, 10): 143 | if i == 0: wid = 636 144 | else: wid = 5 145 | print(36, 396 - 10 * i, 'moveto', wid, '0 rlineto stroke') 146 | cmd = 'moveto' 147 | thm = -.1 148 | for i in range(41): 149 | thp = i * .1 150 | k0, k1 = solve_clothoid(thp - thm, thp + thm) 151 | print(36 + 150 * thp, 396 - 100 * k1, cmd) 152 | cmd = 'lineto' 153 | print('stroke') 154 | print('0 0 1 setrgbcolor') 155 | cmd = 'moveto' 156 | for i in range(41): 157 | thp = i * .1 158 | k1 = 12 * thm * cos(.5 * thp) 159 | k1 = 12 * thm * (1 - (thp / pi) ** 3) 160 | print(36 + 150 * thp, 396 - 100 * k1, cmd) 161 | cmd = 'lineto' 162 | print('stroke') 163 | 164 | plot_by_thp() 165 | -------------------------------------------------------------------------------- /demakein/raphs_curves/numintsynth.py: -------------------------------------------------------------------------------- 1 | # Synthesize a procedure to numerically integrate the 3rd order poly spiral 2 | 3 | tex = False 4 | 5 | if tex: 6 | mulsym = ' ' 7 | else: 8 | mulsym = ' * ' 9 | 10 | class Poly: 11 | def __init__(self, p0, coeffs): 12 | self.p0 = p0 13 | self.coeffs = coeffs 14 | def eval(self, x): 15 | y = x ** self.p0 16 | z = 0 17 | for c in coeffs: 18 | z += y * c 19 | y *= x 20 | return z 21 | 22 | def add(poly0, poly1, nmax): 23 | lp0 = len(poly0.coeffs) 24 | lp1 = len(poly1.coeffs) 25 | p0 = min(poly0.p0, poly1.p0) 26 | n = min(max(poly0.p0 + lp0, poly1.p1 + lp1), nmax) - p0 27 | if n <= 0: return Poly(0, []) 28 | coeffs = [] 29 | for i in range(n): 30 | c = 0 31 | if i >= poly0.p0 - p0 and i < lp0 + poly0.p0 - p0: 32 | c += poly0.coeffs[i + p0 - poly0.p0] 33 | if i >= poly1.p0 - p0 and i < lp1 + poly1.p0 - p0: 34 | c += poly1.coeffs[i + p0 - poly1.p0] 35 | coeffs.append(c) 36 | return Poly(p0, coeffs) 37 | 38 | def pr(str): 39 | if tex: 40 | print(str, '\\\\') 41 | else: 42 | print('\t' + str + ';') 43 | 44 | def prd(str): 45 | if tex: 46 | print(str, '\\\\') 47 | else: 48 | print('\tdouble ' + str + ';') 49 | 50 | def polymul(p0, p1, degree, basename, suppress_odd = False): 51 | result = [] 52 | for i in range(min(degree, len(p0) + len(p1) - 1)): 53 | terms = [] 54 | for j in range(i + 1): 55 | if j < len(p0) and i - j < len(p1): 56 | t0 = p0[j] 57 | t1 = p1[i - j] 58 | if t0 != None and t1 != None: 59 | terms.append(t0 + mulsym + t1) 60 | if terms == []: 61 | result.append(None) 62 | else: 63 | var = basename % i 64 | if (j % 2 == 0) or not suppress_odd: 65 | prd(var + ' = ' + ' + '.join(terms)) 66 | result.append(var) 67 | return result 68 | 69 | def polysquare(p0, degree, basename): 70 | result = [] 71 | for i in range(min(degree, 2 * len(p0) - 1)): 72 | terms = [] 73 | for j in range((i + 1)/ 2): 74 | if i - j < len(p0): 75 | t0 = p0[j] 76 | t1 = p0[i - j] 77 | if t0 != None and t1 != None: 78 | terms.append(t0 + mulsym + t1) 79 | if len(terms) >= 1: 80 | if tex and len(terms) == 1: 81 | terms = ['2 ' + terms[0]] 82 | else: 83 | terms = ['2' + mulsym + '(' + ' + '.join(terms) + ')'] 84 | if (i % 2) == 0: 85 | t = p0[i / 2] 86 | if t != None: 87 | if tex: 88 | terms.append(t + '^2') 89 | else: 90 | terms.append(t + mulsym + t) 91 | if terms == []: 92 | result.append(None) 93 | else: 94 | var = basename % i 95 | prd(var + ' = ' + ' + '.join(terms)) 96 | result.append(var) 97 | return result 98 | 99 | def mkspiro(degree): 100 | if tex: 101 | us = ['u = 1'] 102 | vs = ['v ='] 103 | else: 104 | us = ['u = 1'] 105 | vs = ['v = 0'] 106 | if tex: 107 | tp = [None, 't_{11}', 't_{12}', 't_{13}', 't_{14}'] 108 | else: 109 | tp = [None, 't1_1', 't1_2', 't1_3', 't1_4'] 110 | if tex: 111 | prd(tp[1] + ' = k_0') 112 | prd(tp[2] + ' = \\frac{k_1}{2}') 113 | prd(tp[3] + ' = \\frac{k_2}{6}') 114 | prd(tp[4] + ' = \\frac{k_3}{24}') 115 | else: 116 | prd(tp[1] + ' = km0') 117 | prd(tp[2] + ' = .5 * km1') 118 | prd(tp[3] + ' = (1./6) * km2') 119 | prd(tp[4] + ' = (1./24) * km3') 120 | tlast = tp 121 | coef = 1. 122 | for i in range(1, degree - 1): 123 | tmp = [] 124 | tcoef = coef 125 | #print tlast 126 | for j in range(len(tlast)): 127 | c = tcoef / (j + 1) 128 | if (j % 2) == 0 and tlast[j] != None: 129 | if tex: 130 | tmp.append('\\frac{%s}{%.0f}' % (tlast[j], 1./c)) 131 | else: 132 | if c < 1e-9: 133 | cstr = '%.16e' % c 134 | else: 135 | cstr = '(1./%d)' % int(.5 + (1./c)) 136 | tmp.append(cstr + ' * ' + tlast[j]) 137 | tcoef *= .5 138 | if tmp != []: 139 | sign = ('+', '-')[(i / 2) % 2] 140 | var = ('u', 'v')[i % 2] 141 | if tex: 142 | if i == 1: pref = '' 143 | else: pref = sign + ' ' 144 | str = pref + (' ' + sign + ' ').join(tmp) 145 | else: 146 | str = var + ' ' + sign + '= ' + ' + '.join(tmp) 147 | if var == 'u': us.append(str) 148 | else: vs.append(str) 149 | if i < degree - 1: 150 | if tex: 151 | basename = 't_{%d%%d}' % (i + 1) 152 | else: 153 | basename = 't%d_%%d' % (i + 1) 154 | if i == 1: 155 | tnext = polysquare(tp, degree - 1, basename) 156 | t2 = tnext 157 | elif i == 3: 158 | tnext = polysquare(t2l, degree - 1, basename) 159 | elif (i % 2) == 0: 160 | tnext = polymul(tlast, tp, degree - 1, basename, True) 161 | else: 162 | tnext = polymul(t2l, t2, degree - 1, basename) 163 | t2l = tlast 164 | tlast = tnext 165 | coef /= (i + 1) 166 | if tex: 167 | pr(' '.join(us)) 168 | pr(' '.join(vs)) 169 | else: 170 | for u in us: 171 | pr(u) 172 | for v in vs: 173 | pr(v) 174 | 175 | if __name__ == '__main__': 176 | mkspiro(12) 177 | -------------------------------------------------------------------------------- /demakein/raphs_curves/bezfigs.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from math import * 3 | 4 | from . import fromcubic 5 | from . import tocubic 6 | 7 | from . import cornu 8 | 9 | def eps_prologue(x0, y0, x1, y1, draw_box = False): 10 | print('%!PS-Adobe-3.0 EPSF') 11 | print('%%BoundingBox:', x0, y0, x1, y1) 12 | print('%%EndComments') 13 | print('%%EndProlog') 14 | print('%%Page: 1 1') 15 | if draw_box: 16 | print(x0, y0, 'moveto', x0, y1, 'lineto', x1, y1, 'lineto', x1, y0, 'lineto closepath stroke') 17 | 18 | def eps_trailer(): 19 | print('%%EOF') 20 | 21 | def fit_cubic_superfast(z0, z1, arclen, th0, th1, aab): 22 | chord = hypot(z1[0] - z0[0], z1[1] - z0[1]) 23 | cth0, sth0 = cos(th0), sin(th0) 24 | cth1, sth1 = -cos(th1), -sin(th1) 25 | armlen = .66667 * arclen 26 | a = armlen * aab 27 | b = armlen - a 28 | bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a), 29 | (z1[0] + cth1 * b, z1[1] + sth1 * b), z1] 30 | return bz 31 | 32 | def fit_cubic(z0, z1, arclen, th_fn, fast, aabmin = 0, aabmax = 1.): 33 | chord = hypot(z1[0] - z0[0], z1[1] - z0[1]) 34 | if (arclen < 1.000001 * chord): 35 | return [z0, z1], 0 36 | th0 = th_fn(0) 37 | th1 = th_fn(arclen) 38 | imax = 4 39 | jmax = 10 40 | if fast: 41 | imax = 1 42 | jmax = 0 43 | for i in range(imax): 44 | for j in range(jmax + 1): 45 | if jmax == 0: 46 | aab = 0.5 * (aabmin + aabmax) 47 | else: 48 | aab = aabmin + (aabmax - aabmin) * j / jmax 49 | if fast == 2: 50 | bz = fit_cubic_superfast(z0, z1, arclen, th0, th1, aab) 51 | else: 52 | bz = tocubic.fit_cubic_arclen(z0, z1, arclen, th0, th1, aab) 53 | score = tocubic.measure_bz_rk4(bz, arclen, th_fn) 54 | print('% aab =', aab, 'score =', score) 55 | sys.stdout.flush() 56 | if j == 0 or score < best_score: 57 | best_score = score 58 | best_aab = aab 59 | best_bz = bz 60 | daab = .06 * (aabmax - aabmin) 61 | aabmin = max(0, best_aab - daab) 62 | aabmax = min(1, best_aab + daab) 63 | print('%--- best_aab =', best_aab) 64 | return best_bz, best_score 65 | 66 | def cornu_to_cubic(t0, t1, figno): 67 | if figno == 1: 68 | aabmin = 0 69 | aabmax = 0.4 70 | elif figno == 2: 71 | aabmin = 0.5 72 | aabmax = 1. 73 | else: 74 | aabmin = 0 75 | aabmax = 1. 76 | fast = 0 77 | if figno == 3: 78 | fast = 1 79 | elif figno == 4: 80 | fast = 2 81 | def th_fn(s): 82 | return (s + t0) ** 2 83 | y0, x0 = cornu.eval_cornu(t0) 84 | y1, x1 = cornu.eval_cornu(t1) 85 | bz, score = fit_cubic((x0, y0), (x1, y1), t1 - t0, th_fn, fast, aabmin, aabmax) 86 | return bz, score 87 | 88 | def plot_k_of_bz(bz): 89 | dbz = tocubic.bz_deriv(bz) 90 | ddbz = tocubic.bz_deriv(dbz) 91 | cmd = 'moveto' 92 | ss = [0] 93 | def arclength_deriv(x, ss): 94 | dx, dy = tocubic.bz_eval(dbz, x) 95 | return [hypot(dx, dy)] 96 | dt = 0.01 97 | t = 0 98 | for i in range(101): 99 | dx, dy = tocubic.bz_eval(dbz, t) 100 | ddx, ddy = tocubic.bz_eval(ddbz, t) 101 | k = (ddy * dx - dy * ddx) / (dx * dx + dy * dy) ** 1.5 102 | print(100 + 500 * ss[0], 100 + 200 * k, cmd) 103 | cmd = 'lineto' 104 | 105 | dsdx = arclength_deriv(t, ss) 106 | tocubic.rk4(ss, dsdx, t, .01, arclength_deriv) 107 | t += dt 108 | print('stroke') 109 | 110 | def plot_k_nominal(s0, s1): 111 | k0 = 2 * s0 112 | k1 = 2 * s1 113 | print('gsave 0.5 setlinewidth') 114 | print(100, 100 + 200 * k0, 'moveto') 115 | print(100 + 500 * (s1 - s0), 100 + 200 * k1, 'lineto') 116 | print('stroke grestore') 117 | 118 | def simple_bez(): 119 | eps_prologue(95, 126, 552, 508, 0) 120 | tocubic.plot_prolog() 121 | print('/ss 1.5 def') 122 | print('/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def') 123 | bz, score = cornu_to_cubic(.5, 1.1, 2) 124 | fromcubic.plot_bzs([[bz]], (-400, 100), 1000, True) 125 | print('stroke') 126 | print('/Times-Roman 12 selectfont') 127 | print('95 130 moveto ((x0, y0)) show') 128 | print('360 200 moveto ((x1, y1)) show') 129 | print('480 340 moveto ((x2, y2)) show') 130 | print('505 495 moveto ((x3, y3)) show') 131 | print('showpage') 132 | eps_trailer() 133 | 134 | def fast_bez(figno): 135 | if figno == 3: 136 | y1 = 520 137 | else: 138 | y1 = 550 139 | eps_prologue(95, 140, 552, y1, 0) 140 | tocubic.plot_prolog() 141 | print('/ss 1.5 def') 142 | print('/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def') 143 | bz, score = cornu_to_cubic(.5, 1.1, figno) 144 | fromcubic.plot_bzs([[bz]], (-400, 100), 1000, True) 145 | print('stroke') 146 | plot_k_nominal(.5, 1.1) 147 | plot_k_of_bz(bz) 148 | print('showpage') 149 | eps_trailer() 150 | 151 | def bezfig(s1): 152 | eps_prologue(95, 38, 510, 550, 0) 153 | #print '0.5 0.5 scale 500 100 translate' 154 | tocubic.plot_prolog() 155 | print('/ss 1.5 def') 156 | print('/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def') 157 | bz, score = cornu_to_cubic(.5, 0.85, 1) 158 | fromcubic.plot_bzs([[bz]], (-400, 0), 1000, True) 159 | print('stroke') 160 | plot_k_nominal(.5, 0.85) 161 | plot_k_of_bz(bz) 162 | bz, score = cornu_to_cubic(.5, 0.85, 2) 163 | fromcubic.plot_bzs([[bz]], (-400, 100), 1000, True) 164 | print('stroke') 165 | print('gsave 0 50 translate') 166 | plot_k_nominal(.5, .85) 167 | plot_k_of_bz(bz) 168 | print('grestore') 169 | print('showpage') 170 | 171 | import sys 172 | 173 | if __name__ == '__main__': 174 | figno = int(sys.argv[1]) 175 | if figno == 0: 176 | simple_bez() 177 | elif figno == 1: 178 | bezfig(1.0) 179 | elif figno == 2: 180 | bezfig(0.85) 181 | else: 182 | fast_bez(figno) 183 | #fast_bez(4) 184 | -------------------------------------------------------------------------------- /demakein/raphs_curves/pcorn.py: -------------------------------------------------------------------------------- 1 | # Utilities for piecewise cornu representation of curves 2 | 3 | from math import * 4 | 5 | from . import clothoid 6 | from . import cornu 7 | 8 | class Segment: 9 | def __init__(self, z0, z1, th0, th1): 10 | self.z0 = z0 11 | self.z1 = z1 12 | self.th0 = th0 13 | self.th1 = th1 14 | self.compute() 15 | def __repr__(self): 16 | return '[' + repr(self.z0) + repr(self.z1) + ' ' + repr(self.th0) + ' ' + repr(self.th1) + ']' 17 | def compute(self): 18 | dx = self.z1[0] - self.z0[0] 19 | dy = self.z1[1] - self.z0[1] 20 | chord = hypot(dy, dx) 21 | chth = atan2(dy, dx) 22 | k0, k1 = clothoid.solve_clothoid(self.th0, self.th1) 23 | charc = clothoid.compute_chord(k0, k1) 24 | 25 | self.chord = chord 26 | self.chth = chth 27 | self.k0, self.k1 = k0, k1 28 | self.charc = charc 29 | self.arclen = chord / charc 30 | self.thmid = self.chth - self.th0 + 0.5 * self.k0 - 0.125 * self.k1 31 | 32 | self.setup_xy_fresnel() 33 | 34 | def setup_xy_fresnel(self): 35 | k0, k1 = self.k0, self.k1 36 | if k1 == 0: k1 = 1e-6 # hack 37 | if k1 != 0: 38 | sqrk1 = sqrt(2 * abs(k1)) 39 | t0 = (k0 - .5 * k1) / sqrk1 40 | t1 = (k0 + .5 * k1) / sqrk1 41 | (y0, x0) = cornu.eval_cornu(t0) 42 | (y1, x1) = cornu.eval_cornu(t1) 43 | chord_th = atan2(y1 - y0, x1 - x0) 44 | chord = hypot(y1 - y0, x1 - x0) 45 | scale = self.chord / chord 46 | if k1 >= 0: 47 | th = self.chth - chord_th 48 | self.mxx = scale * cos(th) 49 | self.myx = scale * sin(th) 50 | self.mxy = -self.myx 51 | self.myy = self.mxx 52 | else: 53 | th = self.chth + chord_th 54 | self.mxx = scale * cos(th) 55 | self.myx = scale * sin(th) 56 | self.mxy = self.myx 57 | self.myy = -self.mxx 58 | # rotate -chord_th, flip top/bottom, rotate self.chth 59 | self.x0 = self.z0[0] - (self.mxx * x0 + self.mxy * y0) 60 | self.y0 = self.z0[1] - (self.myx * x0 + self.myy * y0) 61 | 62 | def th(self, s): 63 | u = s / self.arclen - 0.5 64 | return self.thmid + (0.5 * self.k1 * u + self.k0) * u 65 | 66 | def xy(self, s): 67 | # using fresnel integrals; polynomial approx might be better 68 | u = s / self.arclen - 0.5 69 | k0, k1 = self.k0, self.k1 70 | if k1 == 0: k1 = 1e-6 # hack 71 | if k1 != 0: 72 | sqrk1 = sqrt(2 * abs(k1)) 73 | t = (k0 + u * k1) / sqrk1 74 | (y, x) = cornu.eval_cornu(t) 75 | return [self.x0 + self.mxx * x + self.mxy * y, 76 | self.y0 + self.myx * x + self.myy * y] 77 | 78 | def find_extrema(self): 79 | # find solutions of th(s) = 0 mod pi/2 80 | # todo: find extra solutions when there's an inflection 81 | th0 = self.thmid + 0.125 * self.k1 - 0.5 * self.k0 82 | th1 = self.thmid + 0.125 * self.k1 + 0.5 * self.k0 83 | twooverpi = 2 / pi 84 | n0 = int(floor(th0 * twooverpi)) 85 | n1 = int(floor(th1 * twooverpi)) 86 | if th1 > th0: signum = 1 87 | else: signum = -1 88 | result = [] 89 | for i in range(n0, n1, signum): 90 | th = pi/2 * (i + 0.5 * (signum + 1)) 91 | a = .5 * self.k1 92 | b = self.k0 93 | c = self.thmid - th 94 | if a == 0: 95 | u1 = -c/b 96 | u2 = 1000 97 | else: 98 | sqrtdiscrim = sqrt(b * b - 4 * a * c) 99 | u1 = (-b - sqrtdiscrim) / (2 * a) 100 | u2 = (-b + sqrtdiscrim) / (2 * a) 101 | if u1 >= -0.5 and u1 < 0.5: 102 | result.append(self.arclen * (u1 + 0.5)) 103 | if u2 >= -0.5 and u2 < 0.5: 104 | result.append(self.arclen * (u2 + 0.5)) 105 | return result 106 | 107 | class Curve: 108 | def __init__(self, segs): 109 | self.segs = segs 110 | self.compute() 111 | def compute(self): 112 | arclen = 0 113 | sstarts = [] 114 | for seg in self.segs: 115 | sstarts.append(arclen) 116 | arclen += seg.arclen 117 | 118 | self.arclen = arclen 119 | self.sstarts = sstarts 120 | def th(self, s, deltas = False): 121 | u = s / self.arclen 122 | s = self.arclen * (u - floor(u)) 123 | if s == 0 and not deltas: s = self.arclen 124 | i = 0 125 | while i < len(self.segs) - 1: 126 | # binary search would make a lot of sense here 127 | snext = self.sstarts[i + 1] 128 | if s < snext or (not deltas and s == snext): 129 | break 130 | i += 1 131 | return self.segs[i].th(s - self.sstarts[i]) 132 | def xy(self, s): 133 | u = s / self.arclen 134 | s = self.arclen * (u - floor(u)) 135 | i = 0 136 | while i < len(self.segs) - 1: 137 | # binary search would make a lot of sense here 138 | if s <= self.sstarts[i + 1]: 139 | break 140 | i += 1 141 | return self.segs[i].xy(s - self.sstarts[i]) 142 | def find_extrema(self): 143 | result = [] 144 | for i in range(len(self.segs)): 145 | seg = self.segs[i] 146 | for s in seg.find_extrema(): 147 | result.append(s + self.sstarts[i]) 148 | return result 149 | def find_breaks(self): 150 | result = [] 151 | for i in range(len(self.segs)): 152 | pseg = self.segs[(i + len(self.segs) - 1) % len(self.segs)] 153 | seg = self.segs[i] 154 | th = clothoid.mod_2pi(pseg.chth + pseg.th1 - (seg.chth - seg.th0)) 155 | print('% pseg', pseg.chth + pseg.th1, 'seg', seg.chth - seg.th0) 156 | pisline = pseg.k0 == 0 and pseg.k1 == 0 157 | sisline = seg.k0 == 0 and seg.k1 == 0 158 | if fabs(th) > 1e-3 or (pisline and not sisline) or (sisline and not pisline): 159 | result.append(self.sstarts[i]) 160 | return result 161 | -------------------------------------------------------------------------------- /backyard/mill/old/art.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Tools for constructing depth maps. 4 | 5 | """ 6 | 7 | import numpy, math, sys 8 | from PIL import Image 9 | 10 | import raster 11 | 12 | def load_mask(filename): 13 | #return numpy.asarray(Image.open(filename),'uint8')[::-1,:,3] >= 128 14 | im = Image.open(filename) 15 | data = im.tostring()[3::4] 16 | return numpy.fromstring(data,'uint8').reshape((im.size[1],im.size[0]))[::-1,:] >= 128 17 | 18 | def _upscan(f): 19 | for i, fi in enumerate(f): 20 | if fi == numpy.inf: continue 21 | for j in range(1,i+1): 22 | x = fi+j*j 23 | if f[i-j] < x: break 24 | f[i-j] = x 25 | 26 | def distance2_transform(bitmap): 27 | f = numpy.where(bitmap, 0, min(bitmap.shape[0],bitmap.shape[1])**2) 28 | for i in range(f.shape[0]): 29 | _upscan(f[i,:]) 30 | _upscan(f[i,::-1]) 31 | for i in range(f.shape[1]): 32 | _upscan(f[:,i]) 33 | _upscan(f[::-1,i]) 34 | return f 35 | 36 | 37 | 38 | 39 | def bulge1(mask): 40 | ysize,xsize = mask.shape 41 | height = numpy.zeros(mask.shape,'int16') 42 | height[mask] = max(mask.shape[0],mask.shape[1]) 43 | 44 | def apply(points): 45 | radius = math.sqrt((points[0][0]-points[-1][0])**2+ 46 | (points[0][1]-points[-1][1])**2)*0.5+0.5 47 | midx = (points[0][0]+points[-1][0])*0.5 48 | midy = (points[0][1]+points[-1][1])*0.5 49 | for x,y in points: 50 | d = math.sqrt(radius*radius-(x-midx)**2-(y-midy)**2) 51 | height[y,x] = min(height[y,x], d) 52 | 53 | def doline(x0,y0,x1,y1): 54 | points = raster.line_points2(x0,y0,x1,y1) 55 | begin = 0 56 | for i in range(len(points)): 57 | if ( points[i][0] < 0 or points[i][1] < 0 or 58 | points[i][0] >= xsize or points[i][1] >= ysize or 59 | not mask[points[i][1],points[i][0]] ): 60 | if begin != i: 61 | apply(points[begin:i]) 62 | begin = i + 1 63 | if begin != len(points): 64 | apply(points[begin:]) 65 | 66 | #print points 67 | 68 | #for x in xrange(mask.shape[1]): 69 | # doline(x,0,x,mask.shape[0]-1) 70 | #for y in xrange(mask.shape[0]): 71 | # doline(0,y,mask.shape[1]-1,y) 72 | 73 | n = 32 74 | for i in range(n): 75 | a = i*math.pi/n 76 | dx = math.cos(a) 77 | dy = math.sin(a) 78 | if abs(dx) > abs(dy): 79 | scale = dx/xsize 80 | dx = xsize 81 | dy = dy/scale 82 | for y in range(int(-abs(dy)),ysize+int(abs(dy))): 83 | doline(0,y,dx,y+dy) 84 | else: 85 | scale = dy/ysize 86 | dx = dx/scale 87 | dy = ysize 88 | for x in range(int(-abs(dx)),xsize+int(abs(dx))): 89 | doline(x,0,x+dx,dy) 90 | print(dx, dy) 91 | 92 | return height 93 | 94 | def bulge(mask): 95 | ysize,xsize = mask.shape 96 | 97 | print('Distance2...') 98 | dist2 = distance2_transform(numpy.logical_not(mask)) 99 | 100 | points = [ ] 101 | radius = min(mask.shape[0],mask.shape[1])//2 102 | for x in range(-radius,radius+1): 103 | for y in range(-radius,radius+1): 104 | points.append((x*x+y*y,x,y)) 105 | points.sort() 106 | points = numpy.array(points) 107 | 108 | height = numpy.zeros(mask.shape,'int16') 109 | for y in range(mask.shape[0]): 110 | sys.stdout.write('\r%d '%(mask.shape[0]-y)) 111 | sys.stdout.flush() 112 | for x in range(mask.shape[1]): 113 | #max_d2 = 0 114 | #for d2,ox,oy in points: 115 | # x2 = x+ox 116 | # y2 = y+oy 117 | # if (x2 < 0 or x2 >= xsize or y2 < 0 or y2 >= ysize or not mask[y2,x2]): 118 | # break 119 | # max_d2 = d2 120 | max_d2 = dist2[y,x] 121 | if not max_d2: continue 122 | 123 | dom = False 124 | for ox,oy in [(-1,0),(1,0),(0,-1),(0,1)]: 125 | if x+ox < 0 or y+oy < 0 or x+ox >= xsize or y+oy >= ysize: 126 | continue 127 | if dist2[y,x] < dist2[y+oy,x+ox]: 128 | dom = True 129 | break 130 | 131 | 132 | for d2,ox,oy in points: 133 | if d2 >= max_d2: break 134 | x2 = x+ox 135 | y2 = y+oy 136 | #if d2 and math.sqrt(dist2[y2,x2])+0.001 >= math.sqrt(max_d2)+math.sqrt(d2): 137 | # break 138 | height[y2,x2] = max(height[y2,x2], int(math.sqrt(max_d2-ox*ox-oy*oy)+0.5)) 139 | sys.stdout.write('\r \r') 140 | return height 141 | 142 | 143 | def centroid(thing): 144 | tot = 0.0 145 | totx = 0.0 146 | toty = 0.0 147 | for y in range(thing.shape[0]): 148 | for x in range(thing.shape[1]): 149 | tot += thing[y,x] 150 | totx += x*thing[y,x] 151 | toty += y*thing[y,x] 152 | return totx/tot, toty/tot 153 | 154 | 155 | def save_raster(filename, res, depth): 156 | raster.save(dict(res=res,raster=depth.tostring(),shape=depth.shape,dtype=str(depth.dtype)), filename+'.raster') 157 | 158 | with open(filename+'.stl','wb') as f: 159 | def vert(x,y): 160 | print('vertex %f %f %f' % (-float(x)/res,float(y)/res,-float(depth[y,x])/res), file=f) 161 | print('solid', file=f) 162 | step = max(1,min(depth.shape[0],depth.shape[1])//200) 163 | for x in range(0,depth.shape[1]-step*2+1,step): 164 | for y in range(0,depth.shape[0]-step*2+1,step): 165 | print('facet normal 0 0 0', file=f) 166 | print('outer loop', file=f) 167 | vert(x,y) 168 | vert(x+step,y+step) 169 | vert(x+step,y) 170 | print('endloop', file=f) 171 | print('endfacet', file=f) 172 | print('facet normal 0 0 0', file=f) 173 | print('outer loop', file=f) 174 | vert(x,y) 175 | vert(x,y+step) 176 | vert(x+step,y+step) 177 | print('endloop', file=f) 178 | print('endfacet', file=f) 179 | print('endsolid', file=f) 180 | 181 | 182 | #depth = numpy.zeros(mask.shape,'int16') 183 | #depth[mask] = -res*10 184 | 185 | #mask = load_mask('/tmp/exp.png') 186 | # 187 | #print mask.shape 188 | #res = 10 189 | #print res 190 | # 191 | #depth = -bulge(mask) 192 | # 193 | #raster.save(dict(res=res,raster=depth), '/tmp/exp.raster') 194 | 195 | # python raster.py view /tmp/exp.raster 196 | # python raster.py cut /tmp/exp wood-ball /tmp/exp.raster 197 | # python decode.py 3 plot /tmp/exp-wood-ball.prn 198 | -------------------------------------------------------------------------------- /demakein/raphs_curves/mvc.py: -------------------------------------------------------------------------------- 1 | from math import * 2 | import array 3 | import sys 4 | import random 5 | 6 | def run_mvc(k, k1, k2, k3, C, n = 100, do_print = False): 7 | cmd = 'moveto' 8 | result = array.array('d') 9 | cost = 0 10 | th = 0 11 | x = 0 12 | y = 0 13 | dt = 1.0 / n 14 | for i in range(n): 15 | k4 = -k * (k * k2 - .5 * k1 * k1 + C) 16 | 17 | cost += dt * k1 * k1 18 | x += dt * cos(th) 19 | y += dt * sin(th) 20 | th += dt * k 21 | 22 | k += dt * k1 23 | k1 += dt * k2 24 | k2 += dt * k3 25 | k3 += dt * k4 26 | result.append(k) 27 | if do_print: print(400 + 400 * x, 500 + 400 * y, cmd) 28 | cmd = 'lineto' 29 | return result, cost, x, y, th 30 | 31 | def run_mec_cos(k, lam1, lam2, n = 100, do_print = False): 32 | cmd = 'moveto' 33 | result = array.array('d') 34 | cost = 0 35 | th = 0 36 | x = 0 37 | y = 0 38 | dt = 1.0 / n 39 | for i in range(n): 40 | k1 = lam1 * cos(th) + lam2 * sin(th) 41 | 42 | cost += dt * k * k 43 | x += dt * cos(th) 44 | y += dt * sin(th) 45 | th += dt * k 46 | 47 | k += dt * k1 48 | result.append(k) 49 | if do_print: print(400 + 400 * x, 500 + 400 * y, cmd) 50 | cmd = 'lineto' 51 | return result, cost, x, y, th 52 | 53 | def descend(params, fnl): 54 | delta = 1 55 | for i in range(100): 56 | best = fnl(params, i, True) 57 | bestparams = params 58 | for j in range(2 * len(params)): 59 | ix = j / 2 60 | sign = 1 - 2 * (ix & 1) 61 | newparams = params[:] 62 | newparams[ix] += delta * sign 63 | new = fnl(newparams, i) 64 | if (new < best): 65 | bestparams = newparams 66 | best = new 67 | if (bestparams == params): 68 | delta *= .5 69 | print('%', params, delta) 70 | sys.stdout.flush() 71 | params = bestparams 72 | return bestparams 73 | 74 | def descend2(params, fnl): 75 | delta = 20 76 | for i in range(5): 77 | best = fnl(params, i, True) 78 | bestparams = params 79 | for j in range(100000): 80 | newparams = params[:] 81 | for ix in range(len(params)): 82 | newparams[ix] += delta * (2 * random.random() - 1) 83 | new = fnl(newparams, i) 84 | if (new < best): 85 | bestparams = newparams 86 | best = new 87 | if (bestparams == params): 88 | delta *= .5 89 | params = bestparams 90 | print('%', params, best, delta) 91 | sys.stdout.flush() 92 | return bestparams 93 | 94 | def desc_eval(params, dfdp, fnl, i, x): 95 | newparams = params[:] 96 | for ix in range(len(params)): 97 | newparams[ix] += x * dfdp[ix] 98 | return fnl(newparams, i) 99 | 100 | def descend3(params, fnl): 101 | dp = 1e-6 102 | for i in range(1000): 103 | base = fnl(params, i, True) 104 | dfdp = [] 105 | for ix in range(len(params)): 106 | newparams = params[:] 107 | newparams[ix] += dp 108 | new = fnl(newparams, i) 109 | dfdp.append((new - base) / dp) 110 | print('% dfdp = ', dfdp) 111 | xr = 0. 112 | yr = base 113 | xm = -1e-3 114 | ym = desc_eval(params, dfdp, fnl, i, xm) 115 | if ym > yr: 116 | while ym > yr: 117 | xl, yl = xm, ym 118 | xm = .618034 * xl 119 | ym = desc_eval(params, dfdp, fnl, i, xm) 120 | else: 121 | xl = 1.618034 * xm 122 | yl = desc_eval(params, dfdp, fnl, i, xl) 123 | while ym > yl: 124 | xm, ym = xl, yl 125 | xl = 1.618034 * xm 126 | yl = desc_eval(params, dfdp, fnl, i, xl) 127 | 128 | # We have initial bracket; ym < yl and ym < yr 129 | 130 | x0, x3 = xl, xr 131 | if abs(xr - xm) > abs(xm - xl): 132 | x1, y1 = xm, ym 133 | x2 = xm + .381966 * (xr - xm) 134 | y2 = desc_eval(params, dfdp, fnl, i, x2) 135 | else: 136 | x2, y2 = xm, ym 137 | x1 = xm + .381966 * (xl - xm) 138 | y1 = desc_eval(params, dfdp, fnl, i, x1) 139 | for j in range(30): 140 | if y2 < y1: 141 | x0, x1, x2 = x1, x2, x2 + .381966 * (x3 - x2) 142 | y0, y1 = y1, y2 143 | y2 = desc_eval(params, dfdp, fnl, i, x2) 144 | else: 145 | x1, x2, x3 = x1 + .381966 * (x0 - x1), x1, x2 146 | y1 = desc_eval(params, dfdp, fnl, i, x1) 147 | if y1 < y2: 148 | xbest = x1 149 | ybest = y1 150 | else: 151 | xbest = x2 152 | ybest = y2 153 | for ix in range(len(params)): 154 | params[ix] += xbest * dfdp[ix] 155 | print('%', params, xbest, ybest) 156 | sys.stdout.flush() 157 | return params 158 | 159 | def mk_mvc_fnl(th0, th1): 160 | def fnl(params, i, do_print = False): 161 | k, k1, k2, k3, C = params 162 | ks, cost, x, y, th = run_mvc(k, k1, k2, k3, C, 100) 163 | cost *= hypot(y, x) ** 3 164 | actual_th0 = atan2(y, x) 165 | actual_th1 = th - actual_th0 166 | if do_print: print('%', x, y, actual_th0, actual_th1, cost) 167 | err = (th0 - actual_th0) ** 2 + (th1 - actual_th1) ** 2 168 | multiplier = 1000 169 | return cost + err * multiplier 170 | return fnl 171 | 172 | def mk_mec_fnl(th0, th1): 173 | def fnl(params, i, do_print = False): 174 | k, lam1, lam2 = params 175 | ks, cost, x, y, th = run_mec_cos(k, lam1, lam2) 176 | cost *= hypot(y, x) 177 | actual_th0 = atan2(y, x) 178 | actual_th1 = th - actual_th0 179 | if do_print: print('%', x, y, actual_th0, actual_th1, cost) 180 | err = (th0 - actual_th0) ** 2 + (th1 - actual_th1) ** 2 181 | multiplier = 10 182 | return cost + err * multiplier 183 | return fnl 184 | 185 | #ks, cost, x, y, th = run_mvc(0, 10, -10, 10, 200) 186 | #print '%', cost, x, y 187 | #print 'stroke showpage' 188 | 189 | def mvc_test(): 190 | fnl = mk_mvc_fnl(-pi, pi/4) 191 | params = [0, 0, 0, 0, 0] 192 | params = descend3(params, fnl) 193 | k, k1, k2, k3, C = params 194 | run_mvc(k, k1, k2, k3, C, 100, True) 195 | print('stroke showpage') 196 | print('%', params) 197 | 198 | def mec_test(): 199 | th0, th1 = pi/2, pi/2 200 | fnl = mk_mec_fnl(th0, th1) 201 | params = [0, 0, 0] 202 | params = descend2(params, fnl) 203 | k, lam1, lam2 = params 204 | run_mec_cos(k, lam1, lam2, 1000, True) 205 | print('stroke showpage') 206 | print('%', params) 207 | 208 | mvc_test() 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Demakein 3 | 4 | https://www.logarithmic.net/pfh/design 5 | 6 | Paul Harrison - paul.francis.harrison@gmail.com 7 | 8 | 9 | Demakein is a set of Python 3 tools for designing and making woodwind instruments. 10 | 11 | This generally consists of two stages: 12 | 13 | - The "design" stage is a numerical optimization that chooses the bore shape and the finger hole placement, size, and depth necessary for the instrument to produce the correct notes for a given set of fingerings. 14 | 15 | - The "make" stage takes a design and turns it into a 3D object, then then cuts the object into pieces that can be CNC-milled or 3D-printed. 16 | 17 | Demakein can either be used via the command "demakein" or as a library in Python. Demakein has been designed to be extensible, and I hope you will find it relatively easy to write code to create your own novel instruments. You can either create subclasses of existing classes in order to tweak a few parameters, or create wholly new classes using existing examples as a template. 18 | 19 | 20 | ## Using Demakein with uv (recommended) 21 | 22 | [uv](https://docs.astral.sh/uv/) is currently (2025) the easiest way to install and use Python 3 packages. Once uv is installed, the `uvx` command will let you run Demakein from the command line, ideally using PyPy for speed: 23 | 24 | ``` 25 | uvx -p pypy demakein 26 | ``` 27 | 28 | To use the GitHub version of Demakein: 29 | 30 | ``` 31 | uvx -p pypy git+https://github.com/pfh/demakein 32 | ``` 33 | 34 | uv also makes it easy to use Demakein as a dependency in your own scripts, if you want to develop your own instruments. 35 | 36 | 37 | ## Installing Demakein 38 | 39 | You will need Python 3. Ideally install Demakein in a virtual environment. Demakein can be installed using pip: 40 | 41 | ``` 42 | pip install demakein 43 | ``` 44 | 45 | Hopefully the necessary dependencies install without problems. Please email me if you run into trouble. 46 | 47 | You can then run program by typing: 48 | 49 | ``` 50 | demakein 51 | ``` 52 | 53 | or: 54 | 55 | ``` 56 | python3 -m demakein 57 | ``` 58 | 59 | 60 | ## Using PyPy 61 | 62 | Using PyPy is your Python interpreter will let Demakein run considerably faster. 63 | 64 | 65 | 66 | ## Development version 67 | 68 | I develop Demakein using "hatch". Install hatch using one of the methods listed [here](https://hatch.pypa.io/latest/install/). 69 | 70 | ``` 71 | git clone https://github.com/pfh/demakein.git 72 | cd demakein 73 | 74 | # To enter a virtual environment using pypy 75 | hatch shell pypy 76 | 77 | # To enter a virtual environment using your system python 78 | hatch shell 79 | 80 | # To build source distribution and wheel 81 | hatch build 82 | # or 83 | python3 -m build 84 | ``` 85 | 86 | 87 | ## Examples 88 | 89 | Create a small flute: 90 | 91 | ``` 92 | demakein design-folk-flute: myflute --transpose 12 93 | 94 | demakein make-flute: myflute 95 | ``` 96 | 97 | Files are created in a directory called myflute. 98 | 99 | We've just made STL files for 3D printing. How about if we want to CNC-mill the flute? 100 | 101 | ``` 102 | demakein make-flute: myflute --mill yes --open yes --prefix milling 103 | ``` 104 | 105 | 106 | ## Adding new types of instrument 107 | 108 | If you want to create your own custom instruments, you can create subclasses of the instruments provided. Some examples of how to do this can be found in the "examples" directory. Use these as a starting point. 109 | 110 | Instrument design tools are subclasses of demakein.design.Instrument_designer. These tools define a set of class attributes that constrain the instrument design, listed below: 111 | 112 | ``` 113 | closed_top 114 | - bool 115 | Is the top of the instrument closed? 116 | Reeds and brass-style mouthpieces are effectively closed. 117 | A ney has an open end. 118 | A flute might be approximated as an open end, or the embouchure 119 | hole treated as a hole and the end set to closed. 120 | See examples/simple_reedpipe.py for an example with closed_top=True. 121 | See examples/simple_flute.py for an example with closed_top=False. 122 | 123 | inital_length 124 | - float 125 | Length of the instrument at the start of the optimization. 126 | Automatically adjusted based on --transpose parameter. 127 | Just provide a roughly reasonable value, 128 | eg using demakein.design.wavelength function 129 | 130 | n_holes 131 | - int 132 | Number of finger holes. 133 | 134 | fingerings 135 | - list of tuples (note, [ 0/1,... ]) 136 | Desired fingering patterns to produce each desired note. 137 | is automatically adjusted by --transpose parameter. 138 | The list starts from the bottom of the instrument. 139 | Not all fingering schemes are physically possible, 140 | this may require some experimentation. 141 | 142 | max_hole_diameters 143 | - list of n_holes floats 144 | Maximum allowed finger hole diameters. 145 | 146 | min_hole_diameters 147 | - list of n_holes floats 148 | Minimum allowed finger hole diameters. 149 | 150 | min_hole_spacing 151 | - list of n_holes-1 floats 152 | Minimum space between finger holes in mm. 153 | 154 | top_clearance_fraction 155 | bottom_clearance_fraction 156 | - Minimum distance of top/bottom hole from top/bottom of instrument, 157 | as a fraction of the instrument length. 158 | 159 | balance 160 | - list of n_holes-2 floats or Nones 161 | Values should be in the range zero to one. 162 | Smaller values force the spacing between successive holes to be more similar. 163 | 164 | hole_angles 165 | - list of n_holes floats 166 | Vertical angle of each hole. 167 | Angled holes may allow more comfortable hole spacing. 168 | 169 | inner_diameters 170 | - list of floats [advanced: or tuples (low,high)] 171 | The first element is the bore diameter at the base of the instrument. 172 | The last element is the bore diameter at the top of the instrument. 173 | The bore is piecewise linear, 174 | intervening elements are bore diameters boundaries between pieces (kinks). 175 | Exact placement is subject to numerical optimization. 176 | 177 | Advanced: 178 | Instead of a single diameter, you can give a tuple (low,high) 179 | to create a step in the diameter of the bore. 180 | See the examples/stepped_shawm.py for an example of this. 181 | 182 | initial_inner_fractions 183 | - list of len(inner_diameters)-2 floats 184 | Initial positions of kinks in the bore. 185 | 186 | min_inner_fraction_sep 187 | - list of len(inner_diameters)-1 floats 188 | Minimum size of each linear segment of the bore, 189 | as a fraction of the overall length. 190 | 191 | outer_diameters 192 | initial_outer_fractions 193 | min_outer_fraction_sep 194 | - As for inner_diameters, 195 | but defining the shape of the outside of the instrument 196 | (hence the depth of each finger hole). 197 | 198 | outer_add 199 | - bool, default False 200 | Optionally the outside diameters of the instrument can be defined 201 | as being in addition to the bore diameters rather than 202 | independent of them. 203 | See examples/simple_shawm.py for an example of this. 204 | ``` 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /demakein/optimize.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, math, random, bisect, multiprocessing, signal, time 3 | 4 | from . import legion, grace 5 | 6 | #def status(*items): 7 | # """ Display a status string. """ 8 | # string = ' '.join( str(item) for item in items ) 9 | # if sys.stderr.isatty(): 10 | # sys.stderr.write('\r \x1b[K\x1b[34m' + string + '\x1b[m\r') 11 | # sys.stderr.flush() 12 | 13 | 14 | def worker(scorer, fut): 15 | while True: 16 | value = legion.coordinator().get_future(fut) 17 | if value is None: 18 | break 19 | 20 | item, reply_fut = value 21 | result = scorer(item) 22 | fut = legion.coordinator().new_future() 23 | legion.coordinator().deliver_future(reply_fut, (result, fut)) 24 | 25 | 26 | def make_update(vecs, initial_accuracy, do_noise): 27 | do_noise = do_noise or random.random() < 0.1 28 | 29 | #vecs = random.sample(vecs,min(100,len(vecs))) 30 | 31 | n = len(vecs) 32 | m = len(vecs[0]) 33 | 34 | #mean = [ 35 | # sum([ vec[i] for vec in vecs ])/n 36 | # for i in xrange(m) 37 | # ] 38 | 39 | #weight_weight = (1.25+random.random()) / (n**0.5) 40 | weight_weight = (1.0+2.0*random.random()) / (n**0.5) 41 | #weight_weight = (1.0) / (n**0.5) 42 | weights = [ 43 | random.normalvariate(0.0, weight_weight) 44 | for i in range(n) 45 | ] 46 | 47 | offset = (0.0-sum(weights)) / n 48 | weights = [ weight+offset for weight in weights ] 49 | weights[ random.randrange(n) ] += 1.0 50 | 51 | update = [ 52 | sum( vecs[j][i]*weights[j] for j in range(n) ) 53 | for i in range(m) 54 | ] 55 | 56 | #update = [ 57 | # sum( 58 | # (vecs[j][i]-mean[i])*weights[j] 59 | # for j in xrange(n) 60 | # ) + mean[i] 61 | # for i in xrange(m) 62 | # ] 63 | 64 | if do_noise: 65 | extra = random.random() * initial_accuracy 66 | update = [ 67 | val+random.normalvariate(0.0, extra) 68 | for val in update 69 | ] 70 | 71 | return update 72 | 73 | 74 | def show_status(line): 75 | print("\r\033[K\r" + line, end="") 76 | sys.stdout.flush() 77 | 78 | def improve(comment, constrainer, scorer, start_x, ftol=1e-4, xtol=1e-6, initial_accuracy=0.001, pool_factor=5, workers=1, monitor = lambda x,y: None): 79 | serial = (workers <= 1) 80 | if not serial: 81 | worker_futs = [ legion.coordinator().new_future() for i in range(workers) ] 82 | reply_futs = [ ] 83 | 84 | workers = [ 85 | legion.future(worker,scorer,fut) 86 | for fut in worker_futs 87 | ] 88 | 89 | last_t = 0.0 90 | best = start_x 91 | c_score = constrainer(best) 92 | if c_score: 93 | best_score = (c_score, 0.0) 94 | else: 95 | best_score = (0.0, scorer(best)) 96 | 97 | n = 0 98 | n_good = 0 99 | n_real = 0 100 | n_real_since_best = 0 # A way to at some point give up 101 | jobs = [ ] 102 | 103 | pool_size = int(len(best)*pool_factor) #5 104 | print(len(best),'parameters, pool size', pool_size) 105 | 106 | currents = [ (best, best_score) ] 107 | 108 | done = False 109 | while not done or (not serial and reply_futs): 110 | t = time.time() 111 | if t > last_t+20.0: 112 | def rep(x): 113 | if x[0]: return 'C%.6f' % x[0] 114 | return '%.6f' % x[1] 115 | status = f"Optimizing {comment} best={rep(best_score)} worst={rep(max(item[1] for item in currents))} pool={len(currents)} n_good={n_good} n_real={n_real} n={n}" 116 | show_status(status) 117 | 118 | if best_score[0] == 0: 119 | monitor(best, [ item[0] for item in currents ]) 120 | last_t = time.time() 121 | 122 | have_score = False 123 | 124 | if not done and (serial or worker_futs): 125 | new = make_update([item[0] for item in currents], initial_accuracy, len(currents) < pool_size) 126 | 127 | c_score = constrainer(new) 128 | if c_score: 129 | have_score = True 130 | new_score = (c_score, 0.0) 131 | elif workers == 1: 132 | have_score = True 133 | new_score = (0.0, scorer(new)) 134 | else: 135 | reply_fut = legion.coordinator().new_future() 136 | worker_fut = worker_futs.pop(0) 137 | legion.coordinator().deliver_future(worker_fut, (new, reply_fut)) 138 | reply_futs.append( (new, reply_fut) ) 139 | 140 | if not have_score: 141 | if not reply_futs or (not done and worker_futs): 142 | continue 143 | new, reply_fut = reply_futs.pop(0) 144 | new_score, worker_fut = legion.coordinator().get_future(reply_fut) 145 | new_score = (0.0, new_score) 146 | worker_futs.append(worker_fut) 147 | 148 | n += 1 149 | if new_score[0] == 0.0: 150 | n_real += 1 151 | n_real_since_best += 1 152 | 153 | l = sorted( item[1][1] for item in currents ) 154 | if pool_size < len(l): 155 | c = l[pool_size] 156 | else: 157 | c = 1e30 158 | cutoff = (best_score[0], c) 159 | 160 | if new_score <= cutoff: 161 | currents = [ item for item in currents if item[1] <= cutoff ] 162 | currents.append((new,new_score)) 163 | 164 | n_good += 1 165 | 166 | if new_score < best_score: 167 | best_score = new_score 168 | best = new 169 | n_real_since_best = 0 170 | 171 | if len(currents) >= pool_size and best_score[0] == 0.0: 172 | xspan = 0.0 173 | for i in range(len(start_x)): 174 | xspan = max(xspan, 175 | max(item[0][i] for item in currents) - 176 | min(item[0][i] for item in currents) 177 | ) 178 | 179 | fspan = (max(item[1] for item in currents)[1]-best_score[1]) 180 | 181 | if xspan < xtol or (n_good >= 5000 and fspan < ftol): 182 | done = True 183 | 184 | # Give up if completely stuck 185 | if n_real_since_best >= 1000000: 186 | show_status('') 187 | print("No improvement in 1,000,000 tries.") 188 | done = True 189 | 190 | show_status('') 191 | print(f"Optimized {comment} best={best_score[1]:.5f}") 192 | 193 | if not serial: 194 | while worker_futs: 195 | fut = worker_futs.pop(0) 196 | legion.coordinator().deliver_future(fut, None) 197 | 198 | for item in workers: 199 | item() 200 | 201 | return best 202 | 203 | -------------------------------------------------------------------------------- /demakein/make_flute.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, os, math 3 | 4 | from . import config, design, make, design_flute, profile, shape, pack 5 | 6 | 7 | @config.help("""\ 8 | Produce 3D models using the output of "demakein design-*-flute:" 9 | """) 10 | @config.Bool_flag('open', 'Open both ends, thus requiring a cork.') 11 | #@config.Bool_flag('mill', 'Create shapes for milling (rather than 3D printing).') 12 | #@config.Float_flag('mill_diameter', 'Milling: Bit diameter for milling (affects gap size when packing pieces).') 13 | #@config.Float_flag('mill_length', 'Milling: Wood length for milling.') 14 | #@config.Float_flag('mill_width', 'Milling: Wood width for milling.') 15 | #@config.Float_flag('mill_thickness', 'Milling: Wood thickness for milling.') 16 | #@config.Int_flag('mill_scheme', 'Milling: Division scheme for milling.\nValid values: 2 3 4') 17 | @config.Float_flag('emb_squareness', 'Squareness of embouchure hole, larger = squarer.') 18 | @config.Float_flag('emb_aspect', 'Aspect ratio of embouchure hole, 1 = square, larger = wider.') 19 | @config.Bool_flag('decorate', 'Add some decorations') 20 | class Make_flute(make.Make_millable_instrument): 21 | #mill = False 22 | open = False 23 | 24 | #mill_diameter = 3.0 25 | #mill_length = 180.0 26 | #mill_width = 130.0 27 | #mill_thickness = 19.0 28 | #mill_scheme = 4 29 | 30 | emb_squareness = 0.0 31 | emb_aspect = 1.5 32 | decorate = False 33 | 34 | #SCHEMES = { 35 | # 2 : [ 36 | # [ 0.0, 0.6, 1.0 ], 37 | # [ 0.0, 0.4, 1.0 ], 38 | # ], 39 | # 40 | # 3 : [ 41 | # [ 0.0, 0.3, 0.6, 1.0 ], 42 | # [ 0.0, 0.4, 0.7, 1.0 ], 43 | # ], 44 | # 45 | # 4 : [ 46 | # [ 0.0, 0.2, 0.45, 0.7, 1.0 ], 47 | # [ 0.0, 0.3, 0.55, 0.8, 1.0 ], 48 | # ], 49 | #} 50 | 51 | def run(self): 52 | working = self.working 53 | designer = working.designer 54 | spec = working.spec 55 | workspace = self.get_workspace() 56 | 57 | length = spec.length * 1.05 # Extend a bit to allow cork. 58 | 59 | if self.open: 60 | inner_profile = spec.inner.clipped(-50,length+50) 61 | 62 | cork_length = length - spec.length 63 | cork_diameter = spec.inner(spec.length) 64 | with open(workspace/(self.prefix+'cork.txt'),'wt') as f: 65 | f.write('Cork length %.1fmm\n' % cork_length) 66 | f.write('Cork diameter %.1fmm\n' % cork_diameter) 67 | else: 68 | inner_profile = spec.inner.clipped(-50,spec.length) 69 | 70 | width = max(spec.outer.low) 71 | 72 | outer_profile = spec.outer.clipped(0,length) 73 | if self.decorate: 74 | emfrac = 1.0-spec.hole_positions[-1]/length 75 | for frac, align in [ (1.0-emfrac*2,1.0), (1.0,-1.0) ]: 76 | dpos = length * frac 77 | damount = outer_profile(dpos)*0.1 78 | dpos += damount * align 79 | deco_profile = profile.Profile( 80 | [ dpos+damount*i for i in [-1,-0.333,0.333,1]], 81 | [ damount*i for i in [0,1,1,0] ], 82 | ) 83 | outer_profile = outer_profile + deco_profile 84 | 85 | 86 | if self.emb_aspect > 1.0: 87 | emb_xpad = self.emb_squareness 88 | emb_ypad = (emb_xpad+1.0)*self.emb_aspect-1.0 89 | else: 90 | emb_ypad = self.emb_squareness 91 | emb_xpad = (emb_ypad+1.0)/self.emb_aspect-1.0 92 | 93 | self.make_instrument( 94 | inner_profile=inner_profile, outer_profile=outer_profile, 95 | hole_positions=spec.hole_positions, hole_diameters=spec.hole_diameters, hole_vert_angles=spec.hole_angles, 96 | #hole_horiz_angles=[0.0]*7, 97 | hole_horiz_angles=designer.hole_horiz_angles, 98 | xpad=[0.0]*(designer.n_holes-1)+[emb_xpad], 99 | ypad=[0.0]*(designer.n_holes-1)+[emb_ypad], 100 | with_fingerpad=[True]*6+[False]) 101 | 102 | #instrument = shape.extrude_profile(outer_profile) 103 | #outside = shape.extrude_profile(outer_profile) 104 | # 105 | #if self.open: 106 | # bore = shape.extrude_profile(spec.inner.clipped(-50,length+50)) 107 | #else: 108 | # bore = shape.extrude_profile(spec.inner.clipped(-50,spec.length)) 109 | # 110 | ##xpad = [ .25,.25,0,0,0,0 ] + [ 0.0 ] 111 | ##ypad = [ .25,.25,0,0,0,0 ] + [ 0.75 ] 112 | #xpad = [ 0,0,0,0,0,0 ] + [ 0.0 ] 113 | #ypad = [ 0,0,0,0,0,0 ] + [ 0.75 ] 114 | # 115 | # 116 | # 117 | #for i, pos in enumerate(spec.hole_positions): 118 | # angle = spec.hole_angles[i] 119 | # radians = angle*math.pi/180.0 120 | # 121 | # height = spec.outer(pos)*0.5 122 | # shift = math.sin(radians) * height 123 | # 124 | # hole_length = ( 125 | # math.sqrt(height*height+shift*shift) + 126 | # spec.hole_diameters[i]*0.5*abs(math.sin(radians)) + 127 | # 4.0 128 | # ) 129 | # hole = shape.prism( 130 | # hole_length, spec.hole_diameters[i], 131 | # shape.squared_circle(xpad[i], ypad[i]).with_effective_diameter 132 | # ) 133 | # hole.rotate(1,0,0, -90-angle) 134 | # hole.move(0,0,pos + shift) 135 | # bore.add(hole) 136 | # if angle: 137 | # outside.remove(hole) 138 | # 139 | #instrument.remove(bore) 140 | #instrument.rotate(0,0,1, 180) 141 | #self.save(instrument,'instrument') 142 | 143 | self.make_parts(up = False) 144 | 145 | #if self.mill: 146 | # shapes = pack.cut_and_pack( 147 | # working.outside, working.bore, 148 | # self.SCHEMES[self.mill_scheme][0], self.SCHEMES[self.mill_scheme][1], 149 | # xsize=self.mill_length, 150 | # ysize=self.mill_width, 151 | # zsize=self.mill_thickness, 152 | # bit_diameter=self.mill_diameter, 153 | # save=self.save, 154 | # ) 155 | # 156 | #else: 157 | # for division in designer.divisions: 158 | # cuts = [ ] 159 | # for hole, above in division: 160 | # if hole >= 0: 161 | # cut = spec.hole_positions[hole] + 2*spec.hole_diameters[hole] 162 | # else: 163 | # cut = 0.0 164 | # cut += (length-cut)*above 165 | # cuts.append(cut) 166 | # 167 | # self.segment(cuts, up=False) 168 | 169 | 170 | #cut1 = min(spec.hole_positions[3],spec.inner_hole_positions[3])-spec.hole_diameters[3] * 0.75 171 | #cut1 -= spec.inner(cut1)*0.5 172 | ##cut1 = spec.inner_hole_positions[2]*0.5+spec.inner_hole_positions[3] * 0.5 173 | ##cut2 = cut1 + (length-cut1)*0.3 + spec.outer.maximum()*0.7 174 | #cut2 = length * 0.62 175 | # 176 | #self.segment([ cut1, cut2 ], length, up=False) 177 | 178 | 179 | 180 | if __name__ == '__main__': 181 | shape.main_action(Make_flute()) 182 | 183 | -------------------------------------------------------------------------------- /demakein/engine_trimesh.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from . import shape 4 | 5 | class Shape(object): 6 | def __init__(self, mesh): 7 | self.mesh = mesh 8 | self.cleanup() 9 | 10 | def copy(self): 11 | return Shape(self.mesh.copy()) 12 | 13 | def save(self, filename): 14 | self.mesh.export(filename) 15 | 16 | def is_empty(self): 17 | return len(self.mesh.faces) == 0 18 | 19 | def check(self): 20 | if not self.mesh.is_volume: 21 | print("Warning: shape is not a volume") 22 | #assert self.mesh.is_watertight 23 | #assert self.mesh.is_winding_consistent 24 | 25 | def cleanup(self): 26 | """ Merge all vertices within a small tolerance of each other. Cull degenerate triangles. """ 27 | import scipy.spatial 28 | import trimesh 29 | 30 | tol = 1e-4 31 | 32 | verts = self.mesh.vertices 33 | faces = self.mesh.faces 34 | 35 | # Union find algorithm 36 | parent = list(range(len(verts))) 37 | def root(i): 38 | root = i 39 | while parent[root] != root: 40 | root = parent[root] 41 | while parent[i] != i: 42 | i, parent[i] = parent[i], root 43 | return root 44 | 45 | # Merge all points within tol of each other 46 | for i,j in scipy.spatial.KDTree(verts).query_pairs(tol): 47 | parent[root(j)] = root(i) 48 | 49 | # Extract result as a list of groups 50 | groups = { } 51 | for i in range(len(verts)): 52 | r = root(i) 53 | if r not in groups: groups[r] = [ ] 54 | groups[r].append(i) 55 | groups = list(groups.values()) 56 | 57 | if len(groups) != len(verts): 58 | print("Consolidated", len(verts)-len(groups), "points") 59 | 60 | # Average each group 61 | new_verts = [ verts[group].mean(0) for group in groups ] 62 | new_index = { } 63 | for i, group in enumerate(groups): 64 | for j in group: 65 | new_index[j] = i 66 | 67 | # Remap indices and discard degenerate triangles 68 | new_faces = [ ] 69 | discards = 0 70 | for item in faces: 71 | item = [ new_index[i] for i in item ] 72 | if len(set(item)) < 3: 73 | discards += 1 74 | continue 75 | new_faces.append(item) 76 | 77 | #if discards: 78 | # print("Eliminated", discards, "triangles") 79 | 80 | self.mesh = trimesh.Trimesh(vertices=new_verts, faces=new_faces) 81 | self.check() 82 | 83 | def triangles(self): 84 | return [ tuple(map(tuple,tri)) for tri in self.mesh.triangles.tolist() ] 85 | 86 | def extent(self): 87 | b = self.mesh.bounds 88 | return shape.Limits(b[0,0],b[1,0],b[0,1],b[1,1],b[0,2],b[1,2]) 89 | 90 | def size(self): 91 | xmin,xmax,ymin,ymax,zmin,zmax = self.extent() 92 | return xmax-xmin,ymax-ymin,zmax-zmin 93 | 94 | def move(self, x,y,z): 95 | self.mesh.apply_transform( 96 | [[1,0,0,x], 97 | [0,1,0,y], 98 | [0,0,1,z], 99 | [0,0,0,1]]) 100 | 101 | def position_nicely(self): 102 | e = self.extent() 103 | self.move(-0.5*(e.xmin+e.xmax),-0.5*(e.ymin+e.ymax),-e.zmin) 104 | 105 | def rotate(self, x,y,z,angle): 106 | r = shape.rotation_matrix(x,y,z,angle) 107 | self.mesh.apply_transform( 108 | [[ r[0][0],r[0][1],r[0][2],0], 109 | [ r[1][0],r[1][1],r[1][2],0], 110 | [ r[2][0],r[2][1],r[2][2],0], 111 | [ 0, 0, 0,1]]) 112 | 113 | def add(self, other): 114 | import trimesh 115 | if other.is_empty(): 116 | return 117 | self.mesh = trimesh.boolean.union([ self.mesh, other.mesh ], engine="manifold", check_volume=False) 118 | self.cleanup() 119 | 120 | def remove(self, other): 121 | import trimesh 122 | if self.is_empty() or other.is_empty(): 123 | return 124 | self.mesh = trimesh.boolean.difference([ self.mesh, other.mesh ], engine="manifold", check_volume=False) 125 | self.cleanup() 126 | 127 | def clip(self, other): 128 | import trimesh 129 | if other.is_empty(): 130 | self.mesh = other.mesh.copy() 131 | return 132 | self.mesh = trimesh.boolean.intersection([ self.mesh, other.mesh ], engine="manifold", check_volume=False) 133 | self.cleanup() 134 | 135 | # Convex hull rather than Minkowski sum used in CGAL engine 136 | def mill_hole(self, pad_cone): 137 | import trimesh 138 | hull = trimesh.convex.hull_points(self.mesh.vertices[:,:2]) 139 | 140 | points = [ ] 141 | for x1,y1 in hull.tolist(): 142 | for x2,y2,z2 in pad_cone.mesh.vertices.tolist(): 143 | points.append((x1+x2,y1+y2,z2)) 144 | 145 | result = Shape(trimesh.convex.convex_hull(points)) 146 | result.cleanup() 147 | return result 148 | 149 | # Just provides convex hull 150 | def polygon_mask(self): 151 | return hull_2(self.mesh.vertices[:,:2]) 152 | #triangles = self.triangles() 153 | # 154 | #things = [ create_polygon_2([ (x,y) for x,y,z in triangle ]) 155 | # for triangle in triangles ] 156 | # 157 | #if not things: 158 | # return empty_shape_2() 159 | # 160 | #while len(things) > 1: 161 | # item = things.pop(-1) 162 | # things[len(things)//2].add(item) 163 | #return things[0] 164 | 165 | 166 | class Shape_2(object): 167 | def __init__(self, geom): 168 | self.geom = geom 169 | 170 | def copy(self): 171 | return Shape_2(self.geom) 172 | 173 | def extent(self): 174 | xmin, ymin, xmax, ymax = self.geom.bounds 175 | return shape.Limits_2(xmin, xmax, ymin, ymax) 176 | 177 | def move(self, x,y): 178 | import shapely 179 | self.geom = shapely.transform(self.geom, lambda p: p + (x,y)) 180 | 181 | def intersects(self, other): 182 | # Includes touching borders. Is this right? 183 | return self.geom.intersects(other.geom) 184 | 185 | def add(self, other): 186 | import shapely 187 | self.geom = shapely.union(self.geom, other.geom) 188 | 189 | def loop(self, holes): 190 | assert not holes 191 | return shape.Loop( list(self.geom.exterior.coords) ) 192 | 193 | def offset_curve(self, amount, quality=None): 194 | """ +ve dilation 195 | -ve erosion """ 196 | if amount == 0.0: 197 | return self.copy() 198 | 199 | import shapely 200 | return Shape_2( shapely.buffer(self.geom, amount) ) 201 | 202 | 203 | 204 | def create(verts, faces, name=None): 205 | import trimesh 206 | mesh = trimesh.Trimesh(vertices=verts, faces=faces) 207 | return Shape(mesh) 208 | 209 | def empty_shape(): 210 | return create([], []) 211 | 212 | 213 | 214 | def empty_shape_2(): 215 | import shapely 216 | return Shape_2(shapely.Polygon()) 217 | 218 | def create_polygon_2(points): 219 | import shapely 220 | return Shape_2(shapely.Polygon(points)) 221 | 222 | def hull_2(points): 223 | import shapely 224 | return Shape_2(shapely.MultiPoint(points).convex_hull) 225 | -------------------------------------------------------------------------------- /demakein/raphs_curves/fromcubic.py: -------------------------------------------------------------------------------- 1 | # Convert piecewise cubic into piecewise clothoid representation. 2 | 3 | from math import * 4 | 5 | from . import clothoid 6 | from . import pcorn 7 | from . import tocubic 8 | 9 | from . import offset 10 | 11 | def read_bz(f): 12 | result = [] 13 | for l in f: 14 | s = l.split() 15 | if len(s) > 0: 16 | cmd = s[-1] 17 | #print s[:-1], cmd 18 | if cmd == 'm': 19 | sp = [] 20 | result.append(sp) 21 | curpt = [float(x) for x in s[0:2]] 22 | startpt = curpt 23 | elif cmd == 'l': 24 | newpt = [float(x) for x in s[0:2]] 25 | sp.append((curpt, newpt)) 26 | curpt = newpt 27 | elif cmd == 'c': 28 | c1 = [float(x) for x in s[0:2]] 29 | c2 = [float(x) for x in s[2:4]] 30 | newpt = [float(x) for x in s[4:6]] 31 | sp.append((curpt, c1, c2, newpt)) 32 | curpt = newpt 33 | return result 34 | 35 | def plot_bzs(bzs, z0, scale, fancy = False): 36 | for sp in bzs: 37 | for i in range(len(sp)): 38 | bz = sp[i] 39 | tocubic.plot_bz(bz, z0, scale, i == 0) 40 | print('stroke') 41 | if fancy: 42 | for i in range(len(sp)): 43 | bz = sp[i] 44 | 45 | x0, y0 = z0[0] + scale * bz[0][0], z0[1] + scale * bz[0][1] 46 | print('gsave', x0, y0, 'translate circle fill grestore') 47 | if len(bz) == 4: 48 | x1, y1 = z0[0] + scale * bz[1][0], z0[1] + scale * bz[1][1] 49 | x2, y2 = z0[0] + scale * bz[2][0], z0[1] + scale * bz[2][1] 50 | x3, y3 = z0[0] + scale * bz[3][0], z0[1] + scale * bz[3][1] 51 | print('gsave 0.5 setlinewidth', x0, y0, 'moveto') 52 | print(x1, y1, 'lineto stroke') 53 | print(x2, y2, 'moveto') 54 | print(x3, y3, 'lineto stroke grestore') 55 | print('gsave', x1, y1, 'translate 0.75 dup scale circle fill grestore') 56 | print('gsave', x2, y2, 'translate 0.75 dup scale circle fill grestore') 57 | print('gsave', x3, y3, 'translate 0.75 dup scale circle fill grestore') 58 | 59 | 60 | 61 | def measure_bz_cloth(seg, bz, n = 100): 62 | bz_arclen = tocubic.bz_arclength_rk4(bz) 63 | arclen_ratio = seg.arclen / bz_arclen 64 | dbz = tocubic.bz_deriv(bz) 65 | 66 | def measure_derivs(x, ys): 67 | dx, dy = tocubic.bz_eval(dbz, x) 68 | ds = hypot(dx, dy) 69 | s = ys[0] * arclen_ratio 70 | dscore = ds * (tocubic.mod_2pi(atan2(dy, dx) - seg.th(s)) ** 2) 71 | #print s, atan2(dy, dx), seg.th(s) 72 | return [ds, dscore] 73 | dt = 1./n 74 | t = 0 75 | ys = [0, 0] 76 | for i in range(n): 77 | dydx = measure_derivs(t, ys) 78 | tocubic.rk4(ys, dydx, t, dt, measure_derivs) 79 | t += dt 80 | return ys[1] 81 | 82 | def cubic_bz_to_pcorn(bz, thresh): 83 | dx = bz[3][0] - bz[0][0] 84 | dy = bz[3][1] - bz[0][1] 85 | dx1 = bz[1][0] - bz[0][0] 86 | dy1 = bz[1][1] - bz[0][1] 87 | dx2 = bz[3][0] - bz[2][0] 88 | dy2 = bz[3][1] - bz[2][1] 89 | chth = atan2(dy, dx) 90 | th0 = tocubic.mod_2pi(chth - atan2(dy1, dx1)) 91 | th1 = tocubic.mod_2pi(atan2(dy2, dx2) - chth) 92 | seg = pcorn.Segment(bz[0], bz[3], th0, th1) 93 | err = measure_bz_cloth(seg, bz) 94 | if err < thresh: 95 | return [seg] 96 | else: 97 | # de Casteljau 98 | x01, y01 = 0.5 * (bz[0][0] + bz[1][0]), 0.5 * (bz[0][1] + bz[1][1]) 99 | x12, y12 = 0.5 * (bz[1][0] + bz[2][0]), 0.5 * (bz[1][1] + bz[2][1]) 100 | x23, y23 = 0.5 * (bz[2][0] + bz[3][0]), 0.5 * (bz[2][1] + bz[3][1]) 101 | xl2, yl2 = 0.5 * (x01 + x12), 0.5 * (y01 + y12) 102 | xr1, yr1 = 0.5 * (x12 + x23), 0.5 * (y12 + y23) 103 | xm, ym = 0.5 * (xl2 + xr1), 0.5 * (yl2 + yr1) 104 | bzl = [bz[0], (x01, y01), (xl2, yl2), (xm, ym)] 105 | bzr = [(xm, ym), (xr1, yr1), (x23, y23), bz[3]] 106 | segs = cubic_bz_to_pcorn(bzl, 0.5 * thresh) 107 | segs.extend(cubic_bz_to_pcorn(bzr, 0.5 * thresh)) 108 | return segs 109 | 110 | def bzs_to_pcorn(bzs, thresh = 1e-9): 111 | result = [] 112 | for sp in bzs: 113 | rsp = [] 114 | for bz in sp: 115 | if len(bz) == 2: 116 | dx = bz[1][0] - bz[0][0] 117 | dy = bz[1][1] - bz[0][1] 118 | th = atan2(dy, dx) 119 | rsp.append(pcorn.Segment(bz[0], bz[1], 0, 0)) 120 | else: 121 | rsp.extend(cubic_bz_to_pcorn(bz, thresh)) 122 | result.append(rsp) 123 | return result 124 | 125 | def plot_segs(segs): 126 | for i in range(len(segs)): 127 | seg = segs[i] 128 | if i == 0: 129 | print(seg.z0[0], seg.z0[1], 'moveto') 130 | print(seg.z1[0], seg.z1[1], 'lineto') 131 | print('stroke') 132 | for i in range(len(segs)): 133 | seg = segs[i] 134 | if i == 0: 135 | print('gsave', seg.z0[0], seg.z0[1], 'translate circle fill grestore') 136 | print('gsave', seg.z1[0], seg.z1[1], 'translate circle fill grestore') 137 | 138 | import sys 139 | 140 | def test_to_pcorn(): 141 | C1 = 0.55228 142 | bz = [(100, 100), (100 + 400 * C1, 100), (500, 500 - 400 * C1), (500, 500)] 143 | for i in range(0, 13): 144 | thresh = .1 ** i 145 | segs = cubic_bz_to_pcorn(bz, thresh) 146 | plot_segs(segs) 147 | print(thresh, len(segs), file=sys.stderr) 148 | print('0 20 translate') 149 | 150 | if __name__ == '__main__': 151 | f = file(sys.argv[1]) 152 | bzs = read_bz(f) 153 | rsps = bzs_to_pcorn(bzs, 1) 154 | #print rsps 155 | tocubic.plot_prolog() 156 | print('grestore') 157 | print('1 -1 scale 0 -720 translate') 158 | print('/ss 1.5 def') 159 | print('/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def') 160 | tot = 0 161 | for segs in rsps: 162 | curve = pcorn.Curve(segs) 163 | #curve = offset.offset(curve, 10) 164 | print('%', curve.arclen) 165 | print('%', curve.sstarts) 166 | if 0: 167 | print('gsave 1 0 0 setrgbcolor') 168 | cmd = 'moveto' 169 | for i in range(100): 170 | s = i * .01 * curve.arclen 171 | x, y = curve.xy(s) 172 | th = curve.th(s) 173 | sth = 5 * sin(th) 174 | cth = 5 * cos(th) 175 | print(x, y, cmd) 176 | cmd = 'lineto' 177 | print('closepath stroke grestore') 178 | for i in range(100): 179 | s = i * .01 * curve.arclen 180 | x, y = curve.xy(s) 181 | th = curve.th(s) 182 | sth = 5 * sin(th) 183 | cth = 5 * cos(th) 184 | if 0: 185 | print(x - cth, y - sth, 'moveto') 186 | print(x + cth, y + sth, 'lineto stroke') 187 | if 1: 188 | for s in curve.find_breaks(): 189 | print('gsave 0 1 0 setrgbcolor') 190 | x, y = curve.xy(s) 191 | print(x, y, 'translate 2 dup scale circle fill') 192 | print('grestore') 193 | #plot_segs(segs) 194 | 195 | print('gsave 0 0 0 setrgbcolor') 196 | optim = 3 197 | thresh = 1e-2 198 | new_bzs = tocubic.pcorn_curve_to_bzs(curve, optim, thresh) 199 | tot += len(new_bzs) 200 | plot_bzs([new_bzs], (0, 0), 1, True) 201 | print('grestore') 202 | print('grestore') 203 | print('/Helvetica 12 selectfont') 204 | print('36 720 moveto (thresh=%g optim=%d) show' % (thresh, optim)) 205 | print('( tot segs=%d) show' % tot) 206 | print('showpage') 207 | 208 | #plot_bzs(bzs, (100, 100), 1) 209 | -------------------------------------------------------------------------------- /demakein/mask.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Matrices at arbitrary offsets 4 | 5 | """ 6 | 7 | import numpy, subprocess, os 8 | 9 | def bound_intersection(bounds1, bounds2): 10 | (x1,y1,width1,height1) = bounds1 11 | (x2,y2,width2,height2) = bounds2 12 | if width1 == 0 or height1 == 0: return bounds2 13 | if width2 == 0 or height2 == 0: return bounds1 14 | x = max(x1,x2) 15 | y = max(y1,y2) 16 | width = max(0, min(x1+width1,x2+width2)-x) 17 | height = max(0, min(y1+height1,y2+height2)-y) 18 | return (x,y,width,height) 19 | 20 | def bound_union(bounds1, bounds2): 21 | (x1,y1,width1,height1) = bounds1 22 | (x2,y2,width2,height2) = bounds2 23 | if width1 == 0 or height1 == 0: return bounds2 24 | if width2 == 0 or height2 == 0: return bounds1 25 | x = min(x1,x2) 26 | y = min(y1,y2) 27 | width = max(x1+width1,x2+width2)-x 28 | height = max(y1+height1,y2+height2)-y 29 | return (x,y,width,height) 30 | 31 | class Big_matrix(object): 32 | def __init__(self, x,y,data): 33 | self.x = x 34 | self.y = y 35 | self.height, self.width = data.shape[:2] 36 | self.data = data 37 | 38 | def shift(self, x,y): 39 | return Big_matrix(self.x+x,self.y+y,self.data) 40 | 41 | def bounds(self): 42 | return (self.x,self.y,self.width,self.height) 43 | 44 | def clip(self, x,y,width,height): 45 | if y == self.y and x == self.x and width == self.width and height == self.height: 46 | return self 47 | result = zeros(x,y,width,height,self.data.dtype) 48 | x1 = max(self.x,x) 49 | x2 = min(self.x+self.width,x+width) 50 | y1 = max(self.y,y) 51 | y2 = min(self.y+self.height,y+height) 52 | if y1 < y2 and x1 < x2: 53 | result.data[y1-y:y2-y,x1-x:x2-x] = self.data[y1-self.y:y2-self.y,x1-self.x:x2-self.x] 54 | return result 55 | 56 | def apply(self, other, bounds, operation): 57 | a = self.clip(*bounds) 58 | b = other.clip(*bounds) 59 | return Big_matrix(a.x,a.y,operation(a.data,b.data)) 60 | 61 | def union_apply(self, other, operation): 62 | bounds = bound_union(self.bounds(),other.bounds()) 63 | return self.apply(other, bounds, operation) 64 | 65 | def intersection_apply(self, other, operation): 66 | bounds = bound_intersection(self.bounds(),other.bounds()) 67 | return self.apply(other, bounds, operation) 68 | 69 | def __and__(self,other): 70 | return self.intersection_apply(other,lambda a,b: a&b) 71 | def __or__(self,other): 72 | return self.union_apply(other,lambda a,b: a|b) 73 | 74 | def and_not(self, other): 75 | return self.apply(other, self.bounds(), lambda a,b: a&~b) 76 | 77 | def spans(self): 78 | for y in range(self.height): 79 | start = 0 80 | for x in range(self.width): 81 | if not self.data[y,x]: 82 | if start != x: 83 | yield y+self.y, start+self.x, x+self.x 84 | start = x+1 85 | if start != self.width: 86 | yield y+self.y, start+self.x, self.width+self.x 87 | 88 | def morph(self, mask, operation): 89 | spans = [ None, self ] 90 | def get_span(n): 91 | while len(spans) <= n: 92 | spans.append(operation(spans[-1],self.shift(len(spans)-1,0))) 93 | return spans[n] 94 | result = None 95 | for y,x1,x2 in mask.spans(): 96 | span = get_span(x2-x1).shift(x1,y) 97 | if result is None: 98 | result = span 99 | else: 100 | result = operation(result, span) 101 | assert result is not None, 'empty mask' 102 | return result 103 | 104 | def dilate(self,mask): 105 | return self.morph(mask, lambda a,b: a|b) 106 | 107 | def erode(self,mask): 108 | return self.morph(mask, lambda a,b: a&b) 109 | 110 | def open(self,mask): 111 | return self.erode(mask).dilate(mask) 112 | 113 | def close(self,mask): 114 | return self.dilate(mask).erode(mask) 115 | 116 | def trace(self,res=1.0): 117 | return trace(self,res) 118 | 119 | 120 | def ones(x,y,width,height,type=bool): 121 | return Big_matrix(x,y, numpy.ones((height,width),type)) 122 | 123 | def zeros(x,y,width,height,type=bool): 124 | return Big_matrix(x,y, numpy.zeros((height,width),type)) 125 | 126 | def zero(type=bool): 127 | return zeros(0,0,0,0,type) 128 | 129 | 130 | 131 | 132 | 133 | 134 | def line_param(x1,y1,x2,y2): 135 | if x2 == x1: 136 | return 0.0,y1 137 | m = (y2-y1)/(x2-x1) 138 | c = y1-m*x1 139 | return m,c 140 | 141 | def int_floor(x): 142 | return int(numpy.floor(x)) 143 | 144 | def int_ceil(x): 145 | return int(numpy.ceil(x)) 146 | 147 | def make_mask(lines): 148 | base_x = int_floor(min( min(x1,x2) for x1,y1,x2,y2 in lines )) 149 | base_y = int_floor(min( min(y1,y2) for x1,y1,x2,y2 in lines )) 150 | width = int_ceil(max( max(x1,x2) for x1,y1,x2,y2 in lines )) - base_x + 1 151 | height = int_ceil(max( max(y1,y2) for x1,y1,x2,y2 in lines )) - base_y + 1 152 | 153 | line_count = { } 154 | for x1,y1,x2,y2 in lines: 155 | if x1 < x2: 156 | n = 1 157 | key = (x1,y1,x2,y2) 158 | else: 159 | n = -1 160 | key = (x2,y2,x1,y1) 161 | line_count[key] = line_count.get(key,0) + n 162 | 163 | count = numpy.zeros((height,width), 'int32') 164 | 165 | for (x1,y1,x2,y2), n in list(line_count.items()): 166 | if n == 0: continue 167 | ix1 = int_ceil(x1) 168 | ix2 = int_ceil(x2) 169 | if ix1 == ix2: continue 170 | 171 | m,c = line_param(x1,y1,x2,y2) 172 | for x in range(ix1,ix2): 173 | y = int_ceil(m*x+c) 174 | count[y-base_y,x-base_x] += n 175 | 176 | for y in range(1, height): 177 | count[y] += count[y-1] 178 | 179 | return Big_matrix(base_x, base_y, count > 0) 180 | 181 | 182 | 183 | 184 | def write_pbm(f, data): 185 | f.write('P1\n%d %d\n' % (data.shape[1],data.shape[0])) 186 | for line in data: 187 | f.write(''.join([ '1' if item else '0' for item in line ])) 188 | #f.write('\n') 189 | 190 | def save(prefix, mask): 191 | with open(prefix+'.pbm','wt') as f: write_pbm(f, mask.data) 192 | os.system('pnmtopng <%s.pbm >%s.png' % (prefix,prefix)) 193 | 194 | 195 | def trace(mask, res=1.0): 196 | from . import shape 197 | 198 | process = subprocess.Popen([ 199 | 'potrace', '-a', '-1', '-t', '0', '-u', '100', '-b', 'svg' 200 | ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) 201 | 202 | write_pbm(process.stdin, mask.data) 203 | 204 | from xml.etree import ElementTree 205 | doc = ElementTree.parse(process.stdout) 206 | 207 | loops = [ ] 208 | for path in doc.getroot().findall('{http://www.w3.org/2000/svg}g/{http://www.w3.org/2000/svg}path'): 209 | items = path.attrib['d'].split() 210 | items = [ int(item.strip('Mczl')) for item in items ] 211 | for i in range(2,len(items)): 212 | items[i] += items[i-2] 213 | 214 | loop = [ ] 215 | for i in range(len(items)//2): 216 | loop.append( (mask.x+items[i*2]*0.01-0.5,mask.y+mask.data.shape[0]-items[i*2+1]*0.01-0.5) ) 217 | 218 | i = 0 219 | while i < len(loop): 220 | if loop[i] == loop[(i+1)%len(loop)]: 221 | del loop[i] 222 | else: 223 | i += 1 224 | 225 | assert len(loop) == len(set(loop)) 226 | 227 | loops.append( shape.Loop(loop[::-1]).scale(1.0/res) ) 228 | return loops 229 | 230 | #loop = [ ] 231 | #loops = [ loop ] 232 | #for line in process.stdout: 233 | # parts = line.rstrip().split() 234 | # if parts[0] != 'TYPE:': continue 235 | # assert parts[2] == 'X:' and parts[4] == 'Y:' 236 | # if parts[1] == '2': continue 237 | # if parts[1] == '3': 238 | # loop = [ ] 239 | # loops.append(loop) 240 | # loop.append( (0.5+mask.x+int(parts[3])*0.01,0.5+mask.y+int(parts[5])*0.01) ) 241 | #return [ shape.Loop(item[::-1]) for item in loops ] 242 | 243 | 244 | if __name__ == '__main__': 245 | from . import shape 246 | loop = shape.Loop( shape.circle(50.0)[:50] ) 247 | with open('/tmp/test.pbm','wt') as f: write_pbm(f,loop.mask(1.0).data) 248 | print(( max(x for x,y in loop) - min(x for x,y in loop) )) 249 | for i in range(10): 250 | mask = loop.mask(1.0) 251 | with open('/tmp/test2.pbm','wt') as f: write_pbm(f,mask.data) 252 | [ loop ] = trace(mask) 253 | print(( max(x for x,y in loop), min(x for x,y in loop) )) 254 | print(( max(y for x,y in loop), min(y for x,y in loop) )) 255 | print() 256 | #print( min(x for x,y in loop) ) 257 | #mask[5,5] = 0 258 | with open('/tmp/test2.pbm','wt') as f: write_pbm(f,mask.data) 259 | 260 | 261 | #mask = shape.circle(20).mask(40) 262 | # 263 | #thing = zeros(0,0,100,100) 264 | #thing.data[:,:] = True 265 | # 266 | #thing = thing | thing.shift(100,100) 267 | #thing = thing.close(mask) 268 | #with open('/tmp/test3.pbm','wt') as f: write_pbm(f,thing.data) 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /demakein/make_panpipe.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | 8mm thickness, 6mm holes 4 | 5 | C6 6 | 7 | D6 8 | got: 1120Hz -> 309mm 9 | want: 294.6 10 | error: 14.4mm 11 | 12 | E6 13 | got: 1231Hz -> 281.2mm 14 | want: 262.5mm 15 | error: 18.7mm 16 | 17 | G6 18 | got: 1462 -> 236.7mm 19 | want: 220.7mm 20 | error: 16.0mm 21 | 22 | C7 23 | got: 1942Hz ~ 178.2mm 24 | want: 165.4mm 25 | error: 12.8mm 26 | 27 | average: 15.5 wavelength = 3.87 lengths = 0.645 diameters 28 | 29 | """ 30 | 31 | 32 | 33 | import sys, os, math 34 | 35 | from . import config, design, make, shape, profile 36 | 37 | 38 | RES = 20 39 | 40 | LENGTH_CORRECTION = -0.7 41 | #-0.645 # * diameter 42 | # Hole is rounded at end, so make it a little longer 43 | # - unknown further correction 44 | 45 | SCALE = [ 46 | 'C6', 47 | 'D6', 48 | 'E6', 49 | 'F6', 50 | 'G6', 51 | 'A6', 52 | 'B6', 53 | 'C7', 54 | 'D7', 55 | ] 56 | 57 | #SCALE = [ 58 | # 'F6', 59 | # 'G6', 60 | # 'A6', 61 | # 'C7', 62 | # 'D7', 63 | # 'F7', 64 | #] 65 | 66 | def make_hole(diameter, length): 67 | #pos = [ 0.0 ] 68 | 69 | # Extend a little beyond instrument to avoid boolean ops errors 70 | pos = [ -1.0 ] 71 | 72 | diam = [ diameter ] 73 | 74 | radius = diameter*0.5 75 | n = 4 76 | for i in range(0,n): 77 | x = math.pi/2 * float(i)/n 78 | pos.append(length+radius*(math.sin(x)-1)) 79 | diam.append(diameter*math.cos(x)) 80 | 81 | prof = profile.Profile(pos,diam,diam) 82 | hole = shape.extrude_profile(prof) 83 | 84 | return hole 85 | 86 | @config.help("""\ 87 | Make a Viking-style panpipe. 88 | """) 89 | @config.Float_flag('thickness', 'Instrument thickness.\n(Wood thickness should be half this if milling.)') 90 | @config.Float_flag('wall', 'Minimum wall thickness') 91 | @config.Int_flag('transpose', 'Transpose (semitones)') 92 | class Make_panpipe(make.Make): 93 | thickness = 8.0 94 | wall = 1.0 95 | transpose = 0 96 | 97 | def run(self): 98 | zsize = self.thickness 99 | 100 | pad = self.wall 101 | diameter = zsize - pad*2 102 | 103 | self.log.log('Thickness: %.1fmm\n' % zsize) 104 | self.log.log('Min wall thickness: %.1fmm\n' % pad) 105 | self.log.log('Hole diameter: %.1fmm\n' % diameter) 106 | 107 | negatives = [ ] 108 | lengths = [ ] 109 | xs = [ ] 110 | for i, note in enumerate(SCALE): 111 | print('Make hole %d / %d' % (i+1,len(SCALE))) 112 | length = design.wavelength(note,self.transpose)*0.25 + LENGTH_CORRECTION*diameter 113 | x = i*(diameter+pad) 114 | lengths.append(length) 115 | xs.append(x) 116 | hole = make_hole(diameter, length) 117 | hole.move(x,0,0) 118 | negatives.append(hole) 119 | 120 | string_hole_diameter = diameter*1.5 121 | string_hole_loop = shape.circle(string_hole_diameter) 122 | string_hole = shape.extrusion([-zsize,zsize],[string_hole_loop,string_hole_loop]) 123 | string_hole.rotate(1,0,0, 90) 124 | 125 | xlow = xs[0]-zsize 126 | #xmid = xs[-1]*0.5 127 | xhigh = xs[-1]+zsize 128 | 129 | zhigh = lengths[0]+zsize*0.5 130 | #zmid = max(lengths[-1]+zsize*0.5, zhigh-(xhigh-xmid)) 131 | 132 | trim = min(xhigh-xlow,zhigh) * 0.5 133 | 134 | #string_x = (xmid+xhigh)*0.5 - diameter 135 | #string_z = (zmid+zhigh)*0.5 - diameter 136 | #string_z = lengths[3] + diameter*2 137 | #string_x = xhigh-trim*0.5-diameter 138 | #string_z = zhigh-trim*0.5-diameter 139 | 140 | a = math.pi*-5/8 141 | d = diameter * 1.5 142 | string_x = xhigh-trim + math.cos(a)*d 143 | string_z = zhigh + math.sin(a)*d 144 | 145 | string_hole.move(string_x,0,string_z) 146 | 147 | #p = pad*0.5 148 | #loop = shape.Loop([(0,p),(p*2,-p),(-p*2,p)]) 149 | #r = diameter-pad 150 | #stick = shape.extrusion([-r,r],[loop,loop]) 151 | # 152 | #for i in xrange(-1,len(SCALE)+2): 153 | # for j in xrange(int(zhigh / (diameter+pad))+1): 154 | # x = (i-0.5)*(diameter+pad) 155 | # z = r + j*(diameter+pad) 156 | # c = stick.copy() 157 | # if (i^j)&1: 158 | # c.rotate(0,1,0,90) 159 | # c.move(x,-diameter*0.5-pad,z) 160 | # negatives.append(c) 161 | 162 | loop = shape.Loop([ 163 | (xlow,0), 164 | (xhigh,0), 165 | (xhigh,zhigh-trim), 166 | (xhigh-trim,zhigh), 167 | (xlow,zhigh) 168 | ]) 169 | 170 | bev = zsize / 4.0 171 | loop_bevel = shape.Loop([ 172 | (xlow,bev), 173 | (xhigh,bev), 174 | (xhigh,zhigh-trim), 175 | (xhigh-trim,zhigh), 176 | (xlow,zhigh) 177 | ]) 178 | 179 | #mask = loop.mask(RES) 180 | 181 | #amount = diameter * 0.5 182 | #op = shape.circle(amount).mask(RES) 183 | #mask = mask.open(op) 184 | 185 | #loop = mask.trace(RES)[0] 186 | 187 | #sloop = mask.erode(op).trace(RES)[0] 188 | 189 | #z1 = zsize*0.5-amount 190 | z2 = zsize*0.5 191 | 192 | #positive = shape.extrusion([-z2,z2],[loop,loop]) 193 | positive = shape.extrusion([-z2,-z2+bev,z2-bev,z2],[loop_bevel,loop,loop,loop_bevel]) 194 | positive.rotate(1,0,0,90) 195 | 196 | negative = string_hole.copy() 197 | for i, item in enumerate(negatives): 198 | print('Merge %d / %d' % (i+1,len(negatives))) 199 | negative.add(item) 200 | 201 | del negatives 202 | 203 | instrument = positive.copy() 204 | 205 | print('Remove holes from instrument') 206 | instrument.remove(negative) 207 | 208 | del positive 209 | del negative 210 | 211 | instrument.rotate(1,0,0,90) 212 | extent = instrument.extent() 213 | 214 | copy = instrument.copy() 215 | copy.rotate(0,0,1,-45) 216 | cextent = copy.extent() 217 | copy.move(-(cextent.xmin+cextent.xmax)*0.5, 218 | -(cextent.ymin+cextent.ymax)*0.5, 219 | -cextent.zmin) 220 | self.save(copy,'instrument') 221 | del copy 222 | 223 | # Milling patterns 224 | 225 | top = shape.block( 226 | extent.xmin-1,extent.xmax+1, 227 | extent.ymin-1,extent.ymax+1, 228 | 0,extent.zmax+1 229 | ) 230 | bottom = shape.block( 231 | extent.xmin-1,extent.xmax+1, 232 | extent.ymin-1,extent.ymax+1, 233 | extent.zmin-1,0 234 | ) 235 | 236 | top.clip(instrument) 237 | bottom.clip(instrument) 238 | top.rotate(1,0,0,180) 239 | top.move(0,4,0) 240 | bottom.add(top) 241 | pattern = bottom 242 | 243 | del top 244 | del bottom 245 | 246 | pattern.move(0,0,z2) 247 | pattern.rotate(0,0,1, -90) 248 | 249 | bit_pad = 4.0 250 | extra_depth = 3.0 251 | a = bit_pad 252 | b = bit_pad + z2/10.0 253 | pad_cone = shape.extrude_profile(profile.Profile( 254 | [-extra_depth,0,z2],[a*2,a*2,b*2] 255 | ), 256 | cross_section=lambda d: shape.circle(d,16)) 257 | 258 | extra = b + 0.5 259 | 260 | extent = pattern.extent() 261 | mill = shape.block( 262 | extent.xmin-extra, extent.xmax+extra, 263 | extent.ymin-extra, extent.ymax+extra, 264 | -extra_depth, z2, 265 | ) 266 | 267 | mill.remove( pattern.mill_hole(pad_cone) ) 268 | mill.add( pattern ) 269 | 270 | del pad_cone 271 | del pattern 272 | 273 | self.save(mill, 'mill') 274 | 275 | 276 | # dilations = [ 277 | ## (z, shape.circle(2.0*( 2.0+z/20.0 )).mask(self.res)) 278 | ## for z in range(1) 279 | # (z, mask.ones(0,0,1,1) if not z else 280 | # shape.circle(2.0*( z/10.0 )).mask(self.res)) 281 | # for z in range(0,int(self.zsize),5) 282 | # ] 283 | 284 | #dilator = shape.circle(2* bit_diameter).mask(res) 285 | #hole_mask = mask.zero() 286 | #for x,y,packable in self.items: 287 | # if packable.use_upper: 288 | # hole_mask = hole_mask | packable.dilated_mask.offset(int(x*self.res+0.5),int(y*self.res+0.5)) 289 | #hole_mask = hole_mask.close(dilator) 290 | 291 | #mask = bottom.mask(RES) 292 | #op = shape.circle(6).mask(RES) 293 | #loop = mask.dilate(op).trace(RES) 294 | # 295 | #z1 = -self.thickness+1.5 296 | #z2 = 0.0 297 | #loop_ext = shape.extrusion([z1,z2],[loop,loop]) 298 | #extent = loop_ext.extent() 299 | # 300 | #cut = shape.block( 301 | # extent.xmin-1,extent.xmax+1, 302 | # extent.ymin-1,extent.ymax+1, 303 | # z1,z2 304 | #) 305 | #cut.remove(loop_ext) 306 | #cut.add(bottom) 307 | #self.save(cut,'mill.stl') 308 | 309 | 310 | if __name__ == '__main__': 311 | shape.main_action(Make_panpipe()) 312 | 313 | 314 | -------------------------------------------------------------------------------- /demakein/profile.py: -------------------------------------------------------------------------------- 1 | 2 | import bisect, math 3 | from .raphs_curves import cornu 4 | 5 | 6 | class Profile: 7 | def __init__(self, pos, low, high=None): 8 | if high is None: high = low 9 | self.pos = pos 10 | self.low = low 11 | self.high = high 12 | 13 | 14 | def __call__(self, position, high=False): 15 | if position < self.pos[0]: 16 | return self.low[0] 17 | if position > self.pos[-1]: 18 | return self.high[-1] 19 | i = bisect.bisect_left(self.pos, position) 20 | if self.pos[i] == position: 21 | if high: 22 | return self.high[i] 23 | else: 24 | return self.low[i] 25 | 26 | t = float(position - self.pos[i-1])/(self.pos[i]-self.pos[i-1]) 27 | return (1.0-t)*self.high[i-1] + t*self.low[i] 28 | 29 | 30 | def __repr__(self): 31 | return 'Profile' + repr((self.pos,self.low,self.high)) 32 | 33 | 34 | def start(self): 35 | return self.pos[0] 36 | 37 | 38 | def end(self): 39 | return self.pos[-1] 40 | 41 | 42 | def maximum(self): 43 | return max(max(self.low),max(self.high)) 44 | 45 | 46 | def morph(self, other, operator): 47 | """ Fairly dumb way to combine profiles. 48 | Won't work perfectly for min, max. """ 49 | if not isinstance(other, Profile): 50 | other = Profile([0.0],[other],[other]) 51 | pos = sorted(set(self.pos + other.pos)) 52 | low = [ operator(self(p,False),other(p,False)) for p in pos ] 53 | high = [ operator(self(p,True),other(p,True)) for p in pos ] 54 | return Profile(pos,low,high) 55 | 56 | def max_with(self, other): 57 | return self.morph(other, lambda a,b: max(a,b)) 58 | 59 | def min_with(self, other): 60 | return self.morph(other, lambda a,b: min(a,b)) 61 | 62 | def __add__(self, other): 63 | return self.morph(other, lambda a,b: a+b) 64 | 65 | def __sub__(self, other): 66 | return self.morph(other, lambda a,b: a-b) 67 | 68 | def clipped(self, start, end): 69 | """ Clip or extend a profile """ 70 | 71 | pos = [ start ] 72 | low = [ self(start, True) ] 73 | high = [ self(start, True) ] 74 | 75 | for i, p in enumerate(self.pos): 76 | if p <= start or p >= end: continue 77 | pos.append(p) 78 | low.append(self.low[i]) 79 | high.append(self.high[i]) 80 | 81 | pos.append(end) 82 | low.append(self(end, False)) 83 | high.append(self(end, False)) 84 | return Profile(pos,low,high) 85 | 86 | def reversed(self): 87 | """ Reverse profile. Positions are negated. """ 88 | new_pos = [ -item for item in self.pos[::-1] ] 89 | return Profile(new_pos, self.high[::-1], self.low[::-1]) 90 | 91 | def moved(self, offset): 92 | new_pos = [ item+offset for item in self.pos ] 93 | return Profile(new_pos, self.low, self.high) 94 | 95 | def appended_with(self, other): 96 | other = other.moved(self.pos[-1]) 97 | return Profile( 98 | self.pos[:-1] + other.pos, 99 | self.low + other.low[1:], 100 | self.high[:-1] + other.high, 101 | ) 102 | 103 | def as_stepped(self, max_step): 104 | pos = [ ] 105 | 106 | low = [ ] 107 | high = [ ] 108 | 109 | for i in range(len(self.pos)-1): 110 | pos.append(self.pos[i]) 111 | 112 | ax = self.pos[i] 113 | ay = self.high[i] 114 | bx = self.pos[i+1] 115 | by = self.low[i+1] 116 | n = int( float(abs(by - ay)) / max_step )+1 117 | if not n: continue 118 | pos.extend( (bx-ax)*float(j)/n+ax for j in range(1,n) ) 119 | 120 | pos.append(self.pos[-1]) 121 | 122 | diams = [ self(0.5*(pos[i]+pos[i+1])) for i in range(len(pos)-1) ] 123 | low = [ diams[0] ] + diams 124 | high = diams + [ diams[-1] ] 125 | 126 | #for i in range(len(pos)-1): 127 | # assert high[i] == low[i+1], repr((high[i],low[i+1])) 128 | 129 | return Profile(pos,low,high) 130 | 131 | 132 | def length(x,y): 133 | return math.sqrt(x*x+y*y) 134 | 135 | def cornu_yx(t,mirror): 136 | # Reparamaterize for constant absolute rate of turning 137 | t = math.sqrt(abs(t)) * (1 if t > 0 else -1) 138 | y,x = cornu.eval_cornu(t) 139 | if mirror: y = -y 140 | return y,x 141 | 142 | def solve(a1,a2): 143 | pi = math.pi 144 | two_pi = pi*2 145 | 146 | def score(t1,t2,mirror): 147 | if abs(t1-t2) < 1e-6 or max(abs(t1),abs(t2)) > pi*10.0: return 1e30 148 | 149 | y1,x1 = cornu_yx(t1, mirror) 150 | y2,x2 = cornu_yx(t2, mirror) 151 | chord_a = math.atan2(y2-y1,x2-x1) 152 | chord_l = length(y2-y1, x2-x1) 153 | this_a1 = abs(t1) #t1*t1 154 | this_a2 = abs(t2) #t2*t2 155 | if mirror: 156 | this_a1 = -this_a1 157 | this_a2 = -this_a2 158 | if t1 > t2: 159 | this_a1 += pi 160 | this_a2 += pi 161 | ea1 = (this_a1-chord_a-a1+pi)%two_pi - pi 162 | ea2 = (this_a2-chord_a-a2+pi)%two_pi - pi 163 | return ea1*ea1+ea2*ea2 164 | 165 | s = None 166 | n = 2 167 | for new_mirror in [False,True]: 168 | for i in range(-n,n+1): 169 | for j in range(-n,n+1): 170 | new_t1 = i*pi/n 171 | new_t2 = j*pi/n 172 | new_s = score(new_t1,new_t2,new_mirror) 173 | if s is None or new_s < s: 174 | t1 = new_t1 175 | t2 = new_t2 176 | mirror = new_mirror 177 | s = new_s 178 | 179 | step = pi / n * 0.5 180 | while step >= 1e-4: 181 | for new_t1,new_t2 in [(t1+step,t2+step), (t1-step,t2-step), (t1-step,t2+step), (t1+step,t2-step)]: 182 | new_s = score(new_t1,new_t2,mirror) 183 | if new_s < s: 184 | s = new_s 185 | t1 = new_t1 186 | t2 = new_t2 187 | break 188 | else: 189 | step *= 0.5 190 | 191 | return t1, t2, mirror 192 | 193 | def curved_profile(pos, low, high, low_angle, high_angle, quality=512): 194 | n = len(pos) 195 | 196 | a = [ ] 197 | for i in range(n-1): 198 | x1 = pos[i] 199 | y1 = high[i] * 0.5 200 | x2 = pos[i+1] 201 | y2 = low[i+1] * 0.5 202 | a.append( (math.atan2(y2-y1,x2-x1)+math.pi)%(math.pi*2)-math.pi ) 203 | def interpret(i,value): 204 | if value == None: 205 | return None 206 | if value == 'mean': 207 | return (a[i-1]+a[i])*0.5 208 | if value == 'up': 209 | return a[i] 210 | if value == 'down': 211 | return a[i-1] 212 | return value*math.pi/180 213 | low_angle = [ interpret(i,value) for i,value in enumerate(low_angle) ] 214 | high_angle = [ interpret(i,value) for i,value in enumerate(high_angle) ] 215 | 216 | ppos = [ ] 217 | plow = [ ] 218 | phigh = [ ] 219 | 220 | for i in range(n-1): 221 | ppos.append(pos[i]) 222 | plow.append(low[i]) 223 | phigh.append(high[i]) 224 | 225 | x1 = pos[i] 226 | y1 = high[i] * 0.5 227 | x2 = pos[i+1] 228 | y2 = low[i+1] * 0.5 229 | l = length(x2-x1,y2-y1) 230 | a = math.atan2(y2-y1,x2-x1) 231 | 232 | if high_angle[i] is not None: 233 | a1 = high_angle[i] - a 234 | else: 235 | a1 = 0.0 236 | 237 | if low_angle[i+1] is not None: 238 | a2 = low_angle[i+1] - a 239 | else: 240 | a2 = 0.0 241 | 242 | if abs(a1-a2) < math.pi*2/quality: continue 243 | 244 | #t1 = th1 245 | #t2 = th2 246 | 247 | #k0,k1 = clothoid.solve_clothoid(th1/math.pi,th2/math.pi) 248 | #print(th1, th2, k0, k1) 249 | 250 | #if abs(k1) < 1e-6: continue 251 | 252 | #t1 = k0-k1*0.5 253 | #t2 = k0+k1*0.5 254 | 255 | t1, t2, mirror = solve(a1,a2) 256 | 257 | cy1,cx1 = cornu_yx(t1,mirror) 258 | cy2,cx2 = cornu_yx(t2,mirror) 259 | cl = length(cx2-cx1,cy2-cy1) 260 | if abs(cl) < 1e-10: continue 261 | 262 | ca = math.atan2(cy2-cy1,cx2-cx1) 263 | 264 | steps = int( abs(t2-t1) / (math.pi*2) * quality ) 265 | for i in range(1,steps): 266 | t = t1+i*(t2-t1)/steps 267 | yy,xx = cornu_yx(t,mirror) 268 | aa = math.atan2(yy-cy1,xx-cx1) 269 | ll = length(yy-cy1,xx-cx1) 270 | x = math.cos(aa-ca+a) * ll/cl*l +x1 271 | y = math.sin(aa-ca+a) * ll/cl*l +y1 272 | ppos.append(x) 273 | plow.append(y*2) 274 | phigh.append(y*2) 275 | 276 | ppos.append(pos[-1]) 277 | plow.append(low[-1]) 278 | phigh.append(high[-1]) 279 | return Profile(ppos, plow, phigh) 280 | 281 | 282 | 283 | def make_profile(spec): 284 | pos = [ ] 285 | low = [ ] 286 | high = [ ] 287 | for item in spec: 288 | if len(item) == 2: 289 | this_pos, this_low = item 290 | this_high = this_low 291 | else: 292 | this_pos, this_low, this_high = item 293 | pos.append(this_pos) 294 | low.append(this_low) 295 | high.append(this_high) 296 | return Profile(pos, low, high) 297 | 298 | if __name__ == '__main__': 299 | from .raphs_curves import cornu 300 | 301 | for i in range(20+1): 302 | t = i / 10.0 - 0.5 303 | 304 | y1,x1 = cornu.eval_cornu(t) 305 | y2,x2 = cornu.eval_cornu(t+1e-3) 306 | print(( t, (math.atan2(y2-y1,x2-x1)+math.pi) % (2*math.pi) -math.pi )) #Angle is t^2 307 | 308 | -------------------------------------------------------------------------------- /demakein/make_shawm.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | F-shawm 4 | theory 495.5 5 | 6 | design 438.4 7 | actual 399 8 | ratio 9 | 10 | """ 11 | 12 | 13 | import sys, os, math 14 | 15 | from . import config, design, make, profile, shape, pack, make_bauble 16 | 17 | 18 | @config.help("""\ 19 | Produce 3D models using the output of "demakein make-shawm:". 20 | """) 21 | @config.Bool_flag('dock', 'Reed docking thing at top. (Not needed for drinking straw reed.)') 22 | @config.Float_flag('dock_top', 'Diameter at top of dock.') 23 | @config.Float_flag('dock_bottom', 'Diameter at bottom of dock, should hold reed snugly.') 24 | @config.Float_flag('dock_length', 'Length of dock.') 25 | @config.Bool_flag('decorate', 'Add some decorative rings.') 26 | @config.Bool_flag('bauble', 'Add a gratuitous bauble to the end.') 27 | class Make_reed_instrument(make.Make_millable_instrument): 28 | dock = False 29 | 30 | #7,5 was too small 31 | #9,6 was just a tad loose 32 | dock_top = 8.5 33 | dock_bottom = 5.5 34 | dock_length = 15 35 | 36 | decorate = False 37 | bauble = False 38 | 39 | def run(self): 40 | spec = self.working.spec 41 | 42 | length = spec.length 43 | 44 | #length = spec.inner.pos[-2] 45 | #if self.working.designer.do_trim: 46 | # length *= 0.855 #Reed accounts for part of length, etc 47 | # # 0.865 was still rather short (Feb 2014) 48 | # # 0.875 needed a rather short reed (shapeways tenor, November 2012) 49 | 50 | #length = spec.inner.pos[-2] 51 | #assert spec.inner(length) == self.working.designer.bore 52 | #self.log.log('Generated shape is %.0f%% of length of simulated instrument, rest is reed.\n' % (length*100.0 / spec.length)) 53 | #self.log.log('(Effective) reed length: %.1fmm\n' % (spec.length-length)) 54 | 55 | outer_profile = spec.outer #.clipped(0,length) 56 | inner_profile = spec.inner #.clipped(0,length) 57 | 58 | if self.dock: 59 | dock_inner = profile.make_profile([ 60 | [0.0, self.dock_bottom], 61 | [self.dock_length, self.dock_top], 62 | ]) 63 | 64 | dock_outer = profile.make_profile([ 65 | [length - 5.0, outer_profile(length)], 66 | [length, self.dock_top + 5.0], 67 | [length + self.dock_length, self.dock_top + 5.0], 68 | ]) 69 | 70 | inner_profile = inner_profile.appended_with(dock_inner) 71 | outer_profile = outer_profile.max_with(dock_outer) 72 | 73 | 74 | m = outer_profile.maximum() 75 | 76 | if self.bauble: 77 | end_dock_length = 5.0 78 | print('Bauble dock: %.1fmm diameter, %.1fmm length' % (m, end_dock_length)) 79 | bauble = make_bauble.Make_bauble(self.working_dir,dock_length=end_dock_length,dock_diameter=m).run() 80 | fixer = profile.make_profile([(0.0,m),(end_dock_length,m),(end_dock_length*2,0.0)]) 81 | outer_profile = outer_profile.max_with(fixer) 82 | 83 | if not self.dock: 84 | dock_diam = self.working.designer.bore * 4.0 85 | dock_length = 20.0 86 | fixer = profile.make_profile([(length-dock_length*1.25,0.0),(length-dock_length,dock_diam)]) 87 | outer_profile = outer_profile.max_with(fixer) 88 | 89 | if self.decorate: 90 | if not self.bauble: 91 | outer_profile = make.decorate(outer_profile, 0.0, 1.0, 0.05) 92 | outer_profile = make.decorate(outer_profile, length-dock_length*1.25, -1.0, 0.15) 93 | 94 | n_holes = self.working.designer.n_holes 95 | 96 | 97 | 98 | self.make_instrument( 99 | inner_profile=inner_profile.clipped(-50,inner_profile.end()+50), 100 | outer_profile=outer_profile, 101 | hole_positions=spec.hole_positions, 102 | hole_diameters=spec.hole_diameters, 103 | hole_vert_angles=spec.hole_angles, 104 | hole_horiz_angles=self.working.designer.hole_horiz_angles, 105 | xpad = [ 0.0 ] * n_holes, 106 | ypad = [ 0.0 ] * n_holes, 107 | with_fingerpad = self.working.designer.with_fingerpad, 108 | ) 109 | 110 | if self.bauble: 111 | bauble.rotate(1,0,0, 180) 112 | bauble.move(0,0,end_dock_length) 113 | binst = self.working.instrument.copy() 114 | binst.add(bauble) 115 | self.save(binst, 'baubled-instrument') 116 | 117 | self.make_parts(up = True) 118 | 119 | 120 | #if not self.mill: 121 | # self.segment([ cut1, cut3, cut5 ], up=True) 122 | # self.segment([ cut2, cut4 ], up=True) 123 | # self.segment([ cut3 ], up=True) 124 | #else: 125 | # pack.cut_and_pack( 126 | # self.working.outside, self.working.bore, 127 | # upper_segments, lower_segments, 128 | # xsize=self.mill_length, 129 | # ysize=self.mill_width, 130 | # zsize=self.mill_thickness, 131 | # bit_diameter=self.mill_diameter, 132 | # save=self.save, 133 | # ) 134 | 135 | #self.working.instrument.rotate(0,1,0, 180) 136 | #self.working.instrument.move(0,0,length) 137 | #need to flip spec 138 | #self.segment([ length-cut3, length-cut2, length-cut1 ], length) 139 | 140 | 141 | #shape.Make_instrument.run(self) 142 | # 143 | #designer = self.designer 144 | #spec = self.instrument 145 | # 146 | ##length = spec.length * 0.91 # Shorten to allow reed effective length. TODO: Tune this 147 | #length = spec.length * 0.875 # Based on trimming alto_shawm 148 | #width = max(spec.outer.low) 149 | # 150 | #instrument = shape.extrude_profile(spec.outer.clipped(0,length)) 151 | #outside = shape.extrude_profile(spec.outer.clipped(0,length)) 152 | #bore = shape.extrude_profile(spec.inner.clipped(-50,length+50)) 153 | # 154 | #xpad = [ 0.0 ] * 8 155 | #ypad = [ 0.0 ] * 8 156 | #angle = [ -20.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 180.0 ] 157 | #drill_outside = [ 1 ] + [ 0 ] * 7 158 | # 159 | #for i, pos in enumerate(spec.hole_positions): 160 | # height = spec.outer(pos)*0.5 + 4.0 161 | # hole = shape.prism( 162 | # height, spec.hole_diameters[i], 163 | # shape.squared_circle(xpad[i], ypad[i]).with_effective_diameter 164 | # ) 165 | # hole.rotate(1,0,0, -90) 166 | # hole.rotate(0,0,1, angle[i]) 167 | # hole.move(0,0,pos) 168 | # bore.add(hole) 169 | # if drill_outside[i]: 170 | # outside.remove(hole) 171 | # 172 | #instrument.remove(bore) 173 | # 174 | #self.save(instrument,'instrument.stl') 175 | # 176 | #shapes = pack.cut_and_pack( 177 | # outside, bore, 178 | # self.SCHEMES[self.scheme][0], self.SCHEMES[self.scheme][1], 179 | # xsize=self.SCHEMES[self.scheme][2], ysize=self.SCHEMES[self.scheme][3], zsize=self.thickness, 180 | # bit_diameter=3.0, 181 | # res=10, 182 | # output_dir=self.output_dir 183 | #) 184 | # 185 | #shape.show_only(instrument, *shapes) 186 | # 187 | 188 | #if self.forms == 1: 189 | # lower, upper = shape.make_formwork( 190 | # outside, bore, length, 191 | # [ 0.0, 0.65, 1.0 ], 192 | # [ 0.0, 0.4, 1.0 ], 193 | # 3.0, 6.0, thickness[0], 200.0 194 | # ) 195 | # 196 | # self.save(lower,'lower.stl') 197 | # self.save(upper,'upper.stl') 198 | # 199 | # shape.show_only(instrument, lower, upper) 200 | # 201 | #elif self.forms == 2: 202 | # lower1, upper1 = shape.make_formwork( 203 | # outside, bore, length, 204 | # [ 0.0, 0.3, 0.6, 1.0 ], 205 | # [ ], 206 | # 3.0, 6.0, thickness[0], 200.0 207 | # ) 208 | # lower2, upper2 = shape.make_formwork( 209 | # outside, bore, length, 210 | # [ ], 211 | # [ 0.0, 0.4, 0.7, 1.0 ], 212 | # 3.0, 6.0, thickness[1], 200.0 213 | # ) 214 | # 215 | # self.save(lower1,'lower1.stl') 216 | # self.save(upper1,'upper1.stl') 217 | # self.save(lower2,'lower2.stl') 218 | # self.save(upper2,'upper2.stl') 219 | # 220 | # shape.show_only(instrument, lower1,upper1,lower2,upper2) 221 | # 222 | #else: 223 | # assert False, 'Unsupported number of forms' 224 | 225 | 226 | @config.help('Make a dock extender, to adjust tuning.') 227 | @config.Float_flag('bore', 'Bore diameter.') 228 | @config.Float_flag('dock_top', 'Diameter at top of dock.') 229 | @config.Float_flag('dock_bottom', 'Diameter at bottom of dock, should hold reed snugly.') 230 | @config.Float_flag('dock_length', 'Length of dock.') 231 | @config.Float_flag('extension', 'Extension amount.') 232 | @config.Float_flag('gap', 'Amount of gap all around, to allow extender to fit into dock.') 233 | class Make_dock_extender(make.Make): 234 | bore = 4.0 235 | dock_top = 8.5 236 | dock_bottom = 5.5 237 | dock_length = 15.0 238 | extension = 10.0 239 | gap = 0.2 240 | 241 | def run(self): 242 | dock_inner = profile.make_profile([ 243 | [-5.0, self.dock_top], 244 | [self.dock_length, self.dock_bottom, self.bore], 245 | [self.dock_length + self.extension + 5.0, self.bore], 246 | ]) 247 | 248 | dock_outer = profile.make_profile([ 249 | [0.0, self.dock_top+5.0], 250 | [self.extension, self.dock_top+5.0, self.dock_top - self.gap*2.0], 251 | [self.extension+self.dock_length, self.dock_bottom - self.gap*2.0], 252 | ]) 253 | 254 | inner = shape.extrude_profile(dock_inner) 255 | thing = shape.extrude_profile(dock_outer) 256 | thing.remove(inner) 257 | self.save(thing, "extender-%.1fmm" % self.extension) 258 | 259 | 260 | if __name__ == '__main__': 261 | shape.main_action(Make_shawm()) 262 | -------------------------------------------------------------------------------- /demakein/design_flute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import copy, math 4 | 5 | from . import profile, design 6 | 7 | from . import config 8 | 9 | pflute_fingerings = [ 10 | ('D4', [1,1,1,1,1,1]), 11 | ('E4', [0,1,1,1,1,1]), 12 | ('F4', [1,0,1,1,1,1]), 13 | ('F#4', [1,1,0,1,1,1]), 14 | ('G4', [0,0,0,1,1,1]), 15 | ('G#4', [1,1,1,0,1,1]), 16 | ('A4', [0,0,0,0,1,1]), 17 | ('Bb4', [0,0,1,1,0,1]), 18 | ('B4', [0,0,0,0,0,1]), 19 | ('C5', [0,0,0,1,1,0]), 20 | ('C#5', [0,0,0,0,0,0]), #Note: 0,0,1,0,0,0 lets you put a finger down 21 | 22 | ('D5', [1,1,1,1,1,0]), 23 | ('D5', [1,1,1,1,1,1]), 24 | 25 | ('E5', [0,1,1,1,1,1]), 26 | ('F5', [1,0,1,1,1,1]), 27 | ('F#5', [0,1,0,1,1,1]), 28 | ('G5', [0,0,0,1,1,1]), 29 | #('G#5', [0,0,1,0,1,1]), 30 | ('A5', [0,0,0,0,1,1]), 31 | ('Bb5', [1,1,1,0,1,1]), 32 | ('B5', [0,1,1,0,1,1]), 33 | 34 | # ('C6', [1,1,1,0,1,0]), # 1,0,1,0,1,0 may also be good 35 | ('C6', [0,1,1,0,0,1]), 36 | #('C#6', [1,1,1,0,0,0]), 37 | 38 | ('D6', [1,1,1,1,1,1]), 39 | #('E6', [0,1,0,0,1,1]), 40 | ] 41 | 42 | folk_fingerings = [ 43 | ('D4', [1,1,1,1,1,1]), 44 | ('E4', [0,1,1,1,1,1]), 45 | ('F#4', [0,0,1,1,1,1]), 46 | ('G4', [0,0,0,1,1,1]), 47 | ('A4', [0,0,0,0,1,1]), 48 | ('B4', [0,0,0,0,0,1]), 49 | ('C5', [0,0,0,1,1,0]), 50 | ('C#5', [0,0,0,0,0,0]), 51 | ('D5', [1,1,1,1,1,0]), 52 | ('E5', [0,1,1,1,1,1]), 53 | ('F#5', [0,0,1,1,1,1]), 54 | ('G5', [0,0,0,1,1,1]), 55 | ('A5', [0,0,0,0,1,1]), 56 | ('B5', [0,0,0,0,0,1]), 57 | #('C#6', [1,1,1,0,0,0]), 58 | ('C#6', [0,0,0,0,0,0]), 59 | ('D6', [1,1,1,1,1,0]), 60 | 61 | # ('D4*3', [1,1,1,1,1,1]), 62 | # ('E4*3', [0,1,1,1,1,1]), 63 | # ('F#4*3', [0,0,1,1,1,1]), 64 | # ('G4*3', [0,0,0,1,1,1]), 65 | # ('A4*3', [0,0,0,0,1,1]), 66 | #('B4*3', [0,0,0,0,0,1]), 67 | #('C#5*3', [0,0,0,0,0,0]), 68 | 69 | #('E6', [0,1,1,1,1,1]), 70 | #('F6', [1,0,1,1,1,1]), 71 | #('G6', [1,0,0,1,1,1]), 72 | #('A6', [0,1,1,1,1,0]), #? 73 | ] 74 | 75 | dorian_fingerings = [ 76 | ('D4', [1,1,1,1,1,1]), 77 | ('E4', [0,1,1,1,1,1]), 78 | ('F4', [0,0,1,1,1,1]), 79 | ('G4', [0,0,0,1,1,1]), 80 | ('A4', [0,0,0,0,1,1]), 81 | ('Bb4', [0,0,1,1,0,1]), 82 | ('B4', [0,0,0,0,0,1]), 83 | ('C5', [0,0,0,0,0,0]), 84 | ('D5', [1,1,1,1,1,1]), 85 | ('E5', [0,1,1,1,1,1]), 86 | ('F5', [0,0,1,1,1,1]), 87 | ('G5', [0,0,0,1,1,1]), 88 | ('A5', [0,0,0,0,1,1]), 89 | ('Bb5', [0,0,0,1,0,1]), 90 | ('B5', [0,0,0,0,0,1]), 91 | ('C6', [0,0,0,0,0,0]), 92 | ('D6', [1,1,1,1,1,1]), 93 | ] 94 | 95 | def fingerings_with_embouchure(fingerings): 96 | return [ 97 | (note, fingering+[0], *etc) 98 | for note, fingering, *etc in fingerings 99 | ] 100 | 101 | 102 | 103 | @config.Float_flag('embextra', 104 | 'Constant controlling extra effective height of the embouchure hole due to lips, etc. ' 105 | 'Small adjustments of this value will change the angle at which the flute needs to be blown ' 106 | 'in order to be in tune.') 107 | class Flute_designer(design.Instrument_designer): 108 | closed_top = True 109 | 110 | # 2.5/8 = 0.3 ~ 20 cents flat 111 | # 5/8 = 0.6 ~ too high 112 | # 0.45 (sop_flute_5) ~ a tiny bit low 113 | # 0.53 (sop_flute_8) ~ a tiny bit low?, needed to push cork in 1.5mm 114 | # + perfect, printed plastic sop flute 115 | # 0.56 ~ definitely high, printed plastic sop flute 116 | 117 | embextra = 0.53 118 | 119 | def patch_instrument(self, inst): 120 | inst = copy.copy(inst) 121 | inst.hole_lengths[-1] += inst.hole_diameters[-1] * self.embextra 122 | return inst 123 | 124 | def calc_emission(self, emission, fingers): 125 | """ Emission is relative to embouchure hole 126 | ie we assume the amplitude at the embouchure hole is fixed. """ 127 | return math.sqrt(sum(item*item for item in emission[:-1])) / emission[-1] 128 | 129 | 130 | # 131 | # @property 132 | # def hole_extra_height_by_diameter(self): 133 | # return [ 0.0 ] * 6 + [ self.embextra ] 134 | 135 | #hole_extra_height_by_diameter = [ 0.0 ] * 6 + [ 0.53 ] 136 | 137 | initial_length = design.wavelength('D4') * 0.5 138 | 139 | n_holes = 7 140 | 141 | @property 142 | def initial_hole_fractions(self): 143 | return [ 0.175 + 0.5*i/(self.n_holes-1) for i in range(self.n_holes-1) ] + [ 0.97 ] 144 | 145 | 146 | # min_hole_diameters = design.sqrt_scaler([ 6.5 ] * 6 + [ 12.2 ]) 147 | # max_hole_diameters = design.sqrt_scaler([ 11.4 ] * 6 + [ 13.9 ]) 148 | # max_hole_diameters = design.sqrt_scaler([ 11.4 ] * 6 + [ 10.5 ]) 149 | 150 | # min_hole_diameters = design.power_scaler(1/3., [ 3.0 ] * 6 + [ 11.3 ]) 151 | # max_hole_diameters = design.power_scaler(1/3., [ 11.4 ] * 6 + [ 11.4 ]) 152 | 153 | @property 154 | def min_hole_diameters(self): 155 | x = [3.0]*(self.n_holes-1) + [11.3] 156 | scale = self.scale ** (1./3) 157 | return [ item*scale for item in x ] 158 | 159 | @property 160 | def max_hole_diameters(self): 161 | x = [11.4]*(self.n_holes-1) + [11.4] 162 | scale = self.scale ** (1./3) 163 | return [ item*scale for item in x ] 164 | 165 | 166 | # These assume a six hole flute, and must be overridden on flutes with a different number of holes: 167 | 168 | hole_horiz_angles = [0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0] 169 | 170 | divisions = [ 171 | [ (5, 0.0) ], 172 | [ (2, 0.0), (5, 0.333) ], 173 | [ (-1, 0.9), (2, 0.0), (5, 0.333), ], 174 | [ (-1, 0.9), (2,0.0), (5,0.0), (5,0.7) ], 175 | ] 176 | 177 | 178 | @config.Float_flag('inner_taper', 'Amount of tapering of bore. Smaller = more tapered.') 179 | @config.Float_flag('outer_taper', 'Amount of tapering of exterior. Smaller = more tapered.') 180 | class Tapered_flute(Flute_designer): 181 | inner_taper = 0.75 182 | outer_taper = 0.85 183 | 184 | #inner_diameters = design.sqrt_scaler([ 14.0, 14.0, 18.4, 21.0, 18.4, 18.4 ]) 185 | @property 186 | def inner_diameters(self): 187 | scale = self.scale ** (1./2) 188 | return [ 189 | 18.4 * self.inner_taper * scale, 190 | 18.4 * self.inner_taper * scale, 191 | 18.4 * (0.5+self.inner_taper*0.5) * scale, 192 | 18.4 * scale, 193 | 21.0 * scale, 194 | 21.0 * scale, 195 | 18.4 * scale, 196 | 18.4 * scale, 197 | ] 198 | 199 | #initial_inner_fractions = [ 0.25, 0.75 ] 200 | #min_inner_fraction_sep = [ 0.0, 0.0, 0.0 ] 201 | 202 | initial_inner_fractions = [ 0.25, 0.3, 0.7, 0.8,0.81, 0.9 ] 203 | min_inner_fraction_sep = [ 0.01, 0.1,0.1, 0.01, 0.01, 0.01, 0.01 ] 204 | 205 | #outer_diameters = design.sqrt_scaler([ 22.1, 32.0, 26.1 ]) 206 | @property 207 | def outer_diameters(self): 208 | scale = self.scale ** (1./2) 209 | return [ 210 | 29.0 * self.outer_taper * scale, 211 | 29.0 * self.outer_taper * scale, 212 | 29.0 * scale, 213 | 29.0 * scale, 214 | #30.0 * scale, 215 | #30.0 * scale, 216 | #32.0 * scale, 217 | #29.0 * scale, 218 | ] 219 | 220 | initial_outer_fractions = [ 0.01, 0.666 ] 221 | min_outer_fraction_sep = [ 0.0, 0.5, 0.0 ] #Looks and feels nicer 222 | 223 | # 224 | #class Straight_flute(Flute_designer): 225 | # inner_diameters = design.sqrt_scaler([ 18.4, 18.4, 21.0, 18.4, 17.0 ]) 226 | # initial_inner_fractions = [ 0.7, 0.8, 0.9 ] 227 | # min_inner_fraction_sep = [ 0.5, 0.03, 0.0, 0.0 ] 228 | # # Note constraint of bulge to upper half of tube. 229 | # # There seems to be an alternate solution for the folk flute 230 | # # where it's stretched out over 3/4 of the flute's length. 231 | # 232 | # outer_diameters = design.sqrt_scaler([ 28.0, 28.0 ]) 233 | # 234 | # #initial_outer_fractions = [ 0.666 ] 235 | # #min_outer_fraction_sep = [ 0.666, 0.0 ] #Looks and feels nicer 236 | # 237 | 238 | @config.help( 239 | 'Design a flute with a recorder-like fingering system.' 240 | ) 241 | class Design_pflute(Tapered_flute): 242 | fingerings = fingerings_with_embouchure(pflute_fingerings) 243 | balance = [ 0.1, None, None, 0.05 ] 244 | #hole_angles = [ -30.0, -30.0, 30.0, -30.0, 30.0, -30.0, 0.0 ] 245 | #hole_angles = [ 30.0, -30.0, 30.0, 0.0, 0.0, 0.0, 0.0 ] 246 | hole_angles = [ 30.0, -30.0, 30.0, 0.0, 0.0, 0.0, 0.0 ] 247 | 248 | max_hole_spacing = design.scaler([ 45.0, 45.0, None, 45.0, 45.0, None ]) 249 | 250 | 251 | @config.help( 252 | 'Design a flute with a pennywhistle-like fingering system.' 253 | ) 254 | class Design_folk_flute(Tapered_flute): 255 | fingerings = fingerings_with_embouchure(folk_fingerings) 256 | balance = [ 0.01, None, None, 0.01 ] 257 | hole_angles = [ -30.0, 30.0, 30.0, -30.0, 0.0, 30.0, 0.0 ] 258 | #min_hole_diameters = design.sqrt_scaler([ 7.5 ] * 6 + [ 12.2 ]) 259 | #max_hole_diameters = design.sqrt_scaler([ 11.4 ] * 6 + [ 13.9 ]) 260 | 261 | max_hole_spacing = design.scaler([ 35.0, 35.0, None, 35.0, 35.0, None ]) 262 | 263 | 264 | # 265 | #class With_tuning_holes(Design_pflute): 266 | # #@property 267 | # #def n_holes(self): 268 | # # x = super(With_tuning_holes,self).n_holes 269 | # # print x 270 | # # return 1+x 271 | # 272 | # tuning_holes = 2 273 | # 274 | # @property 275 | # def min_hole_diameters(self): 276 | # x = super(With_tuning_holes,self).min_hole_diameters 277 | # return [ 0.1 ]*self.tuning_holes+x 278 | # @property 279 | # def max_hole_diameters(self): 280 | # x = super(With_tuning_holes,self).max_hole_diameters 281 | # d = self.inner_diameters[0] 282 | # return [d*0.5]*self.tuning_holes+x 283 | # @property 284 | # def balance(self): 285 | # x = super(With_tuning_holes,self).balance 286 | # return [None]*self.tuning_holes+x 287 | # @property 288 | # def max_hole_spacing(self): 289 | # x = super(With_tuning_holes,self).max_hole_spacing 290 | # return [None]*self.tuning_holes+x 291 | # @property 292 | # def initial_hole_fractions(self): 293 | # x = super(With_tuning_holes,self).initial_hole_fractions 294 | # return [ (i+1.0)/(self.tuning_holes+1.0) for i in xrange(self.tuning_holes) ]+x 295 | # @property 296 | # def hole_angles(self): 297 | # x = super(With_tuning_holes,self).hole_angles 298 | # return [0.0]*self.tuning_holes+x 299 | # @property 300 | # def fingerings(self): 301 | # x = super(With_tuning_holes,self).fingerings 302 | # return [ (a,[0]*self.tuning_holes+b) for a,b in x ] 303 | # 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | --------------------------------------------------------------------------------