├── .gitignore ├── README.md ├── assets ├── 03_thread_and_tool.jpg ├── 05_fabric.jpg ├── exportSample.png ├── thread feeder-03.svg ├── thread feeder-04.svg ├── thread feeder.ai ├── youtube-presentation.png └── youtube-preview.png ├── projects ├── tp00_boundaryTester │ └── tp00_boundaryTester.py ├── tp01_circleTester │ └── tp01_circleTester.py └── tp02_convertImg │ ├── 1200px-Apple-tree_blossoms_2017_G3.jpg │ └── tp02_convertImg.py ├── threadPlotter ├── LICENSE.txt ├── README.md ├── __init__.py ├── build │ └── lib │ │ └── threadPlotter │ │ ├── DirectAuthoringGenerator.py │ │ ├── TP_punchneedle │ │ ├── GridImgConverter.py │ │ ├── __init__.py │ │ ├── embroideryCalculation.py │ │ ├── embroidery_thread_color.csv │ │ ├── threadColor.pkl │ │ └── threadColorManagement.py │ │ ├── TP_structure │ │ ├── PathList.py │ │ ├── Point.py │ │ ├── PunchGroup.py │ │ └── __init__.py │ │ ├── TP_utils │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── clipperHelper.py │ │ ├── fillPath.py │ │ ├── shapeEditing.py │ │ └── svg.py │ │ ├── ThreadPlotter.py │ │ ├── __init__.py │ │ └── updateColor.py ├── dist │ ├── threadPlotter-0.0.2b0-py2.py3-none-any.whl │ ├── threadPlotter-0.0.2b0.tar.gz │ ├── threadPlotter-0.0.2b1-py2.py3-none-any.whl │ ├── threadPlotter-0.0.2b1.tar.gz │ ├── threadPlotter-0.0.2b2-py2.py3-none-any.whl │ └── threadPlotter-0.0.2b2.tar.gz ├── manifest.in ├── setup.cfg ├── setup.py ├── threadPlotter.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── requires.txt │ └── top_level.txt └── threadPlotter │ ├── DirectAuthoringGenerator.py │ ├── TP_punchneedle │ ├── GridImgConverter.py │ ├── __init__.py │ ├── embroideryCalculation.py │ ├── embroidery_thread_color.csv │ ├── original_only.pkl │ ├── original_thread_list.pkl │ ├── threadColor.pkl │ └── threadColorManagement.py │ ├── TP_structure │ ├── PathList.py │ ├── Point.py │ ├── PunchGroup.py │ └── __init__.py │ ├── TP_utils │ ├── __init__.py │ ├── basic.py │ ├── clipperHelper.py │ ├── fillPath.py │ ├── shapeEditing.py │ └── svg.py │ ├── ThreadPlotter.py │ ├── __init__.py │ └── updateColor.py └── tutorial ├── step1_plotterCheck.md ├── step2_physicalSetup.md ├── step3_patternMaking.md └── step4_advancedExamples.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThreadPlotter 2 | 3 | 4 | A toolkit for the design and fabrication of delicate punch needle embroidery using X-Y plotters_ 5 | 6 | ## What? 7 | 8 | ThreadPlotter is a toolkit that supports the designing, editing, and printing of images as punch needle embroidery using an X-Y plotter. It is a supplementary material for the paper: 9 | 10 | ["Plotting with Thread: Fabricating Delicate Punch Needle Embroidery with X-Y Plotters" 11 | Shiqing He, Eytan Adar, to appear, DIS'20, Honorable Mention Award](http://www.cond.org/punchneedle.html) 12 | 13 | The following video briefly introduces the motivation for building this tool and the capability of the ThreadPlotter. 14 | 15 | [![youtube-preview](assets/youtube-preview.png)](https://www.youtube.com/watch?v=zMfiQarMp-8) 16 | 17 | You might also be interested in this 10 minutes presentation that goes over the project in depth. 18 | 19 | 20 | [![youtube-presentation](assets/youtube-presentation.png)](https://www.youtube.com/watch?v=jvuNcWv8kGo) 21 | 22 | 23 | If you are interested in using this toolkit, please consider citing our paper:[Plotting with Thread: Fabricating Delicate Punch Needle Embroidery with X-Y Plotters](http://www.cond.org/punchneedle.html) 24 | 25 | ## How? 26 | 27 | To convert your X-Y plotter into a punch needle fabricator, we will follow the following steps: 28 | 1. Ensure that your plotter is suitable for the task. ([tutorial 1](tutorial/step1_plotterCheck.md)) 29 | 2. Acquire or create several physical components such as needle, fabric, and frame. ([tutorial 2](tutorial/step2_physicalSetup.md)) 30 | 3. Design a punch needle pattern. 31 | 1. [tutorial 3: pattern making overview](tutorial/step3_patternMaking.md) 32 | 2. [tutorial 4: advanced examples](tutorial/step4_advancedExamples.md) #in progress 33 | 34 | We highly recommend that you review our paper before getting started. When you are ready, click on each of the links above. 35 | 36 | ## Show us! Tell us! Ask us! Credit us! 37 | We are excited to see what you can create with this fabrication technique. The toolkit is developed and tested by Licia (on her plotter called "Kitty"). If you have questions about the toolkit, feel free to open up an issue in our [github page](https://github.com/LiciaHe/threadPlotter). 38 | 39 | If you created something and want to share it with us, please use the tag [#plotterembroidery](https://www.instagram.com/explore/tags/plotterembroidery/?hl=en) on SNS. 40 | 41 | ``` 42 | @inproceedings{10.1145/3357236.3395540, 43 | author = {He, Shiqing and Adar, Eytan}, 44 | title = {Plotting with Thread: Fabricating Delicate Punch Needle Embroidery with X-Y Plotters}, 45 | year = {2020}, 46 | isbn = {9781450369749}, 47 | publisher = {Association for Computing Machinery}, 48 | address = {New York, NY, USA}, 49 | url = {https://doi.org/10.1145/3357236.3395540}, 50 | doi = {10.1145/3357236.3395540}, 51 | booktitle = {Proceedings of the 2020 ACM Designing Interactive Systems Conference}, 52 | pages = {1047–1057}, 53 | numpages = {11}, 54 | keywords = {plotter, craft fabrication, embroidery fabrication, punch needle embroidery, craft design, x-y plotter, fiber art}, 55 | location = {Eindhoven, Netherlands}, 56 | series = {DIS ’20} 57 | } 58 | 59 | 60 | ``` 61 | 62 | ### License 63 | 64 | MIT License 65 | 66 | Copyright (c) [2020] [Shiqing He] 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy 69 | of this software and associated documentation files (the "Software"), to deal 70 | in the Software without restriction, including without limitation the rights 71 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 72 | copies of the Software, and to permit persons to whom the Software is 73 | furnished to do so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 84 | SOFTWARE. 85 | -------------------------------------------------------------------------------- /assets/03_thread_and_tool.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/03_thread_and_tool.jpg -------------------------------------------------------------------------------- /assets/05_fabric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/05_fabric.jpg -------------------------------------------------------------------------------- /assets/exportSample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/exportSample.png -------------------------------------------------------------------------------- /assets/thread feeder-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | -------------------------------------------------------------------------------- /assets/thread feeder-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 15 | 17 | 20 | 23 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /assets/thread feeder.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/thread feeder.ai -------------------------------------------------------------------------------- /assets/youtube-presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/youtube-presentation.png -------------------------------------------------------------------------------- /assets/youtube-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/assets/youtube-preview.png -------------------------------------------------------------------------------- /projects/tp00_boundaryTester/tp00_boundaryTester.py: -------------------------------------------------------------------------------- 1 | ''' 2 | boundary test for the thread plotter 3 | ''' 4 | 5 | from threadPlotter.TP_utils import shapeEditing as SHAPE 6 | from threadPlotter.ThreadPlotter import ThreadPlotter as TP 7 | 8 | settings={ 9 | "name":"tp00_boundaryTester",#name of the project 10 | "baseSaveLoc":"C:/licia/art/generative/", #specify where to save the generated files 11 | "basic":{ 12 | #stores settings related to the canvas 13 | "width":10,#inches 14 | "height":10,#inches 15 | "toolsCt": 3,#how many colors 16 | "margins":{"l":2,"r":2,"t":2,"b":2},#in inches 17 | "unit": "inch", # support px, inch and mm. 18 | "displayInnerRect": False, #adding a boundary rectangle to the svg file (will not append to the python files) 19 | "displayOuterRect": False, 20 | "plotterDefaultSetting":{ 21 | "model":2 #according to https://axidraw.com/doc/py_api/#model 22 | } 23 | }, 24 | "spec":{ 25 | #stores any user-defined specs 26 | "segmentLength":0.04,#inches 27 | "trailStitchLength":0.15, 28 | "trailLoopDepthPerc":35, 29 | "plotterSettingRange":{ 30 | "speedPercRange":[20,80], 31 | "depthPercRange":[35,100],#The range(%) that the z axis can move. 100% corresponds to the longest stitch whereas 55% corresponds to the shortest stitch 32 | "distanceRange":[0.03,0.15] #inches 33 | } 34 | } 35 | 36 | } 37 | 38 | 39 | testPlotter=TP(settings) #create an instance 40 | boundaryRect = SHAPE.makeRectPoints( 41 | 0, 42 | 0, 43 | testPlotter.wh_m[0], 44 | testPlotter.wh_m[1], 45 | closed=True 46 | ) #making points for a rectangle. 47 | # wh_m is a list that stores the width and height within 48 | # the plotable area (exclude margin). 49 | # wh_m[0] is the width, and wh_m[1] is the height. 50 | # The value stored is in pixels. 51 | # If you use inch or mm, threadPlotter will convert your settings into px. 52 | 53 | testPlotter.initPunchGroup(0, boundaryRect) 54 | #pattern information are going to be stored as 55 | # PunchGroup instances. The initPunchGroup function takes 56 | # an id of the thread, and a list of (unprocessed) points 57 | 58 | 59 | testPlotter.saveFiles() 60 | #ThreadPlotter will process the path you provided 61 | # by segmenting it and connecting multiple punch groups. 62 | # Then, it will export svg and python to your selected directory. -------------------------------------------------------------------------------- /projects/tp01_circleTester/tp01_circleTester.py: -------------------------------------------------------------------------------- 1 | ''' 2 | boundary test for the thread plotter 3 | ''' 4 | 5 | from threadPlotter.TP_utils import shapeEditing as SHAPE 6 | 7 | from threadPlotter.ThreadPlotter import ThreadPlotter as TP 8 | import random 9 | settings={ 10 | "name":"tp01_circleTester",#name of the project 11 | "baseSaveLoc":"C:/licia/art/generative/", #specify where to save the generated files 12 | "basic":{ 13 | #stores settings related to the canvas 14 | "width":10,#inches 15 | "height":10,#inches 16 | "toolsCt": 3,#how many colors 17 | "margins":{"l":2,"r":2,"t":2,"b":2},#in inches 18 | "unit": "inch", # support px, inch and mm. 19 | "displayInnerRect": False, #adding a boundary rectangle to the svg file (will not append to the python files) 20 | "displayOuterRect": False, 21 | "plotterDefaultSetting":{ 22 | "model":2 #according to https://axidraw.com/doc/py_api/#model 23 | } 24 | }, 25 | "spec":{ 26 | #stores any user-defined specs 27 | "segmentLength":0.04,#inches 28 | "trailStitchLength":0.15, 29 | "trailLoopDepthPerc":35, 30 | "plotterSettingRange":{ 31 | "speedPercRange":[20,80], 32 | "depthPercRange":[35,100],#The range(%) that the z axis can move. 100% corresponds to the longest stitch whereas 55% corresponds to the shortest stitch 33 | "distanceRange":[0.03,0.15] #inches 34 | } 35 | } 36 | 37 | } 38 | 39 | 40 | 41 | testPlotter=TP(settings) #create an instance 42 | #construct a list of random colors 43 | colorList=[] 44 | for i in range(3): 45 | rgb=[random.randint(0,255) for j in range(3)] 46 | colorList.append(rgb) 47 | print(colorList) 48 | testPlotter.matchColor(colorList,allowMix=False) 49 | 50 | maxSize=min(testPlotter.wh_m)/2 #largest circle radius 51 | minSize=10 #smallest circle radius 52 | gap=8 #gap between each circle 53 | circleCt=int((maxSize-minSize)/gap)# how many circles we are going to draw 54 | 55 | for i in range(circleCt): 56 | r=minSize+i*gap 57 | circle=SHAPE.makeUniformPolygon(testPlotter.wh_m[0]/2,testPlotter.wh_m[1]/2,r,50,closed=True) #approximate a circle with a 50 side polygon 58 | colorId=testPlotter.getRandomToolId() 59 | testPlotter.initPunchGroup(colorId, circle) #get a random color and store it 60 | 61 | testPlotter.saveFiles()#export 62 | -------------------------------------------------------------------------------- /projects/tp02_convertImg/1200px-Apple-tree_blossoms_2017_G3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/projects/tp02_convertImg/1200px-Apple-tree_blossoms_2017_G3.jpg -------------------------------------------------------------------------------- /projects/tp02_convertImg/tp02_convertImg.py: -------------------------------------------------------------------------------- 1 | ''' 2 | boundary test for the thread plotter 3 | ''' 4 | 5 | from threadPlotter.ThreadPlotter import ThreadPlotter as TP 6 | from threadPlotter.TP_punchneedle.GridImgConverter import GridImgConverter 7 | from threadPlotter.TP_punchneedle.threadColorManagement import rgbStrToTriplet 8 | settings={ 9 | "name":"tp02_convertImg",#name of the project 10 | "baseSaveLoc":"C:/licia/art/generative/", #specify where to save the generated files 11 | "basic":{ 12 | #stores settings related to the canvas 13 | "width":10,#inches 14 | "height":10,#inches 15 | "toolsCt": 3,#how many colors 16 | "margins":{"l":2,"r":2,"t":2,"b":2},#in inches 17 | "unit": "inch", # support px, inch and mm. 18 | "displayInnerRect": False, #adding a boundary rectangle to the svg file (will not append to the python files) 19 | "displayOuterRect": False, 20 | "plotterDefaultSetting":{ 21 | "model":2 #according to https://axidraw.com/doc/py_api/#model 22 | } 23 | }, 24 | "spec":{ 25 | #stores any user-defined specs 26 | "segmentLength":0.04,#inches 27 | "trailStitchLength":0.15, 28 | "trailLoopDepthPerc":35, 29 | "plotterSettingRange":{ 30 | "speedPercRange":[20,80], 31 | "depthPercRange":[35,100],#The range(%) that the z axis can move. 100% corresponds to the longest stitch whereas 55% corresponds to the shortest stitch 32 | "distanceRange":[0.03,0.15] #inches 33 | } 34 | } 35 | } 36 | 37 | testPlotter=TP(settings) #create an instance 38 | #construct a list of random colors 39 | imageName="1200px-Apple-tree_blossoms_2017_G3.jpg" 40 | gic=GridImgConverter( 41 | imgLoc="", #stored in the same folder 42 | imgName=imageName, 43 | colorCount=testPlotter.toolsCt, 44 | imgSize=testPlotter.wh_m, 45 | pixelization_length=testPlotter.segmentLength 46 | ) 47 | 48 | pathByColor=gic.exportOrderedGrid() 49 | colorListRGBstr=pathByColor.keys() 50 | colorList=[rgbStrToTriplet(rgbStr) for rgbStr in colorListRGBstr] 51 | testPlotter.matchColor(colorList) 52 | for toolI,colorKey in enumerate(colorListRGBstr): 53 | for path in pathByColor[colorKey]: 54 | 55 | testPlotter.initPunchGroup( 56 | toolI, 57 | path, 58 | skipSegment=True 59 | ) 60 | 61 | gic.saveImg(testPlotter.getFullSaveLoc("processedImg_")) 62 | 63 | testPlotter.saveFiles()#export 64 | -------------------------------------------------------------------------------- /threadPlotter/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [Shiqing He] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /threadPlotter/README.md: -------------------------------------------------------------------------------- 1 | #ThreadPlotter 2 | _A toolkit for the design and fabrication of delicate punch needle embroidery using X-Y plotters_ 3 | 4 | ##What? 5 | ThreadPlotter is a toolkit that supports the designing, editing, and printing of images as punch needle embroidery using an X-Y plotter. It is a supplementary material for the paper: 6 | 7 | ["Plotting with Thread: Fabricating Delicate Punch Needle Embroidery with X-Y Plotters" 8 | Shiqing He, Eytan Adar, to appear, DIS'20, Honorable Mention Award](http://www.cond.org/punchneedle.html) 9 | 10 | The following video briefly introduces the motivation for building this tool and the capability of the ThreadPlotter. 11 | 12 | [![youtube-preview](../assets/youtube-preview.png)](http://www.youtube.com/watch?v=YOUTUBE_VIDEO_ID_HERE) 13 | 14 | You might also be interested in this 10 minutes presentation that goes over the project in depth. 15 | 16 | [![youtubePresentation](http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg)](http://www.youtube.com/watch?v=YOUTUBE_VIDEO_ID_HERE) 17 | 18 | If you are interested in using this toolkit, please consider citing our paper:[Plotting with Thread: Fabricating Delicate Punch Needle Embroidery with X-Y Plotters](http://www.cond.org/punchneedle.html) 19 | ``` 20 | #todo: add citation 21 | ``` 22 | ##How? 23 | To convert your X-Y plotter into a punch needle fabricator, we will follow the following steps: 24 | 1. Ensure that your plotter is suitable for the task. ([tutorial 1](../tutorial/step1_plotterCheck.md)) 25 | 2. Acquire or create several physical components such as needle, fabric, and frame. ([tutorial 2](../tutorial/step2_physicalSetup.md)) 26 | 3. Design a punch needle pattern. 27 | 1. [tutorial 3: pattern making overview](../tutorial/step3_patternMaking.md) 28 | 2. [tutorial 4: advanced examples](../tutorial/step4_advancedExamples.md) 29 | 30 | We highly recommend that you review our paper before getting started. When you are ready, click on each of the links above. 31 | 32 | ## Show us! Tell us! Ask us! Credit us! 33 | We are excited to see what you can create with this fabrication technique. The toolkit is developed and tested by Licia (on her plotter called "Kitty"). If you have questions about the toolkit, feel free to open up an issue in our [github page](https://github.com/LiciaHe/threadPlotter). 34 | 35 | If you created something and want to share it with us, please use the tag [#plotterembroidery](https://www.instagram.com/explore/tags/plotterembroidery/?hl=en) on SNS. 36 | ```angular2html 37 | #todo: citation will be available soon 38 | 39 | ``` 40 | 41 | ###License 42 | MIT License 43 | 44 | Copyright (c) [2020] [Shiqing He] 45 | 46 | Permission is hereby granted, free of charge, to any person obtaining a copy 47 | of this software and associated documentation files (the "Software"), to deal 48 | in the Software without restriction, including without limitation the rights 49 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 50 | copies of the Software, and to permit persons to whom the Software is 51 | furnished to do so, subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all 54 | copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 57 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 58 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 59 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 60 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 61 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 62 | SOFTWARE. 63 | -------------------------------------------------------------------------------- /threadPlotter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/__init__.py -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/DirectAuthoringGenerator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Stores basic settings for a svg-python generator 3 | Includes utility functions for storage and setup 4 | merged version of the DirectAuthoring and the MainGenerator 5 | Dealing with lists and does not check for valid path points 6 | ''' 7 | 8 | from threadPlotter.TP_utils import shapeEditing as SHAPE 9 | from threadPlotter.TP_utils import basic as UB 10 | from threadPlotter.TP_utils import svg as SVG 11 | 12 | import datetime,random 13 | 14 | class DirectAuthoringGenerator: 15 | #storage-related 16 | def initStorage(self, makeDate=True): 17 | self.saveLoc = self.baseSaveLoc + self.name + "/" 18 | UB.mkdir(self.saveLoc) 19 | # everything is stored here 20 | if makeDate: 21 | self.dateFolder = self.saveLoc + str(datetime.datetime.now().strftime("%Y-%m-%d")) + "/" 22 | UB.mkdir(self.dateFolder) 23 | def initBasedOnSettings(self, settings, nameKey="name", specKey="spec", basicSettingKey="basic"): 24 | self.settings = settings 25 | if nameKey: 26 | self.name = settings[nameKey] 27 | else: 28 | self.name = settings.name 29 | self.currentSpec = settings[specKey] if specKey else settings.currentSetting 30 | 31 | self.basicSettings = settings[basicSettingKey] if basicSettingKey else settings.basicSettings 32 | 33 | self.baseSaveLoc=self.settings["baseSaveLoc"] if "baseSaveLoc" in self.settings else "" 34 | self.initStorage() 35 | 36 | 37 | 38 | self.timeTag = str(datetime.datetime.now().strftime("%H%M%S")) + "_" + str(random.getrandbits(3)) 39 | self.timedLoc = self.dateFolder + self.timeTag + self.batchName + "/" 40 | UB.mkdir(self.timedLoc) 41 | self.unit=self.basicSettings["unit"] if "unit" in self.basicSettings else "inch" 42 | if "inchToPx" in self.basicSettings: 43 | self.i2p = self.basicSettings["inchToPx"] 44 | else: 45 | self.i2p = 96 46 | 47 | #tools and basic settings 48 | def generateRandomTools(self, toolCount=-1): 49 | ''' 50 | random colored tools 51 | :param toolCount: 52 | :return: 53 | ''' 54 | if toolCount < 0: 55 | toolCount = self.basicSettings["toolsCt"] 56 | self.tools = [] 57 | for i in range(toolCount): 58 | self.tools.append({ 59 | "idx": i, 60 | "stroke-width": 1, 61 | "stroke": 1, 62 | "fill": "none" 63 | }) 64 | ballPenColors = ["#000", "#ff0000", "#8000e8", "#1925ff", "#ffaa00", "#039c2e"] 65 | 66 | if toolCount <= len(ballPenColors): 67 | bpcSample = random.sample(ballPenColors, toolCount) 68 | else: 69 | bpcSample = [random.choice(ballPenColors) for i in range(toolCount)] 70 | for i in range(toolCount): 71 | self.tools[i]["stroke"] = bpcSample[i] 72 | def getRandomToolId(self): 73 | if hasattr(self, "tools"): 74 | return random.randint(0, len(self.tools) - 1) 75 | return -1 76 | 77 | 78 | #axidraw-python authoring 79 | def calculateAxidrawPosition(self,pt,withMargin=True): 80 | dotPathInInches = SHAPE.ptToInchStrWithTranslate(pt, self.i2p, self.marginInch["l"], self.marginInch["t"]) 81 | if not withMargin: 82 | dotPathInInches=SHAPE.ptToInchStr(pt,self.i2p,precision=2) 83 | return dotPathInInches 84 | 85 | def writeEmptyUpdate(self,writerIdx): 86 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 87 | self.axidrawWriters[writerIdx].write("ad.move(0,0)\n") 88 | def updateOptions(self,req,writerIdx): 89 | ''' 90 | update setting manually 91 | assume axidraws are initiated 92 | :param req: 93 | :return: 94 | ''' 95 | 96 | for key in req: 97 | self.axidrawWriters[writerIdx].write("ad.options." + key + "=" + str(req[key]) + "\n") 98 | self.axidrawWriters[writerIdx].write( "ad.update()\n") 99 | def addPenUp(self,writerIdx): 100 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 101 | def addPenDown(self,writerIdx): 102 | self.axidrawWriters[writerIdx].write("ad.pendown()\n") 103 | 104 | def addDrawPt(self,dotCenter,writerIdx,withMargin=True): 105 | ''' 106 | use only up and down 107 | :param dotCenter: 108 | :param toolIdx: 109 | :return: 110 | ''' 111 | self.addMoveTo(dotCenter, writerIdx, withMargin=withMargin) 112 | self.axidrawWriters[writerIdx].write("ad.pendown()\n") 113 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 114 | def addMoveTo(self,dotCenter, writerIdx, withMargin=True): 115 | dotPathInInches=self.calculateAxidrawPosition(dotCenter,withMargin=withMargin) 116 | self.axidrawWriters[writerIdx].write("ad.moveto(" + dotPathInInches[0] + "," + dotPathInInches[1] + ")\n") 117 | def addLineTo(self,pt,writerIdx,withMargin=True): 118 | dotPathInInches = self.calculateAxidrawPosition(pt, withMargin=withMargin) 119 | self.axidrawWriters[writerIdx].write("ad.lineto(" + dotPathInInches[0] + "," + dotPathInInches[1] + ")\n") 120 | def addPath(self,toAppend,d,baseSoup,additionalAttr): 121 | attr={"d": d} 122 | attr.update(additionalAttr) 123 | return SVG.addComponent(baseSoup, toAppend, "path", attr) 124 | def getSaveName(self, additionalTag=""): 125 | return self.timeTag + "_" + additionalTag 126 | def addDrawing(self,pathPoints,writerIdx): 127 | for i, pt in enumerate(pathPoints): 128 | if i == 0: 129 | self.addMoveTo(pt, writerIdx) 130 | else: 131 | self.addLineTo(pt,writerIdx) 132 | def addComment(self, comment, toolIdx): 133 | self.axidrawWriters[toolIdx].write("##" + comment + "\n") 134 | def initNewAxidrawWriter(self, additionalTag=""): 135 | 136 | 137 | currentName = self.getFullSaveLoc(additionalTag=additionalTag + "_" + str(len(self.axidrawWriters))) 138 | 139 | axidrawWriter = open(currentName + ".py", "w") 140 | self.axidrawWriters.append(axidrawWriter) 141 | axidrawWriter.write(self.axidrawHeader) 142 | self.updateOptions(self.normalSetting, len(self.axidrawWriters) - 1) 143 | self.writeEmptyUpdate(len(self.axidrawWriters) - 1) 144 | return len(self.axidrawWriters) - 1 145 | 146 | 147 | ##----save--- 148 | def appendToAxidrawCollection(self, pathPoints, toolIdx=-1): 149 | ''' 150 | adding a path into the path collection 151 | :param pathPoints: 152 | :param toolIdx: 153 | :return: 154 | ''' 155 | if not hasattr(self, "axidrawPathCollection"): 156 | self.axidrawPathCollection = [] 157 | for i in range(len(self.tools)): 158 | self.axidrawPathCollection.append([]) 159 | if toolIdx < 0: 160 | toolIdx = self.getRandomToolId() 161 | 162 | self.axidrawPathCollection[toolIdx].append(pathPoints) 163 | def closeFiles(self): 164 | if hasattr(self, "axidrawPathCollection"): 165 | for i, coll in enumerate(self.axidrawPathCollection): 166 | for pathPoints in coll: 167 | self.addDrawing(pathPoints, i) 168 | self.addDrawing(pathPoints, -1) 169 | self.addPath(self.svg.g, SHAPE.getStraightPath(pathPoints), self.svg, self.tools[i]) 170 | if hasattr(self, "toolSvgs"): 171 | self.addPath(self.toolSvgs[i].g, SHAPE.getStraightPath(pathPoints), self.toolSvgs[i], 172 | self.tools[i]) 173 | self.addMoveTo([0, 0], i, withMargin=False) 174 | 175 | for axidrawWriter in self.axidrawWriters: 176 | axidrawWriter.write("\nad.disconnect()\nprint('end')\n####") 177 | axidrawWriter.close() 178 | 179 | def saveFiles(self): 180 | self.closeFiles() 181 | if hasattr(self, "svg"): 182 | self.saveSvg(self.svg) 183 | if hasattr(self, "toolSvgs"): 184 | for i, s in enumerate(self.toolSvgs): 185 | self.saveSvg(s, str(i)) 186 | def getFullSaveLoc(self, additionalTag=""): 187 | return self.timedLoc + self.timeTag + "_" + additionalTag 188 | def saveSvg(self, soup, additionalTag=""): 189 | name = self.getFullSaveLoc(additionalTag) 190 | SVG.saveSVG(soup, fullPath=name + ".svg") 191 | 192 | def __init__(self,settings,batchName="",svg=True,toolSvg=True): 193 | self.batchName=batchName 194 | self.initBasedOnSettings(settings) 195 | self.axidrawWriters = [] 196 | defaultSettings = { 197 | "normalSetting": { 198 | "pen_pos_up": 60, 199 | "pen_pos_down": 20, 200 | "pen_delay_down": 20, 201 | }, 202 | "toolsSetting": { 203 | "pen_pos_up": 100, 204 | "pen_pos_down": 0, 205 | "pen_delay_down": 20 206 | } 207 | } 208 | for key in ["normalSetting", "toolsSetting"]: 209 | if key not in self.basicSettings: 210 | self.basicSettings[key] = defaultSettings[key] 211 | else: 212 | defaultSettings[key].update(self.basicSettings[key]) 213 | self.basicSettings[key] = defaultSettings[key] 214 | self.normalSetting = self.basicSettings["normalSetting"] 215 | self.toolsSetting = self.basicSettings["toolsSetting"] 216 | self.marginInch = self.basicSettings["margins"].copy() 217 | self.i2p = 96 if "i2p" not in self.basicSettings else self.basicSettings["i2p"] 218 | if self.i2p!=96: 219 | print("The inch to pixel rate is currently set to "+str(self.i2p)) 220 | self.toolsCt = self.basicSettings["toolsCt"] 221 | 222 | axidrawHeader = [ 223 | "'''\n auto-generated axidraw code using ThreadPlotter\n'''\nfrom pyaxidraw import axidraw\nimport " 224 | "time\nad " 225 | "=axidraw.AxiDraw()\nad.interactive()\nad.connect()\n" 226 | ] 227 | for k in self.basicSettings["plotterDefaultSetting"]: 228 | axidrawHeader.append("ad.options."+k+"="+str(self.basicSettings["plotterDefaultSetting"][k])+"\n") 229 | axidrawHeader.append("ad.update()\n") 230 | self.axidrawHeader="\n".join(axidrawHeader)+"\n" 231 | 232 | 233 | 234 | if svg: 235 | self.svg, self.wh, self.wh_m, self.boundaryRect, self.margins = SVG.makeBasicSvgWithFoundations( 236 | self.basicSettings,unit=self.unit,i2p=self.i2p) 237 | self.generateRandomTools() 238 | if toolSvg: 239 | self.toolSvgs=[] 240 | for i in range(len(self.tools)): 241 | svg, wh, whm, br, m = SVG.makeBasicSvgWithFoundations( 242 | self.basicSettings,unit=self.unit,i2p=self.i2p) 243 | self.toolSvgs.append(svg) 244 | for i in range(len(self.tools)): 245 | self.initNewAxidrawWriter() 246 | 247 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_punchneedle/GridImgConverter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Converting an image into a fixed grid layout 3 | ''' 4 | import PIL 5 | try: 6 | import Image 7 | except ImportError: 8 | from PIL import Image 9 | 10 | from scipy.spatial import Delaunay 11 | import numpy as np 12 | from threadPlotter.TP_punchneedle import threadColorManagement as TCM 13 | 14 | class GridImgConverter: 15 | def getIntensities(self,pixelization_length, pixels, i, j): 16 | total_red_intensity = total_green_intensity = total_blue_intensity = 0 17 | averaging_pixel_number = pixelization_length * pixelization_length 18 | # 19 | for k in range(0, pixelization_length): 20 | for l in range(0, pixelization_length): 21 | # print(pixels[i * pixelization_length + k, j * pixelization_length + l]) 22 | total_red_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][0] 23 | total_green_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][1] 24 | total_blue_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][2] 25 | # 26 | average_red_intensity = int(total_red_intensity / averaging_pixel_number) 27 | average_green_intensity = int(total_green_intensity / averaging_pixel_number) 28 | average_blue_intensity = int(total_blue_intensity / averaging_pixel_number) 29 | return average_red_intensity, average_green_intensity, average_blue_intensity 30 | def getIntensities_noAvg(self,pixelization_length, pixels, i, j): 31 | total_red_intensity = total_green_intensity = total_blue_intensity = 0 32 | averaging_pixel_number = pixelization_length * pixelization_length 33 | # 34 | for k in range(0, int(pixelization_length)): 35 | for l in range(0, int(pixelization_length)): 36 | # print(pixels[i * pixelization_length + k, j * pixelization_length + l]) 37 | total_red_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][0] 38 | total_green_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][1] 39 | total_blue_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][2] 40 | # 41 | average_red_intensity = int(total_red_intensity / averaging_pixel_number) 42 | average_green_intensity = int(total_green_intensity / averaging_pixel_number) 43 | average_blue_intensity = int(total_blue_intensity / averaging_pixel_number) 44 | return pixels[i * pixelization_length , j * pixelization_length ][0],pixels[i * pixelization_length , j * pixelization_length ][1],pixels[i * pixelization_length , j * pixelization_length ][2] 45 | 46 | def halftone(self,x_units,y_units,pixelization_length,pixels): 47 | dotLocations = [] 48 | dotColors = [] 49 | 50 | for i in range(0, x_units): 51 | col=[] 52 | self.gridImg.append(col) 53 | for j in range(0, y_units): 54 | average_red_intensity, average_green_intensity, average_blue_intensity = self.getIntensities( 55 | pixelization_length, pixels, i, j) 56 | x0 = i * pixelization_length 57 | y0 = j * pixelization_length 58 | x1 = i * pixelization_length + pixelization_length - 1 59 | y1 = j * pixelization_length + pixelization_length - 1 60 | cx = (x0 + x1) / 2.0 61 | cy = (y0 + y1) / 2.0 62 | dotLocations.append((cx, cy)) 63 | dotColors.append((average_red_intensity, average_green_intensity, average_blue_intensity)) 64 | col.append(((cx,cy),(average_red_intensity, average_green_intensity, average_blue_intensity))) 65 | return dotLocations,dotColors 66 | 67 | def halftone_noAvg(self, x_units, y_units, pixelization_length, pixels): 68 | dotLocations = [] 69 | dotColors = [] 70 | 71 | for i in range(0, x_units): 72 | col = [] 73 | self.gridImg.append(col) 74 | for j in range(0, y_units): 75 | r,g,b= self.getIntensities_noAvg( 76 | pixelization_length, pixels, i, j) 77 | x0 = i * pixelization_length 78 | y0 = j * pixelization_length 79 | x1 = i * pixelization_length + pixelization_length - 1 80 | y1 = j * pixelization_length + pixelization_length - 1 81 | cx = (x0 + x1) / 2.0 82 | cy = (y0 + y1) / 2.0 83 | dotLocations.append((cx, cy)) 84 | 85 | dotColors.append((r,g,b)) 86 | col.append(((cx,cy),(r,g,b))) 87 | 88 | return dotLocations, dotColors 89 | def __init__(self,imgLoc,imgName,colorCount=5,imgSize=(960,960), pixelization_length =4,grayScale=False,removeSmallCollections=True,quantize=True,idealColors=-1): 90 | ''' 91 | convert 92 | :param saveLoc: 93 | :param imgName: 94 | :param imgSize: 95 | :param gridSize: 96 | ''' 97 | img = Image.open(imgLoc + imgName) 98 | img=img.resize([int(xy) for xy in imgSize]) 99 | img = img.transpose(Image.FLIP_LEFT_RIGHT) 100 | if quantize: 101 | img= img.quantize(colorCount) 102 | self.quantize=quantize 103 | if grayScale: 104 | img=img.convert('LA') 105 | img=img.convert('RGB') 106 | self.img=img 107 | # img = img.convert('P', palette=Image.ADAPTIVE, colors=colorCount) 108 | self.gridImg = [] 109 | pixels = img.load() 110 | x_units = int(img.size[0] / pixelization_length) 111 | y_units = int(img.size[1] / pixelization_length) 112 | self.pixelization_length=pixelization_length 113 | dotLocations, dotColors=self.halftone_noAvg(x_units,y_units,pixelization_length,pixels) 114 | self.x_units=x_units 115 | self.y_units=y_units 116 | self.pathCollection={} 117 | 118 | if not self.quantize: 119 | #replacing close colors 120 | lim=80 121 | dotColors,colors=self.replaceColors(lim,dotColors) 122 | while idealColors>0 and len(colors)>idealColors: 123 | lim+=20 124 | dotColors, colors = self.replaceColors(lim, dotColors) 125 | 126 | 127 | for i,c in enumerate(dotColors): 128 | rgb="rgb("+",".join([str(int(xy)) for xy in c])+")" 129 | 130 | dots=[pt for j,pt in enumerate(dotLocations) if dotColors[j]==c] 131 | # dots.sort(key=lambda x:(x[1],x[0])) 132 | self.pathCollection[rgb]=dots 133 | if removeSmallCollections: 134 | keys=list(self.pathCollection.keys()) 135 | for c in keys: 136 | if len(self.pathCollection[c])<30: 137 | del self.pathCollection[c] 138 | print("finished calculation",colorCount,self.pathCollection.keys()) 139 | def saveImg(self,loc): 140 | print("save image to "+loc) 141 | self.img.save(loc + "convertedImg.png") 142 | def replaceColors(self,lim,dotColors): 143 | colorCollection = set(dotColors) 144 | colors = [] 145 | for color in colorCollection: 146 | rgb = TCM.rgbToString(color) 147 | colors.sort(key=lambda c: TCM.calculateColorDifference(color, c)) 148 | if len(colors) > 0 and TCM.calculateColorDifference(color, colors[0]) < lim: 149 | closeColor = colors[0] 150 | # replaceColors[rgb] = closeColor 151 | # replacing 152 | for i, c in enumerate(dotColors): 153 | if c == color: 154 | dotColors[i] = closeColor 155 | for col in self.gridImg: 156 | for j, (pt, c) in enumerate(col): 157 | if c == color: 158 | col[j] = (pt, closeColor) 159 | else: 160 | colors.append(color) 161 | return dotColors,colors 162 | def exportOrderedGrid(self): 163 | centerByColorAndRow={} 164 | for color in self.pathCollection: 165 | centers=self.pathCollection[color] 166 | maxY=max([pt[1] for pt in centers]) 167 | centerByColorAndRow[color] = [[] for i in range(int(maxY/self.pixelization_length)+1)] 168 | # print(lastPtY,) 169 | #assign to list 170 | for pt in centers: 171 | idx=int(pt[1]/self.pixelization_length) 172 | centerByColorAndRow[color][idx].append(pt) 173 | #sort 174 | for i,lst in enumerate(centerByColorAndRow[color]): 175 | reverseLst=i%2==1 176 | lst.sort(key=lambda pt:pt[0],reverse=reverseLst) 177 | print("finished sorting") 178 | return centerByColorAndRow 179 | 180 | def exportOrderedGridListVersion(self): 181 | ''' 182 | reconstruct ordered grid 183 | :return: 184 | ''' 185 | exportGrid=[] 186 | for i, col in enumerate(self.gridImg): 187 | rowCopy=[] 188 | for j,ptRgb in enumerate(col): 189 | rowCopy.append(ptRgb) 190 | if i%2==0: 191 | rowCopy=rowCopy[::-1] 192 | exportGrid.append(rowCopy) 193 | return exportGrid 194 | 195 | def exportDelaunayPath(self): 196 | pathsByColor={} 197 | for color in self.pathCollection: 198 | points=np.array(self.pathCollection[color]) 199 | 200 | tri = Delaunay(points) 201 | triPts = points[tri.simplices] 202 | orderedPts = [] 203 | traveled = set() 204 | for i, triangle in enumerate(triPts): 205 | waitlist = [] 206 | for j in range(3): 207 | ptStr = ",".join([str(xy) for xy in triangle[j]]) 208 | if i != len(triPts) - 1 and triangle[j] in triPts[i + 1]: 209 | waitlist.append(triangle[j]) 210 | elif ptStr not in traveled: 211 | orderedPts.append(list(triangle[j])) 212 | traveled.add(ptStr) 213 | for pt in waitlist: 214 | ptStr = ",".join([str(xy) for xy in pt]) 215 | if ptStr not in traveled: 216 | orderedPts.append(list(pt)) 217 | traveled.add(ptStr) 218 | pathsByColor[color]=orderedPts 219 | return pathsByColor 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_punchneedle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/build/lib/threadPlotter/TP_punchneedle/__init__.py -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_punchneedle/embroideryCalculation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | stores utility functions for embroidery-specific calculations 3 | ''' 4 | import threadPlotter.TP_utils.shapeEditing as SHAPE 5 | 6 | def makeConnectedDot(pathPoints,segmentLength,minDist): 7 | ''' 8 | converted from "convertPathsToConnectedDots" 9 | create punch needle points by 10 | keep spliting line.Will check minimal distance 11 | :param pathPoints: 12 | :param segmentLength: 13 | :return: 14 | ''' 15 | dotCenters=[] 16 | for i, pt in enumerate(pathPoints): 17 | if i==0: 18 | continue 19 | pt0=pathPoints[i-1] 20 | splitLines=SHAPE.splitSingleLine([pt0, pt], segmentLength,toPoint=True) 21 | for dotCenter in splitLines: 22 | dotCenters +=dotCenter 23 | i=1 24 | 25 | while i 0: 32 | complexPath=SHAPE.convertPathToComplexPointWithPathParser(pathString) 33 | for seg in complexPath: 34 | if len(seg) == 4: 35 | pts = self.convertCubicIntoLine(seg) 36 | self.points += pts 37 | elif len(seg)==2: 38 | pt = POINT.Point(seg[0], seg[1]) 39 | self.points.append(pt) 40 | else: 41 | print(seg,"is not a valid seg. It contains paths whose lengths are not 2 or 4") 42 | raise ValueError 43 | if "z" in pathString or "Z" in pathString: 44 | firstPt=self.points[0] 45 | pt = POINT.Point(firstPt.pt[0], firstPt.pt[1]) 46 | self.points.append(pt) 47 | self.closed=True 48 | # print("closedPath",self.getLength(),self.exportPlainList()) 49 | if len(self.points)>0: 50 | if self.points[0].roughEquals(self.points[-1]): 51 | self.closed=True 52 | self.getBBox() 53 | def appendPoint(self,x,y): 54 | pt = POINT.Point(x,y) 55 | self.points.append(pt) 56 | def appendPoints(self,pointList): 57 | for p in pointList: 58 | self.appendPoint(p[0],p[1]) 59 | 60 | def __len__(self): 61 | return len(self.points) 62 | def copy(self): 63 | ''' 64 | :return: a deep copy 65 | ''' 66 | return PathList(starterArray=self.exportPlainList()) 67 | def getPtByIdx(self,idx): 68 | return self.points[idx] 69 | 70 | def getBBox(self): 71 | ''' 72 | this bbox is x1,x2,y1,y2 though 73 | :return: 74 | ''' 75 | # print(self.points) 76 | bbox=SHAPE.getBoundaryBoxPtsVersion(self.exportPlainList()) 77 | 78 | self.bbox=bbox 79 | # print(bbox) 80 | self.w=bbox[1]-bbox[0] 81 | self.h=bbox[3]-bbox[2] 82 | self.isHorizontal=self.w>self.h 83 | return self.bbox 84 | def getBBoxWHversion(self): 85 | return [self.bbox[0],self.bbox[2],self.bbox[1]-self.bbox[0],self.bbox[3]-self.bbox[2]] 86 | 87 | # return SHAPE.getBBoxWHversion(self.exportPlainList()) 88 | def getLength(self): 89 | return len(self.points) 90 | def getCenter(self): 91 | ''' 92 | XMIN,XMAX YMIN YMAX 93 | :return: 94 | ''' 95 | self.getBBox() 96 | return [self.bbox[1] - self.bbox[0], self.bbox[3] + self.bbox[2]] 97 | def isSelfIntersecting(self): 98 | ''' 99 | Determine if a path is self intersecting 100 | using https://algs4.cs.princeton.edu/91primitives/ 101 | :return: 102 | ''' 103 | ##todo 104 | return False 105 | def isClosed(self): 106 | return self.closed 107 | def withinSizeLimit(self,wh): 108 | ''' 109 | return true if the width and height 110 | :param wh: 111 | :return: 112 | ''' 113 | # print(self.bbox,wh, self.bbox[1]-self.bbox[0]<=wh[0],self.bbox[3]-self.bbox[2]<=wh[1]) 114 | return self.bbox[1]-self.bbox[0]<=wh[0] and self.bbox[3]-self.bbox[2]<=wh[1] 115 | def withinBoundaryLimit(self,xmin,xmax,ymin,ymax): 116 | return self.bbox[0]>=xmin and self.bbox[1]<=xmax and self.bbox[2]>=ymin and self.bbox[3]<=ymax 117 | 118 | def adjustOriginToCenter(self): 119 | ''' 120 | this will translate everything along the 0,0 point 121 | :return: 122 | not automatically called by itself 123 | ''' 124 | # print(self.getPathString()) 125 | 126 | center=self.getCenter() 127 | self.translate(center[0],center[1]) 128 | self.getBBox() 129 | return center 130 | def adjustPathToLeftTop(self): 131 | leftTop=[self.bbox[0],self.bbox[2]] 132 | self.translate(-leftTop[0],-leftTop[1]) 133 | self.getBBox() 134 | def exportPlainList(self,precision=None): 135 | lst=[pt.pt.copy() for pt in self.points] 136 | if precision: 137 | lst=[[round(pt[0],precision),round(pt[1],precision)] for pt in lst] 138 | return lst 139 | def convertCubicIntoLine(self,seg,segLength=5): 140 | segPoints=SHAPE.splitSingleCubicIntoLinesPoints(seg,segLength) 141 | # print("segPoint",segPoints) 142 | pts=[] 143 | for lineSeg in segPoints: 144 | for pt in lineSeg: 145 | if type(pt[0])!=int and type(pt[0])!=float: 146 | print("pt is not numerical",pt,"seg",lineSeg) 147 | raise ValueError 148 | pt_point=POINT.Point(pt[0],pt[1]) 149 | if len(pts)>0 and pts[-1].roughEquals(pt_point): 150 | continue 151 | pts.append(pt_point) 152 | return pts 153 | def getPathString(self): 154 | # print(self.points) 155 | p = "M" + " L".join([str(pt) for pt in self.points]) 156 | if self.closed: 157 | return p + "Z" 158 | return p 159 | def translate(self,tx,ty): 160 | for pt in self.points: 161 | pt.translate(tx,ty,True) 162 | self.getBBox() 163 | def rotate(self,degree): 164 | for pt in self.points: 165 | # print(pt) 166 | pt.rotate(degree,True) 167 | self.getBBox() 168 | def rotateAroundPoint(self,center,degree): 169 | for pt in self.points: 170 | pt.rotateAroundPoint(center,degree,True) 171 | self.getBBox() 172 | def rotateAroundCenter(self,degree): 173 | center=[self.bbox[1]-self.bbox[0],self.bbox[3]-self.bbox[2]] 174 | self.rotateAroundPoint(center,degree) 175 | def scalePath(self,scaleFactor): 176 | for pt in self.points: 177 | pt.scalePointAccordingToCenter(scaleFactor, True) 178 | self.getBBox() 179 | def scalePathAccordingToCenter(self,center,scaleX,scaleY): 180 | for pt in self.points: 181 | pt.scalePointAccordingToCenter(center,scaleX,scaleY,True) 182 | self.getBBox() 183 | def offset(self,dist,jointType=None,offsetType=None): 184 | ''' 185 | :param dist: 186 | :return: 187 | ''' 188 | if not offsetType: 189 | offsetType = "CLOSEDPOLYGON" 190 | if not self.closed: 191 | offsetType = "OPENBUTT" 192 | if not jointType: 193 | jointType = "MITER" 194 | 195 | offsetList=CH.makeOffset(self.exportPlainList(),dist,offsetType=offsetType,jointType=jointType) 196 | if len(offsetList)>0: 197 | if self.closed or offsetType.upper().startswith("OPEN"): 198 | offsetList[0].append(offsetList[0][0].copy()) 199 | return offsetList[0] 200 | return offsetList 201 | def exportToStr(self): 202 | return "" 203 | 204 | def __str__(self): 205 | return SHAPE.getStraightPath(self.exportPlainList()) 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_structure/Point.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Stores a point 3 | ''' 4 | import math 5 | class NonNumericalError(Exception): 6 | 7 | def __init__(self, *args): 8 | if args: 9 | self.message=args[0] 10 | else: 11 | self.message="" 12 | def __str__(self): 13 | return "one of the location is not numerical:x and y\n"+str(self.message) 14 | class Point: 15 | def __init__(self,x,y): 16 | self.pt=[x,y] 17 | if not self.isNumerical(x) or not self.isNumerical(y): 18 | raise NonNumericalError(self.pt) 19 | # print(x,y) 20 | def __str__(self): 21 | precision=2 22 | return str(round(self.pt[0],precision)) + "," + str(round(self.pt[1],precision)) 23 | 24 | def copy(self): 25 | return Point(self.x(), self.y()) 26 | def toList(self): 27 | ''' 28 | return a clone of the pt list 29 | :return: 30 | ''' 31 | return [self.pt[0],self.pt[1]] 32 | 33 | def isNumerical(self,val): 34 | return type(val)==int or type(val)==float 35 | def x(self): 36 | return self.pt[0] 37 | def y(self): 38 | return self.pt[1] 39 | def roundPt(self,digit=2): 40 | # print(self.pt) 41 | self.pt[0]=round(self.pt[0],digit) 42 | self.pt[1]=round(self.pt[1],digit) 43 | return self.pt 44 | def rotate(self,angle,inPlace=False): 45 | rad = math.radians(angle) 46 | c = math.cos(rad) 47 | s = math.sin(rad) 48 | x_p = self.pt[0] * c - self.pt[1] * s, 3 49 | y_p = self.pt[0] * s + self.pt[1] * c, 3 50 | if inPlace: 51 | self.pt=[x_p,y_p] 52 | return [x_p,y_p] 53 | 54 | def rotateAroundPoint(self,origin,angleDegree,inPlace=False): 55 | """ 56 | Rotate a point counterclockwise by a given angle around a given origin. 57 | 58 | The angle should be given in radians. 59 | """ 60 | angleRad = math.radians(angleDegree) 61 | ox, oy = origin 62 | px, py = self.pt 63 | 64 | qx = ox + math.cos(angleRad) * (px - ox) - math.sin(angleRad) * (py - oy) 65 | qy = oy + math.sin(angleRad) * (px - ox) + math.cos(angleRad) * (py - oy) 66 | if inPlace: 67 | self.pt=[qx, qy] 68 | return [qx, qy] 69 | 70 | def scalePoints(self, scaleFactor,inPlace=False): 71 | ''' 72 | ASSUME SCALING FROM CENTER 73 | :param points: 74 | :param scaleFactor: 75 | :return: 76 | ''' 77 | scaled=[xy*scaleFactor for xy in self.pt] 78 | if inPlace: 79 | self.pt=scaled 80 | return scaled 81 | 82 | def scalePointAccordingToCenter(self, center, scaleX,scaleY,inPlace=False): 83 | scalePoints = [self.pt[0]*scaleX,self.pt[1]*scaleY] 84 | translateX = (1 - scaleX) * center[0] 85 | translateY = (1 - scaleY) * center[1] 86 | scaled=[scalePoints[0] + translateX, scalePoints[1] + translateY] 87 | if inPlace: 88 | self.pt=scaled 89 | return scaled 90 | def roughEquals(self,pt2): 91 | return str(self)==str(pt2) 92 | def translate(self,tx,ty,inplace=False): 93 | nPT=[self.pt[0]+tx,self.pt[1]+ty] 94 | if inplace: 95 | self.pt=nPT 96 | return nPT 97 | 98 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_structure/PunchGroup.py: -------------------------------------------------------------------------------- 1 | 2 | from threadPlotter.TP_structure.PathList import PathList 3 | from threadPlotter.TP_utils.shapeEditing import pressIntoABox 4 | from threadPlotter.TP_punchneedle import embroideryCalculation as EC 5 | class TransformError(Exception): 6 | def __init__(self, *args): 7 | if args: 8 | self.message=args[0] 9 | else: 10 | self.message="" 11 | def __str__(self): 12 | return self.message+" contains transformation. Make sure your svg has a flat structure(avoid applying transformations in groups),and all paths have no transformation." 13 | class InvalidPathPointInput(Exception): 14 | def __init__(self, *args): 15 | if args: 16 | self.message=args[0] 17 | else: 18 | self.message="" 19 | def __str__(self): 20 | return self.message+" is not a valid list, str, PathList object, or beautiful soup object." 21 | class PunchGroup(PathList): 22 | ''' 23 | contains one path 24 | modify path 25 | ''' 26 | 27 | def __init__(self,pathInput,id,toolId,skipSegment): 28 | ''' 29 | construct pathList 30 | Can only contain a path element 31 | :param path: 32 | ''' 33 | self.id = id 34 | self.toolId=toolId 35 | self.skipSegment=skipSegment 36 | if pathInput==None: 37 | PathList.__init__(self) 38 | elif isinstance(pathInput,list): 39 | #process from pathList 40 | PathList.__init__(self,starterArray=pathInput) 41 | elif isinstance(pathInput,PathList): 42 | PathList.__init__(self, starterArray=pathInput.exportPlainList()) 43 | elif isinstance(pathInput,str): 44 | PathList.__init__(self, pathString=pathInput) 45 | else: 46 | try: 47 | if "transform" in pathInput.attrs: 48 | raise TransformError(pathInput) 49 | d = pathInput.attrs["d"] 50 | PathList.__init__(self,pathString=d) 51 | except: 52 | raise InvalidPathPointInput(pathInput) 53 | self.originalPathList=self.exportPlainList().copy() 54 | 55 | def exportToPunchNeedleReadyPoints(self,segmentLength,boundaryRect,addStartingTrail=False,addEndingTrail=False,minDistance=2): 56 | ''' 57 | segment the punch groups by the segment length 58 | Tasks: 59 | 1. for each segment in the 60 | :return: a list of PunchPoint objects 61 | 62 | :param segmentLength: 63 | :param boundaryRect: XMIN,YMIN,XMAX,YMAX 64 | :param addStartingTrail: 65 | :param addEndingTrail: 66 | :param minDistance: 67 | :return: 68 | ''' 69 | plainPoints=self.exportPlainList(precision=2) 70 | if self.skipSegment: 71 | return plainPoints 72 | if addStartingTrail: 73 | plainPoints=[[0,0]]+plainPoints 74 | if addEndingTrail: 75 | plainPoints.append([0,0]) 76 | pressIntoABox(plainPoints,boundaryRect[0],boundaryRect[1],boundaryRect[2],boundaryRect[3]) 77 | 78 | dotList=EC.makeConnectedDot(plainPoints,segmentLength,minDistance) 79 | return dotList 80 | 81 | def exportTrailToAnotherPunchGroup(self,punchGroup2,trailLength,minDistance=2): 82 | ''' 83 | return a list of trail point centers that connects to pucnh group 2 84 | :param punchGroup2: 85 | :return: 86 | ''' 87 | try: 88 | thisPoint = self.points[-1].toList() 89 | nextPt=punchGroup2.getPtByIdx(0).toList() 90 | except IndexError: 91 | return [] 92 | dotList = EC.makeConnectedDot([thisPoint,nextPt], trailLength, minDistance) 93 | return dotList 94 | 95 | 96 | def restore(self): 97 | self.points=self.originalPathList.copy() 98 | self.getBBox() 99 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_structure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/build/lib/threadPlotter/TP_structure/__init__.py -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/build/lib/threadPlotter/TP_utils/__init__.py -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_utils/basic.py: -------------------------------------------------------------------------------- 1 | 2 | import pickle, json,time,os 3 | import random 4 | 5 | 6 | def rgbToHex(r,g,b): 7 | return '#%02x%02x%02x' % (r, g, b) 8 | def uniformFromRange(arr): 9 | return random.uniform(arr[0],arr[1]) 10 | def getRandomHex(): 11 | return "%06x" % random.randint(0, 0xFFFFFF) 12 | def getOrDefault(storage,key,default=None): 13 | if key in storage: 14 | return storage[key] 15 | return default 16 | 17 | 18 | def mkdir(path): 19 | if (not os.path.exists(path)): 20 | os.mkdir(path) 21 | return True 22 | return False 23 | def unitConvert(val,unit,i2p=96): 24 | i2cm=i2p/25.4 25 | multiplier=1 26 | if "in" in unit.lower(): 27 | multiplier=i2p 28 | elif "cm" in unit.lower(): 29 | multiplier=i2cm 30 | elif "mm" in unit.lower(): 31 | multiplier=i2cm*10 32 | return multiplier*val 33 | 34 | def linearScale(inputVal,domainArr,rangeArr): 35 | ''' 36 | d3 linearScale 37 | :param input: 38 | :param domainArr: 39 | :param rangeArr: 40 | :return: 41 | ''' 42 | inputDiff = domainArr[1] - domainArr[0] 43 | outputDiff = rangeArr[1] - rangeArr[0] 44 | if inputVal - domainArr[0] == 0: 45 | return rangeArr[0] 46 | return (inputVal - domainArr[0]) / inputDiff * outputDiff + rangeArr[0] 47 | def load_object(fileName): 48 | 49 | with open(fileName, 'rb') as inputF: 50 | obj = pickle.load(inputF) 51 | inputF.close() 52 | return obj 53 | def save_object(obj, filename): 54 | with open(filename, 'wb') as output: 55 | pickle.dump(obj, output, pickle.HIGHEST_PROTOCOL) 56 | def roundPoint(point): 57 | return [round(xy,2) for xy in point] 58 | 59 | def getPointFromComplex(complex): 60 | try: 61 | return roundPoint([complex.real,complex.img]) 62 | except Exception: 63 | return roundPoint([complex.real,complex.imag]) 64 | def pointEquals(p1,p2): 65 | return int(p1[0])==int(p2[0]) and int(p1[1])==int(p2[1]) 66 | 67 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_utils/clipperHelper.py: -------------------------------------------------------------------------------- 1 | 2 | import pyclipper 3 | 4 | 5 | OFFSETTYPES={ 6 | "CLOSEDPOLYGON":pyclipper.ET_CLOSEDPOLYGON, 7 | "CLOSEDLINE":pyclipper.ET_CLOSEDLINE, 8 | "OPENROUND":pyclipper.ET_OPENROUND, 9 | "OPENSQUARE":pyclipper.ET_OPENSQUARE, 10 | "OPENBUTT":pyclipper.ET_OPENBUTT 11 | } 12 | JOINTTYPES={ 13 | "MITER":pyclipper.JT_MITER, 14 | "ROUND":pyclipper.JT_ROUND, 15 | "SQUARE":pyclipper.JT_SQUARE 16 | } 17 | CLIPPER_TYPE={"intersection":pyclipper.CT_INTERSECTION,"union":pyclipper.CT_UNION,"difference":pyclipper.CT_DIFFERENCE,"xor":pyclipper.CT_XOR} 18 | FILL_TYPE={"evenodd":pyclipper.PFT_EVENODD,"positive":pyclipper.PFT_POSITIVE,"negative":pyclipper.PFT_NEGATIVE,"nonzero":pyclipper.PFT_NONZERO} 19 | 20 | 21 | def makeOffset(subjPoints,offset_width,offsetType="CLOSEDPOLYGON",jointType="MITER"): 22 | ''' 23 | http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Types/EndType.htm 24 | http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Types/JoinType.htm 25 | :param subjPoints: 26 | :param offset_width: 27 | :return: 28 | ''' 29 | offsetType=OFFSETTYPES[offsetType.upper()] 30 | jointType=JOINTTYPES[jointType.upper()] 31 | pco = pyclipper.PyclipperOffset() 32 | sub_s=pyclipper.scale_to_clipper(subjPoints) 33 | pco.AddPath(sub_s,jointType, offsetType) 34 | solution = pco.Execute(pyclipper.scale_to_clipper(offset_width)) 35 | return pyclipper.scale_from_clipper(solution) 36 | 37 | 38 | 39 | 40 | def makeClipper(subjs,clip,clipperTypeStr,s_fill_key="positive",c_fill_key="positive",subjClosed=True,s_multi=False,c_multi=False): 41 | ''' 42 | 43 | :param subjs: 44 | :param clip: window to be used for the cut 45 | :param clipperTypeStr: 46 | :param s_fill_key: 47 | :param c_fill_key: 48 | :param subjClosed: 49 | :param s_multi: 50 | :param c_multi: 51 | :return: 52 | ''' 53 | pc = pyclipper.Pyclipper() 54 | scaledSubj = pyclipper.scale_to_clipper(subjs) 55 | scaledClip = pyclipper.scale_to_clipper(clip) 56 | # print("clip",scaledClip) 57 | clipperType = CLIPPER_TYPE[clipperTypeStr] 58 | if s_multi: 59 | pc.AddPaths(scaledSubj, pyclipper.PT_SUBJECT, subjClosed) 60 | else: 61 | pc.AddPath(scaledSubj, pyclipper.PT_SUBJECT, subjClosed) 62 | if c_multi: 63 | pc.AddPaths(scaledClip, pyclipper.PT_CLIP, True) 64 | else: 65 | pc.AddPath(scaledClip, pyclipper.PT_CLIP, True) 66 | fill1=FILL_TYPE[s_fill_key] 67 | fill2=FILL_TYPE[c_fill_key] 68 | flattenPaths = [] 69 | solution = pc.Execute2(clipperType, fill1, fill2) 70 | paths = pyclipper.PolyTreeToPaths(solution) 71 | # print(paths) 72 | for p in paths: 73 | flattenPaths.append(pyclipper.scale_from_clipper(p)) 74 | return flattenPaths 75 | 76 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_utils/fillPath.py: -------------------------------------------------------------------------------- 1 | ''' 2 | package to fill things 3 | ''' 4 | import math,re,random 5 | import threadPlotter.TP_utils.shapeEditing as SHAPE 6 | 7 | 8 | def convertLinePathToDots(linePts,gap,innerDist=1,masterBoundary=None,addGap=False,locOnly=False): 9 | lines=[] 10 | for i, pt in enumerate(linePts): 11 | if i==0: 12 | continue 13 | pt0=linePts[i-1] 14 | if pt0!=None: 15 | #TODO : there shouldn't be a none 16 | lines += SHAPE.splitSingleLine([pt0, pt], gap) 17 | pointPaths=[] 18 | prevEnd=None 19 | appendGap=False 20 | for line in lines: 21 | start, middle, endTag, end = PO.parseToStartAndEnd(line) 22 | startPt = [round(float(xy), 2) for xy in start.split(",")] 23 | endPt = [xy + innerDist for xy in startPt] 24 | 25 | if masterBoundary==None or (masterBoundary!=None and PO.ptWithinRect(masterBoundary[0],masterBoundary[1],masterBoundary[2],masterBoundary[3],startPt)): 26 | if appendGap: 27 | newLine=[prevEnd,startPt] 28 | gapLine,gapPointPaths=convertLinePathToDots(newLine,gap,innerDist,masterBoundary=masterBoundary,addGap=addGap,locOnly=locOnly) 29 | pointPaths+=gapPointPaths 30 | path = SHAPE.getStraightPath([startPt, endPt]) 31 | if locOnly: 32 | path=startPt.copy() 33 | pointPaths.append(path) 34 | prevEnd = startPt 35 | appendGap=False 36 | elif addGap: 37 | appendGap=True 38 | 39 | return lines,pointPaths 40 | 41 | def makeTinyDot_pathPoints(startPt,innerDist=1): 42 | endPt = [xy + innerDist for xy in startPt] 43 | return [startPt, endPt] 44 | 45 | 46 | def convertPathsToConnectedDots(pathPoints,boundaryRect=None,dotDistance=5,locOnly=False): 47 | ''' 48 | create punchNeedle ready path. connect all paths together into one, and change it to dots 49 | :param pathPoints:(x,y,w,h, 50 | :return: 51 | ''' 52 | connectedPathPoint=[] 53 | for pathP in pathPoints: 54 | connectedPathPoint += pathP 55 | line,pointPaths=convertLinePathToDots(connectedPathPoint,dotDistance,masterBoundary=boundaryRect,addGap=True,locOnly=locOnly) 56 | return line,pointPaths 57 | 58 | def getPunchNeedleReadyDot(pathPoints,boundaryRect,dotDistance=4,addTrail=True,minDistance=2): 59 | ''' 60 | required boundaryRect 61 | Rule: 1) min/max distance is controlled 62 | 2) try to reduce duplication 63 | :param pathPoint: 64 | :param boundaryRect: (x,y,w,h, 65 | :param addTrail: add returning trail 66 | :return: 67 | ''' 68 | if addTrail: 69 | pathPoints.append([pathPoints[-1][-1],[0,0]]) 70 | line, pointPaths=convertPathsToConnectedDots(pathPoints,boundaryRect,dotDistance=dotDistance,locOnly=True) 71 | i=1 72 | while i xmax: 50 | p[0] = xmax 51 | if y > ymax: 52 | p[1] = ymax 53 | def getStraightPath(pointArr,closed=False): 54 | p="M"+" L".join([",".join([str(round(l,2)) for l in xy[:2]]) for xy in pointArr]) 55 | if closed: 56 | return p+"Z" 57 | return p 58 | def splitSingleLine(start_end,unitLength,toPoint=False): 59 | ''' 60 | return strings 61 | :param start_end: 62 | :param unitLength: 63 | :return: 64 | ''' 65 | if UB.pointEquals(start_end[0],start_end[1]): 66 | return [] 67 | pathStr=getStraightPath(start_end) 68 | path=parse_path(pathStr) 69 | try: 70 | l=path.length() 71 | if l>unitLength: 72 | paths=[] 73 | t=unitLength/l 74 | ts=0 75 | te=t 76 | for i in range(int(math.ceil(l/unitLength))): 77 | seg=Line(path.point(ts),path.point(te)) 78 | p=Path(seg) 79 | if toPoint: 80 | paths.append([UB.getPointFromComplex(path.point(ts)), UB.getPointFromComplex(path.point(te))]) 81 | else: 82 | paths.append(p.d()) 83 | 84 | ts+=t 85 | te+=t 86 | te=min(1,te) 87 | if toPoint: 88 | paths.append( 89 | [UB.getPointFromComplex(path.point(te)), UB.getPointFromComplex(path.point(1))]) 90 | else: 91 | paths.append(getStraightPath([UB.getPointFromComplex(path.point(te)),UB.getPointFromComplex(path.point(1))])) 92 | return paths 93 | if toPoint: 94 | return [start_end] 95 | else: 96 | return [pathStr] 97 | except Exception as e: 98 | print(start_end,"something wrong with the splitLine",e) 99 | return [] 100 | def calculateDistBetweenPoints(p1, p2): 101 | return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) 102 | def calculatePathLength(pathPoints): 103 | l=0 104 | if len(pathPoints)<2: 105 | return l 106 | for i in range(1,len(pathPoints)): 107 | l+=calculateDistBetweenPoints(pathPoints[i-1],pathPoints[i]) 108 | return l 109 | def makeRectPoints(x,y,w,h,closed=False): 110 | pts=[[x,y],[x+w,y],[x+w,y+h],[x,y+h]] 111 | if closed: 112 | pts.append([x,y]) 113 | return pts 114 | def makeUniformPolygon(x, y, r, sideCt,closed=False): 115 | points = [] 116 | for i in range(sideCt): 117 | points.append([x+r * math.cos(2 * math.pi * i / sideCt), y+r * math.sin(2 * math.pi * i / sideCt)]) 118 | if closed: 119 | points.append(points[0].copy()) 120 | return points 121 | 122 | def convertListToDotStr(dotList,dist=1): 123 | ''' 124 | Given a list of points, construct mini "dots" that are actually short lines (controlled by dist) 125 | :param dotList: 126 | :param dist: 127 | :return: a list of path strings. 128 | ''' 129 | strList=[] 130 | for pt in dotList: 131 | ps=getStraightPath([pt,[xy+dist for xy in pt]]) 132 | strList.append(ps) 133 | return strList -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/TP_utils/svg.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from threadPlotter.TP_utils.basic import unitConvert 3 | import re 4 | 5 | svgStarter='' 6 | def makeExportReadySvg(baseSoup,repList=None): 7 | defaultRep = ["path", "rect", "circle", "line", "polygon"] 8 | if repList: 9 | defaultRep += repList 10 | svgStr = baseSoup.prettify() 11 | for rep in defaultRep: 12 | if rep in svgStr: 13 | svgStr = "".join(svgStr.split("")) 14 | pathBackRef = re.compile("(<" + rep + ".*?)(>)") 15 | svgStr = pathBackRef.sub(r'\1/>', svgStr) 16 | return svgStr 17 | 18 | def saveSVG(baseSoup,fileName=None,saveLoc=None,postTag=None,repList=None,fullPath=None): 19 | svgStr=makeExportReadySvg(baseSoup,repList) 20 | if fullPath: 21 | sl=fullPath 22 | else: 23 | sl=saveLoc+fileName+postTag+".svg" 24 | with open(sl,'w') as saveFile: 25 | saveFile.write(svgStr) 26 | 27 | 28 | def makeSvg(): 29 | return BeautifulSoup(svgStarter, "html.parser") 30 | 31 | def applyAttrs(soup,attrs): 32 | for key in attrs: 33 | soup.attrs[key]=attrs[key] 34 | def makeSVGwithBasic(wh,margins,id="main_g",innerBox=False,outerBox=False): 35 | svg = makeSvg() 36 | attrs={ 37 | "width":str(wh[0])+"px","height":str(wh[1])+"px", 38 | "viewBox":"0 0 "+str(wh[0])+" "+str(wh[1]) 39 | } 40 | applyAttrs(svg.svg, attrs) 41 | defs = svg.new_tag("defs") 42 | svg.svg.append(defs) 43 | main_g = svg.new_tag("g") 44 | main_g.attrs = {"id": id, "transform": "translate(" + str(margins["l"]) + "," + str(margins["t"]) + ")"} 45 | svg.svg.append(main_g) 46 | if outerBox: 47 | addComponent(svg, svg.svg, "rect", { 48 | "x": 0, 49 | "y": 0, 50 | "width": wh[0], 51 | "height": wh[1], 52 | "fill": "none", 53 | "stroke": "black", 54 | "id":"outerBox" 55 | }) 56 | if innerBox: 57 | innerRect = svg.new_tag("rect") 58 | applyAttrs(innerRect, { 59 | "x": margins["l"], 60 | "y": margins['t'], 61 | "width": wh[0]-margins["l"]-margins["r"], 62 | "height": wh[1]-margins["t"]-margins["b"], 63 | "fill": "none", 64 | "stroke": "black", 65 | "id":"innerBox" 66 | }) 67 | svg.svg.append(innerRect) 68 | 69 | 70 | return svg 71 | def addComponent(soupBase,base,tagName,attrs): 72 | e = soupBase.new_tag(tagName) 73 | applyAttrs(e,attrs) 74 | base.append(e) 75 | return e 76 | 77 | def makeBasicSvgWithFoundations(basicSettings,unit,i2p): 78 | ''' 79 | SHORTER VERSION, WITHOUT TOOLPAD 80 | :param basicsettings: 81 | :return: 82 | ''' 83 | wkey = "width" 84 | hKey = "height" 85 | if "paperWidth" in basicSettings: 86 | wkey="paperWidth" 87 | hKey="paperHeight" 88 | if unit.upper()=="PX": 89 | width_height=[basicSettings[wkey],basicSettings[hKey]] 90 | #init svg 91 | else: 92 | width_height = [unitConvert(v,unit,i2p) for v in [basicSettings[wkey],basicSettings[hKey]]] 93 | 94 | #make margin box 95 | margins=basicSettings["margins"].copy() 96 | for k in margins: 97 | margins[k] = unitConvert(margins[k], unit, i2p) 98 | 99 | svg = makeSVGwithBasic(width_height, margins) 100 | 101 | boundaryRect = {} 102 | wh_m = [width_height[0] - margins["l"] - margins["r"], width_height[1] - margins['t'] - margins["b"]] 103 | # print("wh", width_height, wh_m, margins) 104 | if("displayOuterRect" in basicSettings and basicSettings["displayOuterRect"]): 105 | boundaryRect["outer"]=addComponent(svg,svg.svg,"rect",{ 106 | "x":0, 107 | "y":0, 108 | "width":width_height[0], 109 | "height":width_height[1], 110 | "fill":"none", 111 | "stroke":"black" 112 | }) 113 | 114 | if("displayInnerRect" in basicSettings and basicSettings["displayInnerRect"]): 115 | innerRect=svg.new_tag("rect") 116 | applyAttrs(innerRect,{ 117 | "x":margins["l"], 118 | "y":margins['t'], 119 | "width":wh_m[0], 120 | "height":wh_m[1], 121 | "fill":"none", 122 | "stroke":"black" 123 | }) 124 | svg.svg.append(innerRect) 125 | boundaryRect["inner"] = innerRect 126 | 127 | return svg, width_height,wh_m,boundaryRect,margins 128 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/ThreadPlotter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | threadPlotter is a class that contains all the basic functions for generating a plotter-based punch needle embroidery piece. 3 | One project is associated with one design, and can output to svgs and python files (for Axidraw) 4 | ''' 5 | 6 | from threadPlotter.DirectAuthoringGenerator import DirectAuthoringGenerator as DirectAuthoringGenerator 7 | from threadPlotter.TP_utils.shapeEditing import convertListToDotStr 8 | 9 | from threadPlotter.TP_punchneedle import threadColorManagement as TCM 10 | from threadPlotter.TP_utils import basic as UB 11 | from threadPlotter.TP_utils import svg as SVG 12 | from threadPlotter.TP_structure.PunchGroup import PunchGroup 13 | import json 14 | 15 | class ThreadPlotter(DirectAuthoringGenerator): 16 | 17 | def initSpeedAndDepthMap(self): 18 | ''' 19 | generate linear mapping functions for the depth and speed 20 | :return: 21 | ''' 22 | plotterSettingRange=self.currentSpec["plotterSettingRange"] 23 | self.depthRange=plotterSettingRange["depthPercRange"] 24 | self.speedRange=plotterSettingRange["speedPercRange"] 25 | distanceRange=plotterSettingRange["distanceRange"] 26 | self.distanceRange=[UB.unitConvert(d,self.unit,self.i2p) for d in distanceRange] 27 | 28 | def depthSpeedCalculator_batch(self,distance): 29 | storage={} 30 | for dist in distance: 31 | storage[dist]=self.depthSpeedCalculator(dist) 32 | return storage 33 | def depthSpeedCalculator(self,dist): 34 | ''' 35 | given distance, calculate speed and depth 36 | 37 | :param distance: a list of distances (in px) 38 | :return: 39 | ''' 40 | speed = UB.linearScale(dist, self.distanceRange, self.speedRange) 41 | depth = UB.linearScale(dist, self.distanceRange, self.depthRange) 42 | return {"pen_pos_down": int(depth), "pen_rate_raise": int(speed)} 43 | 44 | 45 | 46 | def closeFiles(self): 47 | ''' 48 | Assuming all information has been stored in the list: 49 | self.punchGroupCollection 50 | for each punch group, process the information contained (segment into center points) and export to svg and python. 51 | :return: 52 | ''' 53 | lastPgId=len(self.punchGroupCollection)-1 54 | boundaryBox=[0,0,self.wh_m[0],self.wh_m[1]] 55 | 56 | for pgi,punchGroup in enumerate(self.punchGroupCollection): 57 | toolId=punchGroup.toolId 58 | dotList=punchGroup.exportToPunchNeedleReadyPoints(self.segmentLength,boundaryBox) 59 | if len(dotList)<1: 60 | continue 61 | trailList=[] 62 | if pgi!=lastPgId: 63 | trailList=punchGroup.exportTrailToAnotherPunchGroup( 64 | self.punchGroupCollection[pgi+1], 65 | self.trailStitchLength 66 | ) 67 | self.updateOptions(self.stitchSetting,toolId) 68 | for pt in dotList: 69 | self.addDrawPt(pt,toolId) 70 | self.updateOptions(self.trailStitchSetting,toolId) 71 | for trailPt in trailList: 72 | self.addDrawPt(trailPt,toolId) 73 | 74 | pathStrs = convertListToDotStr(dotList+trailList) 75 | for ps in pathStrs: 76 | self.addPath(self.svg.g, ps, self.svg, self.tools[toolId]) 77 | if hasattr(self, "toolSvgs"): 78 | self.addPath(self.toolSvgs[toolId].g, ps, self.toolSvgs[toolId], 79 | self.tools[toolId]) 80 | 81 | 82 | def saveFiles(self): 83 | print("exporting to " + self.getFullSaveLoc()) 84 | self.closeFiles() 85 | DirectAuthoringGenerator.saveFiles(self) 86 | #export thread matching guide 87 | self.generateColorPlan() 88 | 89 | def initPunchGroup(self,toolId,startingPathPoints=None,skipSegment=False): 90 | ''' 91 | make a thread group, append to storage. 92 | :param startingPathPoints: 93 | :return: index of this threadGroup 94 | ''' 95 | pgI=len(self.punchGroupCollection) 96 | self.punchGroupCollection.append( 97 | PunchGroup(startingPathPoints,pgI,toolId,skipSegment) 98 | ) 99 | return pgI 100 | def generateColorPlan(self): 101 | ''' 102 | export the color plan into svg and json files 103 | :return: 104 | ''' 105 | 106 | svg, width_height, wh_m, boundaryRect, margins= SVG.makeBasicSvgWithFoundations({"paperWidth":5, "paperHeight": len(self.colorList) * 2, "inchToPx":96, "margins":{"l":0.1, "r":0.1, "t":0.1, "b":0.1}}, "inch", 96) 107 | boxW=wh_m[0] 108 | boxH=wh_m[1]/len(self.colorList) 109 | fontSize=15 110 | for i, colorObj in enumerate(self.colorList): 111 | originalRgb="RGB("+",".join([str(s) for s in self.rgbList[i]])+")" 112 | y=boxH*i 113 | SVG.addComponent(svg, svg.g, "rect", 114 | {"x": 0, "y": y, "width": boxW, "height": boxH, 115 | "fill": "none",'stroke':"black"}) 116 | 117 | #text original color 118 | idxText= SVG.addComponent(svg, svg.g, "text", {"x":fontSize, "y": fontSize + y, "style": 'font-size:' + str(fontSize) + ';'}) 119 | idxText.string="Tool:"+str(i)+" "+str(originalRgb) 120 | SVG.addComponent(svg, svg.g, "rect", 121 | {"x": 0, "y": fontSize*2 + y, "width":boxW,"height":boxH/4,"fill":originalRgb}) 122 | 123 | #generate matched color 124 | if "mixedExpect" not in colorObj: 125 | #make one grid 126 | gridWidth=boxW 127 | else: 128 | gridWidth=boxW/4 129 | jy=fontSize*3 + y+boxH/4 130 | textY=fontSize*3 + y+boxH/2 131 | 132 | for j,cObj in enumerate(colorObj["c"]): 133 | rgbStrToAppend=TCM.rgbToString(cObj["rgb"]) 134 | jx=j*gridWidth 135 | SVG.addComponent(svg, svg.g, "rect", 136 | {"x": jx, "y": jy, "width": gridWidth, "height": boxH / 4, 137 | "fill": rgbStrToAppend}) 138 | idxText = SVG.addComponent(svg, svg.g, "text", {"x": 0, "y": textY + fontSize + j * fontSize, "style": 'font-size:' + str(fontSize * 0.8) + ';'}) 139 | idxText.string = "|".join(["id:"+str(cObj["i"]),"code:"+str(cObj['code']),rgbStrToAppend]) 140 | if "mixedExpect" in colorObj: 141 | SVG.addComponent(svg, svg.g, "rect", 142 | {"x": gridWidth*3, "y": jy, "width": gridWidth, "height": boxH / 4, 143 | "fill": TCM.rgbToString(colorObj["mixedExpect"]),"stroke":"black"}) 144 | #save 145 | SVG.saveSVG(svg, fullPath=self.getFullSaveLoc("tool") + ".svg") 146 | with open(self.getFullSaveLoc("threadColor")+ ".json", 'w') as outfile: 147 | json.dump({"colorList":self.colorList,"tools":self.tools}, outfile) 148 | def generate(self): 149 | ''' 150 | sample generation 151 | :return: 152 | ''' 153 | return 154 | 155 | def pickRandomThreadColors(self,ct=None): 156 | ''' 157 | pick random thread colors (contains potential color mixing) 158 | :param ct: 159 | :return: 160 | ''' 161 | if not ct: 162 | ct=self.basicSettings["toolsCt"] 163 | self.rgbList,self.colorList=TCM.pickRandomThreadColor(ct) 164 | for i in range(ct): 165 | self.tools[i]["stroke"]="RGB("+",".join([str(s) for s in self.rgbList[i]])+")" 166 | # print(self.tools) 167 | print("picked random color:",self.tools) 168 | def matchColor(self,colorList,allowMix=True): 169 | ''' 170 | given a color list (list of rgb tuples), match the best thread color and assign to this threadPlotter object 171 | :param colorList: 172 | :return: 173 | ''' 174 | self.rgbList=colorList 175 | self.plainColor,self.mixedColor,self.colorList=TCM.pickThreadColor(colorList,allowMix=allowMix) 176 | def __init__(self,settings,batchName="",svg=True,toolSvg=True): 177 | DirectAuthoringGenerator.__init__(self,settings,batchName=batchName,svg=svg,toolSvg=toolSvg) 178 | #extract thread plotter specific information from the settings 179 | self.initSpeedAndDepthMap() 180 | self.segmentLength=UB.unitConvert(self.currentSpec["segmentLength"],self.unit,self.i2p) 181 | self.trailStitchLength=UB.unitConvert(self.currentSpec["trailStitchLength"],self.unit,self.i2p) 182 | self.pickRandomThreadColors() 183 | self.stitchSetting=self.depthSpeedCalculator(self.segmentLength) 184 | self.trailStitchSetting=self.depthSpeedCalculator(self.trailStitchLength) 185 | self.punchGroupCollection=[] 186 | -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/build/lib/threadPlotter/__init__.py -------------------------------------------------------------------------------- /threadPlotter/build/lib/threadPlotter/updateColor.py: -------------------------------------------------------------------------------- 1 | from threadPlotter.TP_utils import basic as UB 2 | from itertools import combinations 3 | import math,os,pkg_resources 4 | 5 | PKL_PATH=pkg_resources.resource_filename('threadPlotter', 'TP_punchneedle/threadColor.pkl') 6 | CSV_PATH=pkg_resources.resource_filename('threadPlotter', 'TP_punchneedle/embroidery_thread_color.csv') 7 | 8 | def exportThreadColorListPKL(): 9 | ''' 10 | export list of thread accordingn to settings according to here: 11 | https://pypi.org/project/pyembroidery/ 12 | :return: 13 | ''' 14 | seedFile = CSV_PATH 15 | 16 | originalColor=[] 17 | i=0 18 | with open(seedFile,"r") as seed: 19 | for row in seed: 20 | if "name" in row or "," not in row: 21 | continue 22 | content=row.strip().split(",") 23 | c={ 24 | "id":i, 25 | "color":(int(content[3]),int(content[4]),int(content[5])), 26 | "code":content[1], 27 | "name":content[2], 28 | "brand":"NEWBROTHREAD" 29 | } 30 | i+=1 31 | originalColor.append(c) 32 | saveDir= os.path.join(this_dir, "TP_punchneedle", "original_thread_list.pkl") 33 | UB.save_object(originalColor, saveDir) 34 | 35 | def getAverageColor(c1,c2,c3): 36 | ''' 37 | return avg rgb 38 | :param c1: 0-255 triplets 39 | :param c2: 40 | :param c3: 41 | :return: 42 | ''' 43 | 44 | comb=[c1,c2,c3] 45 | r=sum([c[0]**2 for c in comb]) 46 | g=sum([c[1]**2 for c in comb]) 47 | b=sum([c[2]**2 for c in comb]) 48 | 49 | return (int(math.sqrt(r / 3.0)), int(math.sqrt(g / 3.0)), int(math.sqrt(b / 3.0))) 50 | def makeColorCombinations(): 51 | ''' 52 | take the csv for embroidery thread color and build a python pkl. store locally 53 | :return: 54 | ''' 55 | seedFile = CSV_PATH 56 | 57 | originalColor=[] 58 | i=0 59 | with open(seedFile,"r") as seed: 60 | for row in seed: 61 | if "name" in row or "," not in row: 62 | continue 63 | content=row.strip().split(",") 64 | c={ 65 | "i":i, 66 | "rgb":(int(content[3]),int(content[4]),int(content[5])), 67 | "code":content[1], 68 | "name":content[2] 69 | } 70 | i+=1 71 | originalColor.append(c) 72 | 73 | 74 | combs=combinations(list(range(i))*3,3) 75 | combinedColor=[] 76 | for c1i,c2i,c3i in combs: 77 | c1=originalColor[c1i]["rgb"] 78 | c2=originalColor[c2i]["rgb"] 79 | c3=originalColor[c3i]["rgb"] 80 | avgColor=getAverageColor(c1,c2,c3) 81 | combinedColor.append(((c1i,c2i,c3i),avgColor)) 82 | print(len(combinedColor)) 83 | 84 | UB.save_object({"original":originalColor,"mixed":combinedColor},PKL_PATH) 85 | 86 | if __name__=="__main__": 87 | makeColorCombinations() 88 | -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b0.tar.gz -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b1-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b1-py2.py3-none-any.whl -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b1.tar.gz -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b2-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b2-py2.py3-none-any.whl -------------------------------------------------------------------------------- /threadPlotter/dist/threadPlotter-0.0.2b2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/dist/threadPlotter-0.0.2b2.tar.gz -------------------------------------------------------------------------------- /threadPlotter/manifest.in: -------------------------------------------------------------------------------- 1 | include threadPlotter/TP_punchneedle/threadColor.pkl 2 | include threadPlotter/TP_punchneedle/embroidery_thread_color.csv -------------------------------------------------------------------------------- /threadPlotter/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE.txt -------------------------------------------------------------------------------- /threadPlotter/setup.py: -------------------------------------------------------------------------------- 1 | 2 | import setuptools 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='threadPlotter', 7 | version='0.0.2b2', 8 | description='a toolkit for designing plotter-compatible embroidery patterns', 9 | long_description="Supplementary material for the paper:Plotting with Thread:Fabricating Delicate Punch Needle Embroidery with X-Y Plotters.", 10 | url="http://eyesofpanda.com/projects/thread_plotter/", 11 | author='Shiqing Licia He', 12 | author_email='heslicia@umich.edu', 13 | license="MIT", 14 | classifiers=[ 15 | # How mature is this project? Common values are 16 | # 3 - Alpha 17 | # 4 - Beta 18 | # 5 - Production/Stable 19 | 'Development Status :: 4 - Beta', 20 | 21 | # Indicate who your project is intended for 22 | 'Intended Audience :: Manufacturing', 23 | 'Topic :: Adaptive Technologies', 24 | 25 | # Pick your license as you wish (should match "license" above) 26 | 'License :: OSI Approved :: MIT License', 27 | 28 | # Specify the Python versions you support here. In particular, ensure 29 | # that you indicate whether you support Python 2, Python 3 or both. 30 | 'Programming Language :: Python :: 3.6', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Programming Language :: Python :: 3.8', 33 | ], 34 | keywords='punch needle embroidery; plotter embroidery', 35 | project_urls={ 36 | 'Documentation': 'https://github.com/LiciaHe/threadPlotter', 37 | 'Project Site': 'http://eyesofpanda.com/projects/thread_plotter/', 38 | 'Source': 'https://github.com/LiciaHe/threadPlotter', 39 | }, 40 | packages=setuptools.find_packages(), 41 | python_requires='~=3.6', 42 | install_requires=[ 43 | 'Pillow >=7.1.2', 44 | 'pyclipper>=1.1.0.post3', 45 | 'svgpathtools>=1.3.3', 46 | 'bs4 >=0.0.1', 47 | 'scipy', 48 | 'numpy' 49 | ], 50 | include_package_data=True, 51 | # data_files=[('threadCsv', ['threadPlotter/TP_punchneedle/embroidery_thread_color.csv']),('threadColorPkl',['threadPlotter/TP_punchneedle/threadColor.pkl'])] 52 | 53 | ) 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.2 2 | Name: threadPlotter 3 | Version: 0.0.2b2 4 | Summary: a toolkit for designing plotter-compatible embroidery patterns 5 | Home-page: http://eyesofpanda.com/projects/thread_plotter/ 6 | Author: Shiqing Licia He 7 | Author-email: heslicia@umich.edu 8 | License: MIT 9 | Project-URL: Documentation, https://github.com/LiciaHe/threadPlotter 10 | Project-URL: Project Site, http://eyesofpanda.com/projects/thread_plotter/ 11 | Project-URL: Source, https://github.com/LiciaHe/threadPlotter 12 | Description: Supplementary material for the paper:Plotting with Thread:Fabricating Delicate Punch Needle Embroidery with X-Y Plotters. 13 | Keywords: punch needle embroidery; plotter embroidery 14 | Platform: UNKNOWN 15 | Classifier: Development Status :: 4 - Beta 16 | Classifier: Intended Audience :: Manufacturing 17 | Classifier: Topic :: Adaptive Technologies 18 | Classifier: License :: OSI Approved :: MIT License 19 | Classifier: Programming Language :: Python :: 3.6 20 | Classifier: Programming Language :: Python :: 3.7 21 | Classifier: Programming Language :: Python :: 3.8 22 | Requires-Python: ~=3.6 23 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | MANIFEST.in 3 | README.md 4 | setup.cfg 5 | setup.py 6 | threadPlotter/DirectAuthoringGenerator.py 7 | threadPlotter/ThreadPlotter.py 8 | threadPlotter/__init__.py 9 | threadPlotter/updateColor.py 10 | threadPlotter.egg-info/PKG-INFO 11 | threadPlotter.egg-info/SOURCES.txt 12 | threadPlotter.egg-info/dependency_links.txt 13 | threadPlotter.egg-info/requires.txt 14 | threadPlotter.egg-info/top_level.txt 15 | threadPlotter/TP_punchneedle/GridImgConverter.py 16 | threadPlotter/TP_punchneedle/__init__.py 17 | threadPlotter/TP_punchneedle/embroideryCalculation.py 18 | threadPlotter/TP_punchneedle/embroidery_thread_color.csv 19 | threadPlotter/TP_punchneedle/threadColor.pkl 20 | threadPlotter/TP_punchneedle/threadColorManagement.py 21 | threadPlotter/TP_structure/PathList.py 22 | threadPlotter/TP_structure/Point.py 23 | threadPlotter/TP_structure/PunchGroup.py 24 | threadPlotter/TP_structure/__init__.py 25 | threadPlotter/TP_utils/__init__.py 26 | threadPlotter/TP_utils/basic.py 27 | threadPlotter/TP_utils/clipperHelper.py 28 | threadPlotter/TP_utils/fillPath.py 29 | threadPlotter/TP_utils/shapeEditing.py 30 | threadPlotter/TP_utils/svg.py -------------------------------------------------------------------------------- /threadPlotter/threadPlotter.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | Pillow>=7.1.2 2 | pyclipper>=1.1.0.post3 3 | svgpathtools>=1.3.3 4 | bs4>=0.0.1 5 | scipy 6 | numpy 7 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | threadPlotter 2 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/DirectAuthoringGenerator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Stores basic settings for a svg-python generator 3 | Includes utility functions for storage and setup 4 | merged version of the DirectAuthoring and the MainGenerator 5 | Dealing with lists and does not check for valid path points 6 | ''' 7 | 8 | from threadPlotter.TP_utils import shapeEditing as SHAPE 9 | from threadPlotter.TP_utils import basic as UB 10 | from threadPlotter.TP_utils import svg as SVG 11 | 12 | import datetime,random 13 | 14 | class DirectAuthoringGenerator: 15 | #storage-related 16 | def initStorage(self, makeDate=True): 17 | self.saveLoc = self.baseSaveLoc + self.name + "/" 18 | UB.mkdir(self.saveLoc) 19 | # everything is stored here 20 | if makeDate: 21 | self.dateFolder = self.saveLoc + str(datetime.datetime.now().strftime("%Y-%m-%d")) + "/" 22 | UB.mkdir(self.dateFolder) 23 | def initBasedOnSettings(self, settings, nameKey="name", specKey="spec", basicSettingKey="basic"): 24 | self.settings = settings 25 | if nameKey: 26 | self.name = settings[nameKey] 27 | else: 28 | self.name = settings.name 29 | self.currentSpec = settings[specKey] if specKey else settings.currentSetting 30 | 31 | self.basicSettings = settings[basicSettingKey] if basicSettingKey else settings.basicSettings 32 | 33 | self.baseSaveLoc=self.settings["baseSaveLoc"] if "baseSaveLoc" in self.settings else "" 34 | self.initStorage() 35 | 36 | 37 | 38 | self.timeTag = str(datetime.datetime.now().strftime("%H%M%S")) + "_" + str(random.getrandbits(3)) 39 | self.timedLoc = self.dateFolder + self.timeTag + self.batchName + "/" 40 | UB.mkdir(self.timedLoc) 41 | self.unit=self.basicSettings["unit"] if "unit" in self.basicSettings else "inch" 42 | if "inchToPx" in self.basicSettings: 43 | self.i2p = self.basicSettings["inchToPx"] 44 | else: 45 | self.i2p = 96 46 | 47 | #tools and basic settings 48 | def generateRandomTools(self, toolCount=-1): 49 | ''' 50 | random colored tools 51 | :param toolCount: 52 | :return: 53 | ''' 54 | if toolCount < 0: 55 | toolCount = self.basicSettings["toolsCt"] 56 | self.tools = [] 57 | for i in range(toolCount): 58 | self.tools.append({ 59 | "idx": i, 60 | "stroke-width": 1, 61 | "stroke": 1, 62 | "fill": "none" 63 | }) 64 | ballPenColors = ["#000", "#ff0000", "#8000e8", "#1925ff", "#ffaa00", "#039c2e"] 65 | 66 | if toolCount <= len(ballPenColors): 67 | bpcSample = random.sample(ballPenColors, toolCount) 68 | else: 69 | bpcSample = [random.choice(ballPenColors) for i in range(toolCount)] 70 | for i in range(toolCount): 71 | self.tools[i]["stroke"] = bpcSample[i] 72 | def getRandomToolId(self): 73 | if hasattr(self, "tools"): 74 | return random.randint(0, len(self.tools) - 1) 75 | return -1 76 | 77 | 78 | #axidraw-python authoring 79 | def calculateAxidrawPosition(self,pt,withMargin=True): 80 | dotPathInInches = SHAPE.ptToInchStrWithTranslate(pt, self.i2p, self.marginInch["l"], self.marginInch["t"]) 81 | if not withMargin: 82 | dotPathInInches=SHAPE.ptToInchStr(pt,self.i2p,precision=2) 83 | return dotPathInInches 84 | 85 | def writeEmptyUpdate(self,writerIdx): 86 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 87 | self.axidrawWriters[writerIdx].write("ad.move(0,0)\n") 88 | def updateOptions(self,req,writerIdx): 89 | ''' 90 | update setting manually 91 | assume axidraws are initiated 92 | :param req: 93 | :return: 94 | ''' 95 | 96 | for key in req: 97 | self.axidrawWriters[writerIdx].write("ad.options." + key + "=" + str(req[key]) + "\n") 98 | self.axidrawWriters[writerIdx].write( "ad.update()\n") 99 | def addPenUp(self,writerIdx): 100 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 101 | def addPenDown(self,writerIdx): 102 | self.axidrawWriters[writerIdx].write("ad.pendown()\n") 103 | 104 | def addDrawPt(self,dotCenter,writerIdx,withMargin=True): 105 | ''' 106 | use only up and down 107 | :param dotCenter: 108 | :param toolIdx: 109 | :return: 110 | ''' 111 | self.addMoveTo(dotCenter, writerIdx, withMargin=withMargin) 112 | self.axidrawWriters[writerIdx].write("ad.pendown()\n") 113 | self.axidrawWriters[writerIdx].write("ad.penup()\n") 114 | def addMoveTo(self,dotCenter, writerIdx, withMargin=True): 115 | dotPathInInches=self.calculateAxidrawPosition(dotCenter,withMargin=withMargin) 116 | self.axidrawWriters[writerIdx].write("ad.moveto(" + dotPathInInches[0] + "," + dotPathInInches[1] + ")\n") 117 | def addLineTo(self,pt,writerIdx,withMargin=True): 118 | dotPathInInches = self.calculateAxidrawPosition(pt, withMargin=withMargin) 119 | self.axidrawWriters[writerIdx].write("ad.lineto(" + dotPathInInches[0] + "," + dotPathInInches[1] + ")\n") 120 | def addPath(self,toAppend,d,baseSoup,additionalAttr): 121 | attr={"d": d} 122 | attr.update(additionalAttr) 123 | return SVG.addComponent(baseSoup, toAppend, "path", attr) 124 | def getSaveName(self, additionalTag=""): 125 | return self.timeTag + "_" + additionalTag 126 | def addDrawing(self,pathPoints,writerIdx): 127 | for i, pt in enumerate(pathPoints): 128 | if i == 0: 129 | self.addMoveTo(pt, writerIdx) 130 | else: 131 | self.addLineTo(pt,writerIdx) 132 | def addComment(self, comment, toolIdx): 133 | self.axidrawWriters[toolIdx].write("##" + comment + "\n") 134 | def initNewAxidrawWriter(self, additionalTag=""): 135 | 136 | 137 | currentName = self.getFullSaveLoc(additionalTag=additionalTag + "_" + str(len(self.axidrawWriters))) 138 | 139 | axidrawWriter = open(currentName + ".py", "w") 140 | self.axidrawWriters.append(axidrawWriter) 141 | axidrawWriter.write(self.axidrawHeader) 142 | self.updateOptions(self.normalSetting, len(self.axidrawWriters) - 1) 143 | self.writeEmptyUpdate(len(self.axidrawWriters) - 1) 144 | return len(self.axidrawWriters) - 1 145 | 146 | 147 | ##----save--- 148 | def appendToAxidrawCollection(self, pathPoints, toolIdx=-1): 149 | ''' 150 | adding a path into the path collection 151 | :param pathPoints: 152 | :param toolIdx: 153 | :return: 154 | ''' 155 | if not hasattr(self, "axidrawPathCollection"): 156 | self.axidrawPathCollection = [] 157 | for i in range(len(self.tools)): 158 | self.axidrawPathCollection.append([]) 159 | if toolIdx < 0: 160 | toolIdx = self.getRandomToolId() 161 | 162 | self.axidrawPathCollection[toolIdx].append(pathPoints) 163 | def closeFiles(self): 164 | if hasattr(self, "axidrawPathCollection"): 165 | for i, coll in enumerate(self.axidrawPathCollection): 166 | for pathPoints in coll: 167 | self.addDrawing(pathPoints, i) 168 | self.addDrawing(pathPoints, -1) 169 | self.addPath(self.svg.g, SHAPE.getStraightPath(pathPoints), self.svg, self.tools[i]) 170 | if hasattr(self, "toolSvgs"): 171 | self.addPath(self.toolSvgs[i].g, SHAPE.getStraightPath(pathPoints), self.toolSvgs[i], 172 | self.tools[i]) 173 | self.addMoveTo([0, 0], i, withMargin=False) 174 | 175 | for axidrawWriter in self.axidrawWriters: 176 | axidrawWriter.write("\nad.disconnect()\nprint('end')\n####") 177 | axidrawWriter.close() 178 | 179 | def saveFiles(self): 180 | self.closeFiles() 181 | if hasattr(self, "svg"): 182 | self.saveSvg(self.svg) 183 | if hasattr(self, "toolSvgs"): 184 | for i, s in enumerate(self.toolSvgs): 185 | self.saveSvg(s, str(i)) 186 | def getFullSaveLoc(self, additionalTag=""): 187 | return self.timedLoc + self.timeTag + "_" + additionalTag 188 | def saveSvg(self, soup, additionalTag=""): 189 | name = self.getFullSaveLoc(additionalTag) 190 | SVG.saveSVG(soup, fullPath=name + ".svg") 191 | 192 | def __init__(self,settings,batchName="",svg=True,toolSvg=True): 193 | self.batchName=batchName 194 | self.initBasedOnSettings(settings) 195 | self.axidrawWriters = [] 196 | defaultSettings = { 197 | "normalSetting": { 198 | "pen_pos_up": 60, 199 | "pen_pos_down": 20, 200 | "pen_delay_down": 20, 201 | }, 202 | "toolsSetting": { 203 | "pen_pos_up": 100, 204 | "pen_pos_down": 0, 205 | "pen_delay_down": 20 206 | } 207 | } 208 | for key in ["normalSetting", "toolsSetting"]: 209 | if key not in self.basicSettings: 210 | self.basicSettings[key] = defaultSettings[key] 211 | else: 212 | defaultSettings[key].update(self.basicSettings[key]) 213 | self.basicSettings[key] = defaultSettings[key] 214 | self.normalSetting = self.basicSettings["normalSetting"] 215 | self.toolsSetting = self.basicSettings["toolsSetting"] 216 | self.marginInch = self.basicSettings["margins"].copy() 217 | self.i2p = 96 if "i2p" not in self.basicSettings else self.basicSettings["i2p"] 218 | if self.i2p!=96: 219 | print("The inch to pixel rate is currently set to "+str(self.i2p)) 220 | self.toolsCt = self.basicSettings["toolsCt"] 221 | 222 | axidrawHeader = [ 223 | "'''\n auto-generated axidraw code using ThreadPlotter\n'''\nfrom pyaxidraw import axidraw\nimport " 224 | "time\nad " 225 | "=axidraw.AxiDraw()\nad.interactive()\nad.connect()\n" 226 | ] 227 | for k in self.basicSettings["plotterDefaultSetting"]: 228 | axidrawHeader.append("ad.options."+k+"="+str(self.basicSettings["plotterDefaultSetting"][k])+"\n") 229 | axidrawHeader.append("ad.update()\n") 230 | self.axidrawHeader="\n".join(axidrawHeader)+"\n" 231 | 232 | 233 | 234 | if svg: 235 | self.svg, self.wh, self.wh_m, self.boundaryRect, self.margins = SVG.makeBasicSvgWithFoundations( 236 | self.basicSettings,unit=self.unit,i2p=self.i2p) 237 | self.generateRandomTools() 238 | if toolSvg: 239 | self.toolSvgs=[] 240 | for i in range(len(self.tools)): 241 | svg, wh, whm, br, m = SVG.makeBasicSvgWithFoundations( 242 | self.basicSettings,unit=self.unit,i2p=self.i2p) 243 | self.toolSvgs.append(svg) 244 | for i in range(len(self.tools)): 245 | self.initNewAxidrawWriter() 246 | 247 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_punchneedle/GridImgConverter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Converting an image into a fixed grid layout 3 | ''' 4 | import PIL 5 | try: 6 | import Image 7 | except ImportError: 8 | from PIL import Image 9 | 10 | from scipy.spatial import Delaunay 11 | import numpy as np 12 | from threadPlotter.TP_punchneedle import threadColorManagement as TCM 13 | 14 | class GridImgConverter: 15 | def getIntensities(self,pixelization_length, pixels, i, j): 16 | total_red_intensity = total_green_intensity = total_blue_intensity = 0 17 | averaging_pixel_number = pixelization_length * pixelization_length 18 | # 19 | for k in range(0, pixelization_length): 20 | for l in range(0, pixelization_length): 21 | # print(pixels[i * pixelization_length + k, j * pixelization_length + l]) 22 | total_red_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][0] 23 | total_green_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][1] 24 | total_blue_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][2] 25 | # 26 | average_red_intensity = int(total_red_intensity / averaging_pixel_number) 27 | average_green_intensity = int(total_green_intensity / averaging_pixel_number) 28 | average_blue_intensity = int(total_blue_intensity / averaging_pixel_number) 29 | return average_red_intensity, average_green_intensity, average_blue_intensity 30 | def getIntensities_noAvg(self,pixelization_length, pixels, i, j): 31 | total_red_intensity = total_green_intensity = total_blue_intensity = 0 32 | averaging_pixel_number = pixelization_length * pixelization_length 33 | # 34 | for k in range(0, int(pixelization_length)): 35 | for l in range(0, int(pixelization_length)): 36 | # print(pixels[i * pixelization_length + k, j * pixelization_length + l]) 37 | total_red_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][0] 38 | total_green_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][1] 39 | total_blue_intensity += pixels[i * pixelization_length + k, j * pixelization_length + l][2] 40 | # 41 | average_red_intensity = int(total_red_intensity / averaging_pixel_number) 42 | average_green_intensity = int(total_green_intensity / averaging_pixel_number) 43 | average_blue_intensity = int(total_blue_intensity / averaging_pixel_number) 44 | return pixels[i * pixelization_length , j * pixelization_length ][0],pixels[i * pixelization_length , j * pixelization_length ][1],pixels[i * pixelization_length , j * pixelization_length ][2] 45 | 46 | def halftone(self,x_units,y_units,pixelization_length,pixels): 47 | dotLocations = [] 48 | dotColors = [] 49 | 50 | for i in range(0, x_units): 51 | col=[] 52 | self.gridImg.append(col) 53 | for j in range(0, y_units): 54 | average_red_intensity, average_green_intensity, average_blue_intensity = self.getIntensities( 55 | pixelization_length, pixels, i, j) 56 | x0 = i * pixelization_length 57 | y0 = j * pixelization_length 58 | x1 = i * pixelization_length + pixelization_length - 1 59 | y1 = j * pixelization_length + pixelization_length - 1 60 | cx = (x0 + x1) / 2.0 61 | cy = (y0 + y1) / 2.0 62 | dotLocations.append((cx, cy)) 63 | dotColors.append((average_red_intensity, average_green_intensity, average_blue_intensity)) 64 | col.append(((cx,cy),(average_red_intensity, average_green_intensity, average_blue_intensity))) 65 | return dotLocations,dotColors 66 | 67 | def halftone_noAvg(self, x_units, y_units, pixelization_length, pixels): 68 | dotLocations = [] 69 | dotColors = [] 70 | 71 | for i in range(0, x_units): 72 | col = [] 73 | self.gridImg.append(col) 74 | for j in range(0, y_units): 75 | r,g,b= self.getIntensities_noAvg( 76 | pixelization_length, pixels, i, j) 77 | x0 = i * pixelization_length 78 | y0 = j * pixelization_length 79 | x1 = i * pixelization_length + pixelization_length - 1 80 | y1 = j * pixelization_length + pixelization_length - 1 81 | cx = (x0 + x1) / 2.0 82 | cy = (y0 + y1) / 2.0 83 | dotLocations.append((cx, cy)) 84 | 85 | dotColors.append((r,g,b)) 86 | col.append(((cx,cy),(r,g,b))) 87 | 88 | return dotLocations, dotColors 89 | def __init__(self,imgLoc,imgName,colorCount=5,imgSize=(960,960), pixelization_length =4,grayScale=False,removeSmallCollections=True,quantize=True,idealColors=-1): 90 | ''' 91 | convert 92 | :param saveLoc: 93 | :param imgName: 94 | :param imgSize: 95 | :param gridSize: 96 | ''' 97 | img = Image.open(imgLoc + imgName) 98 | img=img.resize([int(xy) for xy in imgSize]) 99 | img = img.transpose(Image.FLIP_LEFT_RIGHT) 100 | if quantize: 101 | img= img.quantize(colorCount) 102 | self.quantize=quantize 103 | if grayScale: 104 | img=img.convert('LA') 105 | img=img.convert('RGB') 106 | self.img=img 107 | # img = img.convert('P', palette=Image.ADAPTIVE, colors=colorCount) 108 | self.gridImg = [] 109 | pixels = img.load() 110 | x_units = int(img.size[0] / pixelization_length) 111 | y_units = int(img.size[1] / pixelization_length) 112 | self.pixelization_length=pixelization_length 113 | dotLocations, dotColors=self.halftone_noAvg(x_units,y_units,pixelization_length,pixels) 114 | self.x_units=x_units 115 | self.y_units=y_units 116 | self.pathCollection={} 117 | 118 | if not self.quantize: 119 | #replacing close colors 120 | lim=80 121 | dotColors,colors=self.replaceColors(lim,dotColors) 122 | while idealColors>0 and len(colors)>idealColors: 123 | lim+=20 124 | dotColors, colors = self.replaceColors(lim, dotColors) 125 | 126 | 127 | for i,c in enumerate(dotColors): 128 | rgb="rgb("+",".join([str(int(xy)) for xy in c])+")" 129 | 130 | dots=[pt for j,pt in enumerate(dotLocations) if dotColors[j]==c] 131 | # dots.sort(key=lambda x:(x[1],x[0])) 132 | self.pathCollection[rgb]=dots 133 | if removeSmallCollections: 134 | keys=list(self.pathCollection.keys()) 135 | for c in keys: 136 | if len(self.pathCollection[c])<30: 137 | del self.pathCollection[c] 138 | print("finished calculation",colorCount,self.pathCollection.keys()) 139 | def saveImg(self,loc): 140 | print("save image to "+loc) 141 | self.img.save(loc + "convertedImg.png") 142 | def replaceColors(self,lim,dotColors): 143 | colorCollection = set(dotColors) 144 | colors = [] 145 | for color in colorCollection: 146 | rgb = TCM.rgbToString(color) 147 | colors.sort(key=lambda c: TCM.calculateColorDifference(color, c)) 148 | if len(colors) > 0 and TCM.calculateColorDifference(color, colors[0]) < lim: 149 | closeColor = colors[0] 150 | # replaceColors[rgb] = closeColor 151 | # replacing 152 | for i, c in enumerate(dotColors): 153 | if c == color: 154 | dotColors[i] = closeColor 155 | for col in self.gridImg: 156 | for j, (pt, c) in enumerate(col): 157 | if c == color: 158 | col[j] = (pt, closeColor) 159 | else: 160 | colors.append(color) 161 | return dotColors,colors 162 | def exportOrderedGrid(self): 163 | centerByColorAndRow={} 164 | for color in self.pathCollection: 165 | centers=self.pathCollection[color] 166 | maxY=max([pt[1] for pt in centers]) 167 | centerByColorAndRow[color] = [[] for i in range(int(maxY/self.pixelization_length)+1)] 168 | # print(lastPtY,) 169 | #assign to list 170 | for pt in centers: 171 | idx=int(pt[1]/self.pixelization_length) 172 | centerByColorAndRow[color][idx].append(pt) 173 | #sort 174 | for i,lst in enumerate(centerByColorAndRow[color]): 175 | reverseLst=i%2==1 176 | lst.sort(key=lambda pt:pt[0],reverse=reverseLst) 177 | print("finished sorting") 178 | return centerByColorAndRow 179 | 180 | def exportOrderedGridListVersion(self): 181 | ''' 182 | reconstruct ordered grid 183 | :return: 184 | ''' 185 | exportGrid=[] 186 | for i, col in enumerate(self.gridImg): 187 | rowCopy=[] 188 | for j,ptRgb in enumerate(col): 189 | rowCopy.append(ptRgb) 190 | if i%2==0: 191 | rowCopy=rowCopy[::-1] 192 | exportGrid.append(rowCopy) 193 | return exportGrid 194 | 195 | def exportDelaunayPath(self): 196 | pathsByColor={} 197 | for color in self.pathCollection: 198 | points=np.array(self.pathCollection[color]) 199 | 200 | tri = Delaunay(points) 201 | triPts = points[tri.simplices] 202 | orderedPts = [] 203 | traveled = set() 204 | for i, triangle in enumerate(triPts): 205 | waitlist = [] 206 | for j in range(3): 207 | ptStr = ",".join([str(xy) for xy in triangle[j]]) 208 | if i != len(triPts) - 1 and triangle[j] in triPts[i + 1]: 209 | waitlist.append(triangle[j]) 210 | elif ptStr not in traveled: 211 | orderedPts.append(list(triangle[j])) 212 | traveled.add(ptStr) 213 | for pt in waitlist: 214 | ptStr = ",".join([str(xy) for xy in pt]) 215 | if ptStr not in traveled: 216 | orderedPts.append(list(pt)) 217 | traveled.add(ptStr) 218 | pathsByColor[color]=orderedPts 219 | return pathsByColor 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_punchneedle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/threadPlotter/TP_punchneedle/__init__.py -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_punchneedle/embroideryCalculation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | stores utility functions for embroidery-specific calculations 3 | ''' 4 | import threadPlotter.TP_utils.shapeEditing as SHAPE 5 | 6 | def makeConnectedDot(pathPoints,segmentLength,minDist): 7 | ''' 8 | converted from "convertPathsToConnectedDots" 9 | create punch needle points by 10 | keep spliting line.Will check minimal distance 11 | :param pathPoints: 12 | :param segmentLength: 13 | :return: 14 | ''' 15 | dotCenters=[] 16 | for i, pt in enumerate(pathPoints): 17 | if i==0: 18 | continue 19 | pt0=pathPoints[i-1] 20 | splitLines=SHAPE.splitSingleLine([pt0, pt], segmentLength,toPoint=True) 21 | for dotCenter in splitLines: 22 | dotCenters +=dotCenter 23 | i=1 24 | 25 | while i 0: 32 | complexPath=SHAPE.convertPathToComplexPointWithPathParser(pathString) 33 | for seg in complexPath: 34 | if len(seg) == 4: 35 | pts = self.convertCubicIntoLine(seg) 36 | self.points += pts 37 | elif len(seg)==2: 38 | pt = POINT.Point(seg[0], seg[1]) 39 | self.points.append(pt) 40 | else: 41 | print(seg,"is not a valid seg. It contains paths whose lengths are not 2 or 4") 42 | raise ValueError 43 | if "z" in pathString or "Z" in pathString: 44 | firstPt=self.points[0] 45 | pt = POINT.Point(firstPt.pt[0], firstPt.pt[1]) 46 | self.points.append(pt) 47 | self.closed=True 48 | # print("closedPath",self.getLength(),self.exportPlainList()) 49 | if len(self.points)>0: 50 | if self.points[0].roughEquals(self.points[-1]): 51 | self.closed=True 52 | self.getBBox() 53 | def appendPoint(self,x,y): 54 | pt = POINT.Point(x,y) 55 | self.points.append(pt) 56 | def appendPoints(self,pointList): 57 | for p in pointList: 58 | self.appendPoint(p[0],p[1]) 59 | 60 | def __len__(self): 61 | return len(self.points) 62 | def copy(self): 63 | ''' 64 | :return: a deep copy 65 | ''' 66 | return PathList(starterArray=self.exportPlainList()) 67 | def getPtByIdx(self,idx): 68 | return self.points[idx] 69 | 70 | def getBBox(self): 71 | ''' 72 | this bbox is x1,x2,y1,y2 though 73 | :return: 74 | ''' 75 | # print(self.points) 76 | bbox=SHAPE.getBoundaryBoxPtsVersion(self.exportPlainList()) 77 | 78 | self.bbox=bbox 79 | # print(bbox) 80 | self.w=bbox[1]-bbox[0] 81 | self.h=bbox[3]-bbox[2] 82 | self.isHorizontal=self.w>self.h 83 | return self.bbox 84 | def getBBoxWHversion(self): 85 | return [self.bbox[0],self.bbox[2],self.bbox[1]-self.bbox[0],self.bbox[3]-self.bbox[2]] 86 | 87 | # return SHAPE.getBBoxWHversion(self.exportPlainList()) 88 | def getLength(self): 89 | return len(self.points) 90 | def getCenter(self): 91 | ''' 92 | XMIN,XMAX YMIN YMAX 93 | :return: 94 | ''' 95 | self.getBBox() 96 | return [self.bbox[1] - self.bbox[0], self.bbox[3] + self.bbox[2]] 97 | def isSelfIntersecting(self): 98 | ''' 99 | Determine if a path is self intersecting 100 | using https://algs4.cs.princeton.edu/91primitives/ 101 | :return: 102 | ''' 103 | ##todo 104 | return False 105 | def isClosed(self): 106 | return self.closed 107 | def withinSizeLimit(self,wh): 108 | ''' 109 | return true if the width and height 110 | :param wh: 111 | :return: 112 | ''' 113 | # print(self.bbox,wh, self.bbox[1]-self.bbox[0]<=wh[0],self.bbox[3]-self.bbox[2]<=wh[1]) 114 | return self.bbox[1]-self.bbox[0]<=wh[0] and self.bbox[3]-self.bbox[2]<=wh[1] 115 | def withinBoundaryLimit(self,xmin,xmax,ymin,ymax): 116 | return self.bbox[0]>=xmin and self.bbox[1]<=xmax and self.bbox[2]>=ymin and self.bbox[3]<=ymax 117 | 118 | def adjustOriginToCenter(self): 119 | ''' 120 | this will translate everything along the 0,0 point 121 | :return: 122 | not automatically called by itself 123 | ''' 124 | # print(self.getPathString()) 125 | 126 | center=self.getCenter() 127 | self.translate(center[0],center[1]) 128 | self.getBBox() 129 | return center 130 | def adjustPathToLeftTop(self): 131 | leftTop=[self.bbox[0],self.bbox[2]] 132 | self.translate(-leftTop[0],-leftTop[1]) 133 | self.getBBox() 134 | def exportPlainList(self,precision=None): 135 | lst=[pt.pt.copy() for pt in self.points] 136 | if precision: 137 | lst=[[round(pt[0],precision),round(pt[1],precision)] for pt in lst] 138 | return lst 139 | def convertCubicIntoLine(self,seg,segLength=5): 140 | segPoints=SHAPE.splitSingleCubicIntoLinesPoints(seg,segLength) 141 | # print("segPoint",segPoints) 142 | pts=[] 143 | for lineSeg in segPoints: 144 | for pt in lineSeg: 145 | if type(pt[0])!=int and type(pt[0])!=float: 146 | print("pt is not numerical",pt,"seg",lineSeg) 147 | raise ValueError 148 | pt_point=POINT.Point(pt[0],pt[1]) 149 | if len(pts)>0 and pts[-1].roughEquals(pt_point): 150 | continue 151 | pts.append(pt_point) 152 | return pts 153 | def getPathString(self): 154 | # print(self.points) 155 | p = "M" + " L".join([str(pt) for pt in self.points]) 156 | if self.closed: 157 | return p + "Z" 158 | return p 159 | def translate(self,tx,ty): 160 | for pt in self.points: 161 | pt.translate(tx,ty,True) 162 | self.getBBox() 163 | def rotate(self,degree): 164 | for pt in self.points: 165 | # print(pt) 166 | pt.rotate(degree,True) 167 | self.getBBox() 168 | def rotateAroundPoint(self,center,degree): 169 | for pt in self.points: 170 | pt.rotateAroundPoint(center,degree,True) 171 | self.getBBox() 172 | def rotateAroundCenter(self,degree): 173 | center=[self.bbox[1]-self.bbox[0],self.bbox[3]-self.bbox[2]] 174 | self.rotateAroundPoint(center,degree) 175 | def scalePath(self,scaleFactor): 176 | for pt in self.points: 177 | pt.scalePointAccordingToCenter(scaleFactor, True) 178 | self.getBBox() 179 | def scalePathAccordingToCenter(self,center,scaleX,scaleY): 180 | for pt in self.points: 181 | pt.scalePointAccordingToCenter(center,scaleX,scaleY,True) 182 | self.getBBox() 183 | def offset(self,dist,jointType=None,offsetType=None): 184 | ''' 185 | :param dist: 186 | :return: 187 | ''' 188 | if not offsetType: 189 | offsetType = "CLOSEDPOLYGON" 190 | if not self.closed: 191 | offsetType = "OPENBUTT" 192 | if not jointType: 193 | jointType = "MITER" 194 | 195 | offsetList=CH.makeOffset(self.exportPlainList(),dist,offsetType=offsetType,jointType=jointType) 196 | if len(offsetList)>0: 197 | if self.closed or offsetType.upper().startswith("OPEN"): 198 | offsetList[0].append(offsetList[0][0].copy()) 199 | return offsetList[0] 200 | return offsetList 201 | def exportToStr(self): 202 | return "" 203 | 204 | def __str__(self): 205 | return SHAPE.getStraightPath(self.exportPlainList()) 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_structure/Point.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Stores a point 3 | ''' 4 | import math 5 | class NonNumericalError(Exception): 6 | 7 | def __init__(self, *args): 8 | if args: 9 | self.message=args[0] 10 | else: 11 | self.message="" 12 | def __str__(self): 13 | return "one of the location is not numerical:x and y\n"+str(self.message) 14 | class Point: 15 | def __init__(self,x,y): 16 | self.pt=[x,y] 17 | if not self.isNumerical(x) or not self.isNumerical(y): 18 | raise NonNumericalError(self.pt) 19 | # print(x,y) 20 | def __str__(self): 21 | precision=2 22 | return str(round(self.pt[0],precision)) + "," + str(round(self.pt[1],precision)) 23 | 24 | def copy(self): 25 | return Point(self.x(), self.y()) 26 | def toList(self): 27 | ''' 28 | return a clone of the pt list 29 | :return: 30 | ''' 31 | return [self.pt[0],self.pt[1]] 32 | 33 | def isNumerical(self,val): 34 | return type(val)==int or type(val)==float 35 | def x(self): 36 | return self.pt[0] 37 | def y(self): 38 | return self.pt[1] 39 | def roundPt(self,digit=2): 40 | # print(self.pt) 41 | self.pt[0]=round(self.pt[0],digit) 42 | self.pt[1]=round(self.pt[1],digit) 43 | return self.pt 44 | def rotate(self,angle,inPlace=False): 45 | rad = math.radians(angle) 46 | c = math.cos(rad) 47 | s = math.sin(rad) 48 | x_p = self.pt[0] * c - self.pt[1] * s, 3 49 | y_p = self.pt[0] * s + self.pt[1] * c, 3 50 | if inPlace: 51 | self.pt=[x_p,y_p] 52 | return [x_p,y_p] 53 | 54 | def rotateAroundPoint(self,origin,angleDegree,inPlace=False): 55 | """ 56 | Rotate a point counterclockwise by a given angle around a given origin. 57 | 58 | The angle should be given in radians. 59 | """ 60 | angleRad = math.radians(angleDegree) 61 | ox, oy = origin 62 | px, py = self.pt 63 | 64 | qx = ox + math.cos(angleRad) * (px - ox) - math.sin(angleRad) * (py - oy) 65 | qy = oy + math.sin(angleRad) * (px - ox) + math.cos(angleRad) * (py - oy) 66 | if inPlace: 67 | self.pt=[qx, qy] 68 | return [qx, qy] 69 | 70 | def scalePoints(self, scaleFactor,inPlace=False): 71 | ''' 72 | ASSUME SCALING FROM CENTER 73 | :param points: 74 | :param scaleFactor: 75 | :return: 76 | ''' 77 | scaled=[xy*scaleFactor for xy in self.pt] 78 | if inPlace: 79 | self.pt=scaled 80 | return scaled 81 | 82 | def scalePointAccordingToCenter(self, center, scaleX,scaleY,inPlace=False): 83 | scalePoints = [self.pt[0]*scaleX,self.pt[1]*scaleY] 84 | translateX = (1 - scaleX) * center[0] 85 | translateY = (1 - scaleY) * center[1] 86 | scaled=[scalePoints[0] + translateX, scalePoints[1] + translateY] 87 | if inPlace: 88 | self.pt=scaled 89 | return scaled 90 | def roughEquals(self,pt2): 91 | return str(self)==str(pt2) 92 | def translate(self,tx,ty,inplace=False): 93 | nPT=[self.pt[0]+tx,self.pt[1]+ty] 94 | if inplace: 95 | self.pt=nPT 96 | return nPT 97 | 98 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_structure/PunchGroup.py: -------------------------------------------------------------------------------- 1 | 2 | from threadPlotter.TP_structure.PathList import PathList 3 | from threadPlotter.TP_utils.shapeEditing import pressIntoABox 4 | from threadPlotter.TP_punchneedle import embroideryCalculation as EC 5 | class TransformError(Exception): 6 | def __init__(self, *args): 7 | if args: 8 | self.message=args[0] 9 | else: 10 | self.message="" 11 | def __str__(self): 12 | return self.message+" contains transformation. Make sure your svg has a flat structure(avoid applying transformations in groups),and all paths have no transformation." 13 | class InvalidPathPointInput(Exception): 14 | def __init__(self, *args): 15 | if args: 16 | self.message=args[0] 17 | else: 18 | self.message="" 19 | def __str__(self): 20 | return self.message+" is not a valid list, str, PathList object, or beautiful soup object." 21 | class PunchGroup(PathList): 22 | ''' 23 | contains one path 24 | modify path 25 | ''' 26 | 27 | def __init__(self,pathInput,id,toolId,skipSegment): 28 | ''' 29 | construct pathList 30 | Can only contain a path element 31 | :param path: 32 | ''' 33 | self.id = id 34 | self.toolId=toolId 35 | self.skipSegment=skipSegment 36 | if pathInput==None: 37 | PathList.__init__(self) 38 | elif isinstance(pathInput,list): 39 | #process from pathList 40 | PathList.__init__(self,starterArray=pathInput) 41 | elif isinstance(pathInput,PathList): 42 | PathList.__init__(self, starterArray=pathInput.exportPlainList()) 43 | elif isinstance(pathInput,str): 44 | PathList.__init__(self, pathString=pathInput) 45 | else: 46 | try: 47 | if "transform" in pathInput.attrs: 48 | raise TransformError(pathInput) 49 | d = pathInput.attrs["d"] 50 | PathList.__init__(self,pathString=d) 51 | except: 52 | raise InvalidPathPointInput(pathInput) 53 | self.originalPathList=self.exportPlainList().copy() 54 | 55 | def exportToPunchNeedleReadyPoints(self,segmentLength,boundaryRect,addStartingTrail=False,addEndingTrail=False,minDistance=2): 56 | ''' 57 | segment the punch groups by the segment length 58 | Tasks: 59 | 1. for each segment in the 60 | :return: a list of PunchPoint objects 61 | 62 | :param segmentLength: 63 | :param boundaryRect: XMIN,YMIN,XMAX,YMAX 64 | :param addStartingTrail: 65 | :param addEndingTrail: 66 | :param minDistance: 67 | :return: 68 | ''' 69 | plainPoints=self.exportPlainList(precision=2) 70 | if self.skipSegment: 71 | return plainPoints 72 | if addStartingTrail: 73 | plainPoints=[[0,0]]+plainPoints 74 | if addEndingTrail: 75 | plainPoints.append([0,0]) 76 | pressIntoABox(plainPoints,boundaryRect[0],boundaryRect[1],boundaryRect[2],boundaryRect[3]) 77 | 78 | dotList=EC.makeConnectedDot(plainPoints,segmentLength,minDistance) 79 | return dotList 80 | 81 | def exportTrailToAnotherPunchGroup(self,punchGroup2,trailLength,minDistance=2): 82 | ''' 83 | return a list of trail point centers that connects to pucnh group 2 84 | :param punchGroup2: 85 | :return: 86 | ''' 87 | try: 88 | thisPoint = self.points[-1].toList() 89 | nextPt=punchGroup2.getPtByIdx(0).toList() 90 | except IndexError: 91 | return [] 92 | dotList = EC.makeConnectedDot([thisPoint,nextPt], trailLength, minDistance) 93 | return dotList 94 | 95 | 96 | def restore(self): 97 | self.points=self.originalPathList.copy() 98 | self.getBBox() 99 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_structure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/threadPlotter/TP_structure/__init__.py -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/threadPlotter/TP_utils/__init__.py -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_utils/basic.py: -------------------------------------------------------------------------------- 1 | 2 | import pickle, json,time,os 3 | import random 4 | 5 | 6 | def rgbToHex(r,g,b): 7 | return '#%02x%02x%02x' % (r, g, b) 8 | def uniformFromRange(arr): 9 | return random.uniform(arr[0],arr[1]) 10 | def getRandomHex(): 11 | return "%06x" % random.randint(0, 0xFFFFFF) 12 | def getOrDefault(storage,key,default=None): 13 | if key in storage: 14 | return storage[key] 15 | return default 16 | 17 | 18 | def mkdir(path): 19 | if (not os.path.exists(path)): 20 | os.mkdir(path) 21 | return True 22 | return False 23 | def unitConvert(val,unit,i2p=96): 24 | i2cm=i2p/25.4 25 | multiplier=1 26 | if "in" in unit.lower(): 27 | multiplier=i2p 28 | elif "cm" in unit.lower(): 29 | multiplier=i2cm 30 | elif "mm" in unit.lower(): 31 | multiplier=i2cm*10 32 | return multiplier*val 33 | 34 | def linearScale(inputVal,domainArr,rangeArr): 35 | ''' 36 | d3 linearScale 37 | :param input: 38 | :param domainArr: 39 | :param rangeArr: 40 | :return: 41 | ''' 42 | inputDiff = domainArr[1] - domainArr[0] 43 | outputDiff = rangeArr[1] - rangeArr[0] 44 | if inputVal - domainArr[0] == 0: 45 | return rangeArr[0] 46 | return (inputVal - domainArr[0]) / inputDiff * outputDiff + rangeArr[0] 47 | def load_object(fileName): 48 | 49 | with open(fileName, 'rb') as inputF: 50 | obj = pickle.load(inputF) 51 | inputF.close() 52 | return obj 53 | def save_object(obj, filename): 54 | with open(filename, 'wb') as output: 55 | pickle.dump(obj, output, pickle.HIGHEST_PROTOCOL) 56 | def roundPoint(point): 57 | return [round(xy,2) for xy in point] 58 | 59 | def getPointFromComplex(complex): 60 | try: 61 | return roundPoint([complex.real,complex.img]) 62 | except Exception: 63 | return roundPoint([complex.real,complex.imag]) 64 | def pointEquals(p1,p2): 65 | return int(p1[0])==int(p2[0]) and int(p1[1])==int(p2[1]) 66 | 67 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_utils/clipperHelper.py: -------------------------------------------------------------------------------- 1 | 2 | import pyclipper 3 | 4 | 5 | OFFSETTYPES={ 6 | "CLOSEDPOLYGON":pyclipper.ET_CLOSEDPOLYGON, 7 | "CLOSEDLINE":pyclipper.ET_CLOSEDLINE, 8 | "OPENROUND":pyclipper.ET_OPENROUND, 9 | "OPENSQUARE":pyclipper.ET_OPENSQUARE, 10 | "OPENBUTT":pyclipper.ET_OPENBUTT 11 | } 12 | JOINTTYPES={ 13 | "MITER":pyclipper.JT_MITER, 14 | "ROUND":pyclipper.JT_ROUND, 15 | "SQUARE":pyclipper.JT_SQUARE 16 | } 17 | CLIPPER_TYPE={"intersection":pyclipper.CT_INTERSECTION,"union":pyclipper.CT_UNION,"difference":pyclipper.CT_DIFFERENCE,"xor":pyclipper.CT_XOR} 18 | FILL_TYPE={"evenodd":pyclipper.PFT_EVENODD,"positive":pyclipper.PFT_POSITIVE,"negative":pyclipper.PFT_NEGATIVE,"nonzero":pyclipper.PFT_NONZERO} 19 | 20 | 21 | def makeOffset(subjPoints,offset_width,offsetType="CLOSEDPOLYGON",jointType="MITER"): 22 | ''' 23 | http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Types/EndType.htm 24 | http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Types/JoinType.htm 25 | :param subjPoints: 26 | :param offset_width: 27 | :return: 28 | ''' 29 | offsetType=OFFSETTYPES[offsetType.upper()] 30 | jointType=JOINTTYPES[jointType.upper()] 31 | pco = pyclipper.PyclipperOffset() 32 | sub_s=pyclipper.scale_to_clipper(subjPoints) 33 | pco.AddPath(sub_s,jointType, offsetType) 34 | solution = pco.Execute(pyclipper.scale_to_clipper(offset_width)) 35 | return pyclipper.scale_from_clipper(solution) 36 | 37 | 38 | 39 | 40 | def makeClipper(subjs,clip,clipperTypeStr,s_fill_key="positive",c_fill_key="positive",subjClosed=True,s_multi=False,c_multi=False): 41 | ''' 42 | 43 | :param subjs: 44 | :param clip: window to be used for the cut 45 | :param clipperTypeStr: 46 | :param s_fill_key: 47 | :param c_fill_key: 48 | :param subjClosed: 49 | :param s_multi: 50 | :param c_multi: 51 | :return: 52 | ''' 53 | pc = pyclipper.Pyclipper() 54 | scaledSubj = pyclipper.scale_to_clipper(subjs) 55 | scaledClip = pyclipper.scale_to_clipper(clip) 56 | # print("clip",scaledClip) 57 | clipperType = CLIPPER_TYPE[clipperTypeStr] 58 | if s_multi: 59 | pc.AddPaths(scaledSubj, pyclipper.PT_SUBJECT, subjClosed) 60 | else: 61 | pc.AddPath(scaledSubj, pyclipper.PT_SUBJECT, subjClosed) 62 | if c_multi: 63 | pc.AddPaths(scaledClip, pyclipper.PT_CLIP, True) 64 | else: 65 | pc.AddPath(scaledClip, pyclipper.PT_CLIP, True) 66 | fill1=FILL_TYPE[s_fill_key] 67 | fill2=FILL_TYPE[c_fill_key] 68 | flattenPaths = [] 69 | solution = pc.Execute2(clipperType, fill1, fill2) 70 | paths = pyclipper.PolyTreeToPaths(solution) 71 | # print(paths) 72 | for p in paths: 73 | flattenPaths.append(pyclipper.scale_from_clipper(p)) 74 | return flattenPaths 75 | 76 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_utils/fillPath.py: -------------------------------------------------------------------------------- 1 | ''' 2 | package to fill things 3 | ''' 4 | import math,re,random 5 | import threadPlotter.TP_utils.shapeEditing as SHAPE 6 | 7 | 8 | def convertLinePathToDots(linePts,gap,innerDist=1,masterBoundary=None,addGap=False,locOnly=False): 9 | lines=[] 10 | for i, pt in enumerate(linePts): 11 | if i==0: 12 | continue 13 | pt0=linePts[i-1] 14 | if pt0!=None: 15 | #TODO : there shouldn't be a none 16 | lines += SHAPE.splitSingleLine([pt0, pt], gap) 17 | pointPaths=[] 18 | prevEnd=None 19 | appendGap=False 20 | for line in lines: 21 | start, middle, endTag, end = PO.parseToStartAndEnd(line) 22 | startPt = [round(float(xy), 2) for xy in start.split(",")] 23 | endPt = [xy + innerDist for xy in startPt] 24 | 25 | if masterBoundary==None or (masterBoundary!=None and PO.ptWithinRect(masterBoundary[0],masterBoundary[1],masterBoundary[2],masterBoundary[3],startPt)): 26 | if appendGap: 27 | newLine=[prevEnd,startPt] 28 | gapLine,gapPointPaths=convertLinePathToDots(newLine,gap,innerDist,masterBoundary=masterBoundary,addGap=addGap,locOnly=locOnly) 29 | pointPaths+=gapPointPaths 30 | path = SHAPE.getStraightPath([startPt, endPt]) 31 | if locOnly: 32 | path=startPt.copy() 33 | pointPaths.append(path) 34 | prevEnd = startPt 35 | appendGap=False 36 | elif addGap: 37 | appendGap=True 38 | 39 | return lines,pointPaths 40 | 41 | def makeTinyDot_pathPoints(startPt,innerDist=1): 42 | endPt = [xy + innerDist for xy in startPt] 43 | return [startPt, endPt] 44 | 45 | 46 | def convertPathsToConnectedDots(pathPoints,boundaryRect=None,dotDistance=5,locOnly=False): 47 | ''' 48 | create punchNeedle ready path. connect all paths together into one, and change it to dots 49 | :param pathPoints:(x,y,w,h, 50 | :return: 51 | ''' 52 | connectedPathPoint=[] 53 | for pathP in pathPoints: 54 | connectedPathPoint += pathP 55 | line,pointPaths=convertLinePathToDots(connectedPathPoint,dotDistance,masterBoundary=boundaryRect,addGap=True,locOnly=locOnly) 56 | return line,pointPaths 57 | 58 | def getPunchNeedleReadyDot(pathPoints,boundaryRect,dotDistance=4,addTrail=True,minDistance=2): 59 | ''' 60 | required boundaryRect 61 | Rule: 1) min/max distance is controlled 62 | 2) try to reduce duplication 63 | :param pathPoint: 64 | :param boundaryRect: (x,y,w,h, 65 | :param addTrail: add returning trail 66 | :return: 67 | ''' 68 | if addTrail: 69 | pathPoints.append([pathPoints[-1][-1],[0,0]]) 70 | line, pointPaths=convertPathsToConnectedDots(pathPoints,boundaryRect,dotDistance=dotDistance,locOnly=True) 71 | i=1 72 | while i xmax: 50 | p[0] = xmax 51 | if y > ymax: 52 | p[1] = ymax 53 | def getStraightPath(pointArr,closed=False): 54 | p="M"+" L".join([",".join([str(round(l,2)) for l in xy[:2]]) for xy in pointArr]) 55 | if closed: 56 | return p+"Z" 57 | return p 58 | def splitSingleLine(start_end,unitLength,toPoint=False): 59 | ''' 60 | return strings 61 | :param start_end: 62 | :param unitLength: 63 | :return: 64 | ''' 65 | if UB.pointEquals(start_end[0],start_end[1]): 66 | return [] 67 | pathStr=getStraightPath(start_end) 68 | path=parse_path(pathStr) 69 | try: 70 | l=path.length() 71 | if l>unitLength: 72 | paths=[] 73 | t=unitLength/l 74 | ts=0 75 | te=t 76 | for i in range(int(math.ceil(l/unitLength))): 77 | seg=Line(path.point(ts),path.point(te)) 78 | p=Path(seg) 79 | if toPoint: 80 | paths.append([UB.getPointFromComplex(path.point(ts)), UB.getPointFromComplex(path.point(te))]) 81 | else: 82 | paths.append(p.d()) 83 | 84 | ts+=t 85 | te+=t 86 | te=min(1,te) 87 | if toPoint: 88 | paths.append( 89 | [UB.getPointFromComplex(path.point(te)), UB.getPointFromComplex(path.point(1))]) 90 | else: 91 | paths.append(getStraightPath([UB.getPointFromComplex(path.point(te)),UB.getPointFromComplex(path.point(1))])) 92 | return paths 93 | if toPoint: 94 | return [start_end] 95 | else: 96 | return [pathStr] 97 | except Exception as e: 98 | print(start_end,"something wrong with the splitLine",e) 99 | return [] 100 | def calculateDistBetweenPoints(p1, p2): 101 | return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) 102 | def calculatePathLength(pathPoints): 103 | l=0 104 | if len(pathPoints)<2: 105 | return l 106 | for i in range(1,len(pathPoints)): 107 | l+=calculateDistBetweenPoints(pathPoints[i-1],pathPoints[i]) 108 | return l 109 | def makeRectPoints(x,y,w,h,closed=False): 110 | pts=[[x,y],[x+w,y],[x+w,y+h],[x,y+h]] 111 | if closed: 112 | pts.append([x,y]) 113 | return pts 114 | def makeUniformPolygon(x, y, r, sideCt,closed=False): 115 | points = [] 116 | for i in range(sideCt): 117 | points.append([x+r * math.cos(2 * math.pi * i / sideCt), y+r * math.sin(2 * math.pi * i / sideCt)]) 118 | if closed: 119 | points.append(points[0].copy()) 120 | return points 121 | 122 | def convertListToDotStr(dotList,dist=1): 123 | ''' 124 | Given a list of points, construct mini "dots" that are actually short lines (controlled by dist) 125 | :param dotList: 126 | :param dist: 127 | :return: a list of path strings. 128 | ''' 129 | strList=[] 130 | for pt in dotList: 131 | ps=getStraightPath([pt,[xy+dist for xy in pt]]) 132 | strList.append(ps) 133 | return strList -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/TP_utils/svg.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from threadPlotter.TP_utils.basic import unitConvert 3 | import re 4 | 5 | svgStarter='' 6 | def makeExportReadySvg(baseSoup,repList=None): 7 | defaultRep = ["path", "rect", "circle", "line", "polygon"] 8 | if repList: 9 | defaultRep += repList 10 | svgStr = baseSoup.prettify() 11 | for rep in defaultRep: 12 | if rep in svgStr: 13 | svgStr = "".join(svgStr.split("")) 14 | pathBackRef = re.compile("(<" + rep + ".*?)(>)") 15 | svgStr = pathBackRef.sub(r'\1/>', svgStr) 16 | return svgStr 17 | 18 | def saveSVG(baseSoup,fileName=None,saveLoc=None,postTag=None,repList=None,fullPath=None): 19 | svgStr=makeExportReadySvg(baseSoup,repList) 20 | if fullPath: 21 | sl=fullPath 22 | else: 23 | sl=saveLoc+fileName+postTag+".svg" 24 | with open(sl,'w') as saveFile: 25 | saveFile.write(svgStr) 26 | 27 | 28 | def makeSvg(): 29 | return BeautifulSoup(svgStarter, "html.parser") 30 | 31 | def applyAttrs(soup,attrs): 32 | for key in attrs: 33 | soup.attrs[key]=attrs[key] 34 | def makeSVGwithBasic(wh,margins,id="main_g",innerBox=False,outerBox=False): 35 | svg = makeSvg() 36 | attrs={ 37 | "width":str(wh[0])+"px","height":str(wh[1])+"px", 38 | "viewBox":"0 0 "+str(wh[0])+" "+str(wh[1]) 39 | } 40 | applyAttrs(svg.svg, attrs) 41 | defs = svg.new_tag("defs") 42 | svg.svg.append(defs) 43 | main_g = svg.new_tag("g") 44 | main_g.attrs = {"id": id, "transform": "translate(" + str(margins["l"]) + "," + str(margins["t"]) + ")"} 45 | svg.svg.append(main_g) 46 | if outerBox: 47 | addComponent(svg, svg.svg, "rect", { 48 | "x": 0, 49 | "y": 0, 50 | "width": wh[0], 51 | "height": wh[1], 52 | "fill": "none", 53 | "stroke": "black", 54 | "id":"outerBox" 55 | }) 56 | if innerBox: 57 | innerRect = svg.new_tag("rect") 58 | applyAttrs(innerRect, { 59 | "x": margins["l"], 60 | "y": margins['t'], 61 | "width": wh[0]-margins["l"]-margins["r"], 62 | "height": wh[1]-margins["t"]-margins["b"], 63 | "fill": "none", 64 | "stroke": "black", 65 | "id":"innerBox" 66 | }) 67 | svg.svg.append(innerRect) 68 | 69 | 70 | return svg 71 | def addComponent(soupBase,base,tagName,attrs): 72 | e = soupBase.new_tag(tagName) 73 | applyAttrs(e,attrs) 74 | base.append(e) 75 | return e 76 | 77 | def makeBasicSvgWithFoundations(basicSettings,unit,i2p): 78 | ''' 79 | SHORTER VERSION, WITHOUT TOOLPAD 80 | :param basicsettings: 81 | :return: 82 | ''' 83 | wkey = "width" 84 | hKey = "height" 85 | if "paperWidth" in basicSettings: 86 | wkey="paperWidth" 87 | hKey="paperHeight" 88 | if unit.upper()=="PX": 89 | width_height=[basicSettings[wkey],basicSettings[hKey]] 90 | #init svg 91 | else: 92 | width_height = [unitConvert(v,unit,i2p) for v in [basicSettings[wkey],basicSettings[hKey]]] 93 | 94 | #make margin box 95 | margins=basicSettings["margins"].copy() 96 | for k in margins: 97 | margins[k] = unitConvert(margins[k], unit, i2p) 98 | 99 | svg = makeSVGwithBasic(width_height, margins) 100 | 101 | boundaryRect = {} 102 | wh_m = [width_height[0] - margins["l"] - margins["r"], width_height[1] - margins['t'] - margins["b"]] 103 | # print("wh", width_height, wh_m, margins) 104 | if("displayOuterRect" in basicSettings and basicSettings["displayOuterRect"]): 105 | boundaryRect["outer"]=addComponent(svg,svg.svg,"rect",{ 106 | "x":0, 107 | "y":0, 108 | "width":width_height[0], 109 | "height":width_height[1], 110 | "fill":"none", 111 | "stroke":"black" 112 | }) 113 | 114 | if("displayInnerRect" in basicSettings and basicSettings["displayInnerRect"]): 115 | innerRect=svg.new_tag("rect") 116 | applyAttrs(innerRect,{ 117 | "x":margins["l"], 118 | "y":margins['t'], 119 | "width":wh_m[0], 120 | "height":wh_m[1], 121 | "fill":"none", 122 | "stroke":"black" 123 | }) 124 | svg.svg.append(innerRect) 125 | boundaryRect["inner"] = innerRect 126 | 127 | return svg, width_height,wh_m,boundaryRect,margins 128 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/ThreadPlotter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | threadPlotter is a class that contains all the basic functions for generating a plotter-based punch needle embroidery piece. 3 | One project is associated with one design, and can output to svgs and python files (for Axidraw) 4 | ''' 5 | 6 | from threadPlotter.DirectAuthoringGenerator import DirectAuthoringGenerator as DirectAuthoringGenerator 7 | from threadPlotter.TP_utils.shapeEditing import convertListToDotStr 8 | 9 | from threadPlotter.TP_punchneedle import threadColorManagement as TCM 10 | from threadPlotter.TP_utils import basic as UB 11 | from threadPlotter.TP_utils import svg as SVG 12 | from threadPlotter.TP_structure.PunchGroup import PunchGroup 13 | import json 14 | 15 | class ThreadPlotter(DirectAuthoringGenerator): 16 | 17 | def initSpeedAndDepthMap(self): 18 | ''' 19 | generate linear mapping functions for the depth and speed 20 | :return: 21 | ''' 22 | plotterSettingRange=self.currentSpec["plotterSettingRange"] 23 | self.depthRange=plotterSettingRange["depthPercRange"] 24 | self.speedRange=plotterSettingRange["speedPercRange"] 25 | distanceRange=plotterSettingRange["distanceRange"] 26 | self.distanceRange=[UB.unitConvert(d,self.unit,self.i2p) for d in distanceRange] 27 | 28 | def depthSpeedCalculator_batch(self,distance): 29 | storage={} 30 | for dist in distance: 31 | storage[dist]=self.depthSpeedCalculator(dist) 32 | return storage 33 | def depthSpeedCalculator(self,dist): 34 | ''' 35 | given distance, calculate speed and depth 36 | 37 | :param distance: a list of distances (in px) 38 | :return: 39 | ''' 40 | speed = UB.linearScale(dist, self.distanceRange, self.speedRange) 41 | depth = UB.linearScale(dist, self.distanceRange, self.depthRange) 42 | return {"pen_pos_down": int(depth), "pen_rate_raise": int(speed)} 43 | 44 | 45 | 46 | def closeFiles(self): 47 | ''' 48 | Assuming all information has been stored in the list: 49 | self.punchGroupCollection 50 | for each punch group, process the information contained (segment into center points) and export to svg and python. 51 | :return: 52 | ''' 53 | lastPgId=len(self.punchGroupCollection)-1 54 | boundaryBox=[0,0,self.wh_m[0],self.wh_m[1]] 55 | 56 | for pgi,punchGroup in enumerate(self.punchGroupCollection): 57 | toolId=punchGroup.toolId 58 | dotList=punchGroup.exportToPunchNeedleReadyPoints(self.segmentLength,boundaryBox) 59 | if len(dotList)<1: 60 | continue 61 | trailList=[] 62 | if pgi!=lastPgId: 63 | trailList=punchGroup.exportTrailToAnotherPunchGroup( 64 | self.punchGroupCollection[pgi+1], 65 | self.trailStitchLength 66 | ) 67 | self.updateOptions(self.stitchSetting,toolId) 68 | for pt in dotList: 69 | self.addDrawPt(pt,toolId) 70 | self.updateOptions(self.trailStitchSetting,toolId) 71 | for trailPt in trailList: 72 | self.addDrawPt(trailPt,toolId) 73 | 74 | pathStrs = convertListToDotStr(dotList+trailList) 75 | for ps in pathStrs: 76 | self.addPath(self.svg.g, ps, self.svg, self.tools[toolId]) 77 | if hasattr(self, "toolSvgs"): 78 | self.addPath(self.toolSvgs[toolId].g, ps, self.toolSvgs[toolId], 79 | self.tools[toolId]) 80 | 81 | 82 | def saveFiles(self): 83 | print("exporting to " + self.getFullSaveLoc()) 84 | self.closeFiles() 85 | DirectAuthoringGenerator.saveFiles(self) 86 | #export thread matching guide 87 | self.generateColorPlan() 88 | 89 | def initPunchGroup(self,toolId,startingPathPoints=None,skipSegment=False): 90 | ''' 91 | make a thread group, append to storage. 92 | :param startingPathPoints: 93 | :return: index of this threadGroup 94 | ''' 95 | pgI=len(self.punchGroupCollection) 96 | self.punchGroupCollection.append( 97 | PunchGroup(startingPathPoints,pgI,toolId,skipSegment) 98 | ) 99 | return pgI 100 | def generateColorPlan(self): 101 | ''' 102 | export the color plan into svg and json files 103 | :return: 104 | ''' 105 | 106 | svg, width_height, wh_m, boundaryRect, margins= SVG.makeBasicSvgWithFoundations({"paperWidth":5, "paperHeight": len(self.colorList) * 2, "inchToPx":96, "margins":{"l":0.1, "r":0.1, "t":0.1, "b":0.1}}, "inch", 96) 107 | boxW=wh_m[0] 108 | boxH=wh_m[1]/len(self.colorList) 109 | fontSize=15 110 | for i, colorObj in enumerate(self.colorList): 111 | originalRgb="RGB("+",".join([str(s) for s in self.rgbList[i]])+")" 112 | y=boxH*i 113 | SVG.addComponent(svg, svg.g, "rect", 114 | {"x": 0, "y": y, "width": boxW, "height": boxH, 115 | "fill": "none",'stroke':"black"}) 116 | 117 | #text original color 118 | idxText= SVG.addComponent(svg, svg.g, "text", {"x":fontSize, "y": fontSize + y, "style": 'font-size:' + str(fontSize) + ';'}) 119 | idxText.string="Tool:"+str(i)+" "+str(originalRgb) 120 | SVG.addComponent(svg, svg.g, "rect", 121 | {"x": 0, "y": fontSize*2 + y, "width":boxW,"height":boxH/4,"fill":originalRgb}) 122 | 123 | #generate matched color 124 | if "mixedExpect" not in colorObj: 125 | #make one grid 126 | gridWidth=boxW 127 | else: 128 | gridWidth=boxW/4 129 | jy=fontSize*3 + y+boxH/4 130 | textY=fontSize*3 + y+boxH/2 131 | 132 | for j,cObj in enumerate(colorObj["c"]): 133 | rgbStrToAppend=TCM.rgbToString(cObj["rgb"]) 134 | jx=j*gridWidth 135 | SVG.addComponent(svg, svg.g, "rect", 136 | {"x": jx, "y": jy, "width": gridWidth, "height": boxH / 4, 137 | "fill": rgbStrToAppend}) 138 | idxText = SVG.addComponent(svg, svg.g, "text", {"x": 0, "y": textY + fontSize + j * fontSize, "style": 'font-size:' + str(fontSize * 0.8) + ';'}) 139 | idxText.string = "|".join(["id:"+str(cObj["i"]),"code:"+str(cObj['code']),rgbStrToAppend]) 140 | if "mixedExpect" in colorObj: 141 | SVG.addComponent(svg, svg.g, "rect", 142 | {"x": gridWidth*3, "y": jy, "width": gridWidth, "height": boxH / 4, 143 | "fill": TCM.rgbToString(colorObj["mixedExpect"]),"stroke":"black"}) 144 | #save 145 | SVG.saveSVG(svg, fullPath=self.getFullSaveLoc("tool") + ".svg") 146 | with open(self.getFullSaveLoc("threadColor")+ ".json", 'w') as outfile: 147 | json.dump({"colorList":self.colorList,"tools":self.tools}, outfile) 148 | def generate(self): 149 | ''' 150 | sample generation 151 | :return: 152 | ''' 153 | return 154 | 155 | def pickRandomThreadColors(self,ct=None): 156 | ''' 157 | pick random thread colors (contains potential color mixing) 158 | :param ct: 159 | :return: 160 | ''' 161 | if not ct: 162 | ct=self.basicSettings["toolsCt"] 163 | self.rgbList,self.colorList=TCM.pickRandomThreadColor(ct) 164 | for i in range(ct): 165 | self.tools[i]["stroke"]="RGB("+",".join([str(s) for s in self.rgbList[i]])+")" 166 | # print(self.tools) 167 | print("picked random color:",self.tools) 168 | def matchColor(self,colorList,allowMix=True): 169 | ''' 170 | given a color list (list of rgb tuples), match the best thread color and assign to this threadPlotter object 171 | :param colorList: 172 | :return: 173 | ''' 174 | self.rgbList=colorList 175 | self.plainColor,self.mixedColor,self.colorList=TCM.pickThreadColor(colorList,allowMix=allowMix) 176 | def __init__(self,settings,batchName="",svg=True,toolSvg=True): 177 | DirectAuthoringGenerator.__init__(self,settings,batchName=batchName,svg=svg,toolSvg=toolSvg) 178 | #extract thread plotter specific information from the settings 179 | self.initSpeedAndDepthMap() 180 | self.segmentLength=UB.unitConvert(self.currentSpec["segmentLength"],self.unit,self.i2p) 181 | self.trailStitchLength=UB.unitConvert(self.currentSpec["trailStitchLength"],self.unit,self.i2p) 182 | self.pickRandomThreadColors() 183 | self.stitchSetting=self.depthSpeedCalculator(self.segmentLength) 184 | self.trailStitchSetting=self.depthSpeedCalculator(self.trailStitchLength) 185 | self.punchGroupCollection=[] 186 | -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiciaHe/threadPlotter/64398e34c235305c2a5f4f23dc4f4d0524e85c63/threadPlotter/threadPlotter/__init__.py -------------------------------------------------------------------------------- /threadPlotter/threadPlotter/updateColor.py: -------------------------------------------------------------------------------- 1 | from threadPlotter.TP_utils import basic as UB 2 | from itertools import combinations 3 | import math,os,pkg_resources 4 | 5 | PKL_PATH=pkg_resources.resource_filename('threadPlotter', 'TP_punchneedle/threadColor.pkl') 6 | CSV_PATH=pkg_resources.resource_filename('threadPlotter', 'TP_punchneedle/embroidery_thread_color.csv') 7 | 8 | def exportThreadColorListPKL(): 9 | ''' 10 | export list of thread accordingn to settings according to here: 11 | https://pypi.org/project/pyembroidery/ 12 | :return: 13 | ''' 14 | seedFile = CSV_PATH 15 | 16 | originalColor=[] 17 | i=0 18 | with open(seedFile,"r") as seed: 19 | for row in seed: 20 | if "name" in row or "," not in row: 21 | continue 22 | content=row.strip().split(",") 23 | c={ 24 | "id":i, 25 | "color":(int(content[3]),int(content[4]),int(content[5])), 26 | "code":content[1], 27 | "name":content[2], 28 | "brand":"NEWBROTHREAD" 29 | } 30 | i+=1 31 | originalColor.append(c) 32 | saveDir= os.path.join(this_dir, "TP_punchneedle", "original_thread_list.pkl") 33 | UB.save_object(originalColor, saveDir) 34 | 35 | def getAverageColor(c1,c2,c3): 36 | ''' 37 | return avg rgb 38 | :param c1: 0-255 triplets 39 | :param c2: 40 | :param c3: 41 | :return: 42 | ''' 43 | 44 | comb=[c1,c2,c3] 45 | r=sum([c[0]**2 for c in comb]) 46 | g=sum([c[1]**2 for c in comb]) 47 | b=sum([c[2]**2 for c in comb]) 48 | 49 | return (int(math.sqrt(r / 3.0)), int(math.sqrt(g / 3.0)), int(math.sqrt(b / 3.0))) 50 | def makeColorCombinations(): 51 | ''' 52 | take the csv for embroidery thread color and build a python pkl. store locally 53 | :return: 54 | ''' 55 | seedFile = CSV_PATH 56 | 57 | originalColor=[] 58 | i=0 59 | with open(seedFile,"r") as seed: 60 | for row in seed: 61 | if "name" in row or "," not in row: 62 | continue 63 | content=row.strip().split(",") 64 | c={ 65 | "i":i, 66 | "rgb":(int(content[3]),int(content[4]),int(content[5])), 67 | "code":content[1], 68 | "name":content[2] 69 | } 70 | i+=1 71 | originalColor.append(c) 72 | 73 | 74 | combs=combinations(list(range(i))*3,3) 75 | combinedColor=[] 76 | for c1i,c2i,c3i in combs: 77 | c1=originalColor[c1i]["rgb"] 78 | c2=originalColor[c2i]["rgb"] 79 | c3=originalColor[c3i]["rgb"] 80 | avgColor=getAverageColor(c1,c2,c3) 81 | combinedColor.append(((c1i,c2i,c3i),avgColor)) 82 | print(len(combinedColor)) 83 | 84 | UB.save_object({"original":originalColor,"mixed":combinedColor},PKL_PATH) 85 | 86 | if __name__=="__main__": 87 | makeColorCombinations() 88 | -------------------------------------------------------------------------------- /tutorial/step1_plotterCheck.md: -------------------------------------------------------------------------------- 1 | # Step 1: Plotter Check 2 | Not all X-Y plotter is suitable for the task. Before you start, you should double-check that your machine is suitable for the task. Our [paper](http://www.cond.org/punchneedle.pdf) describe this in detail. 3 | 4 | In our experiment, we chose to convert the commercially available [AxiDraw Pen Plotter (V3/A3 model)](https://axidraw.com/). 5 | 6 | In our study, we hoped to avoid physically modifying our plotter to perform its original function. We identify the following criteria for selecting a plotter. 7 | 8 | #### You can use a plotter (or any other CNC) if all of these criteria are met: 9 | 10 | 1. It provides movement in 3 directions (x, y, and z). 11 | 2. It can hold a punch needle tool. 12 | 3. The distance between the plotting tool and the plotting surface is adjustable and sufficient for holding a fabric stretching frame. 13 | 1. e.g., if your plotter has a paper feeder, it might not be able to perform the task. 14 | 15 | 4. The z-axis movement is large enough to create a minimal stitch. 16 | 1. If your plotter cannot move more than 5cm in the z-direction, it might not be able to perform the task. 17 | 18 | 5. A sufficient downward force can be applied in the z-axis to punch through the tightened fabric. 19 | 1. If your plotter is not drawing on a flat surface, it might be challenging to add additional z-axis force. 20 | 21 | ## Next Step 22 | Once you have the right plotter, please take a look at [step 2: prepare materials](step2_physicalSetup.md). 23 | -------------------------------------------------------------------------------- /tutorial/step2_physicalSetup.md: -------------------------------------------------------------------------------- 1 | # Step 2: Physical Setup 2 | 3 | In this section, we will go over the materials needed for the fabrication process, including fabric, fabric stretcher, thread, thread feeder, clamp, and punch needle. 4 | 5 | ![thread and tool](../assets/03_thread_and_tool.jpg "threadAndTool") 6 | 7 | ## Everything you need 8 | 9 | Here's an overview of everything you need for the project. We included purchase links for these materials for your convenience. We are not affiliated with any of the manufacture/links/platforms listed here. 10 | 1. A plotter ([Axidraw](https://shop.evilmadscientist.com/productsmenu/890)) 11 | 2. At least three spools of polyester embroidery thread ([New Brothread](https://www.amazon.com/New-brothread-Polyester-Embroidery-Husqvarna/dp/B077Z5VJHN/ref=sr_1_8?dchild=1&keywords=polyester+thread&qid=1592946623&sr=8-8)) 12 | 3. Organza (fabric stores) 13 | 4. Extra fine punch needle heads ([Various manufacturers](https://www.amazon.com/BAGERLA-Embroidery-Scissors-Stitching-Beginners/dp/B07XK2X776/ref=sr_1_6?dchild=1&keywords=punch+needle&qid=1592946704&sr=8-6)) 14 | 5. Gripper Frame ([Found on Etsy. We purchased a 10 x 10. The link is just an example](https://www.etsy.com/listing/701260649/punch-needle-embroidery-frame-8-x-8?ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=gripper+frame&ref=sr_gallery-1-2&organic_search_click=1&cns=1)) 15 | 6. Syringe, a plastic cup, and a pen cap 16 | 7. X-acto knife 17 | 8. clamps for stabilizing the plotter 18 | 19 | Optional: 20 | Plywood and Laser cutter (to make the thread station and the thread separator) 21 | 22 | ## Why? 23 | In our paper, we listed the materials we have tested and described why we think the materials we picked work for our plotter. Your plotter and your fabrication needs might require different combinations. 24 | 25 | Please use our [paper](http://www.cond.org/punchneedle.pdf) as the main reference point. In this tutorial, we mainly go over the making of the accessories (threading station, thread separator, and frame holder) 26 | 27 | ### threading station, thread separator, and frame holder 28 | *The thread station ensures that the spools are unwinding from the top of the spool. 29 | 30 | *The thread separator lift the thread above the plotter. It also ensures that multiple strands of threads move independently. 31 | 32 | You can directly download these two SVG files ([svg1](../assets/thread%20feeder-03.svg), [svg2](../assets/thread%20feeder-04.svg)) and fabricate them. If you want to modify the accessories for your own machine (e.g., adjust width/height of the station), you can download this [accessory.ai](../assets/thread%20feeder.ai) file and modify it in the adobe illustrator. 33 | 34 | * Frame holder works as a registration tool for the gripper frame. We laser-cut a wooden square, but you can find alternatives such as using a clamp or heavy items (books, for example) to form an opening. 35 | 36 | 37 | ## Next Step 38 | Once you have the right plotter, please take a look at [step 3: make a pattern](step3_patternMaking.md). 39 | 40 | 41 | -------------------------------------------------------------------------------- /tutorial/step3_patternMaking.md: -------------------------------------------------------------------------------- 1 | # ThreadPlotter 2 | Thread plotter package is a toolkit that can 3 | 1. store and process trajectories (paths), which are list of points that are connected in certain order. 4 | 2. ThreadPlotter only accept points and connect them with straight lines. If you have a curve (e.g., cubic bezier), you will need to approximate them with straight lines. 5 | 2. write svg and axidraw-controlling python scripts. 6 | 1. We utilize [the axidraw api](https://axidraw.com/doc/py_api/#functions-interactive) to get access to fundamental controls such as pen_up and pen_down. (Don't forget to [install pyaxidraw on the computer that you are using to run the script](https://axidraw.com/doc/py_api/#installation).) 7 | 8 | 2. If you use a different plotter, the python scripts would not work on your plotter. However, you can use the same strategy we took and modify the library to author in your own syntax. 9 | 10 | ## Install 11 | We would recommend that you clone this repository so you have access to all of the sample projects. It also makes it easier for you to modify the threadPlotter, especially if you want to use it on plotters other than Axidraw. 12 | 13 | Here's a list of dependencies that you might need to install. 14 | ```angular2html 15 | 'Pillow >=7.1.2', 16 | 'pyclipper>=1.1.0.post3', 17 | 'svgpathtools>=1.3.3', 18 | 'bs4 >=0.0.1' 19 | ``` 20 | 21 | Alternatively, you can install using pip. 22 | ```angular2html 23 | pip install threadPlotter 24 | ``` 25 | 26 | ## Hello Plotter Example 27 | To make a pattern, you need to initiate a ThreadPlotter instance and input pattern content. 28 | 29 | To customize a threadPlotter, you need a dictionary to store the settings. 30 | 31 | For example, 32 | ```python 33 | settings={ 34 | "name":"tp00_boundaryTester",#name of the project,required 35 | "baseSaveLoc":"C:/licia/art/generative/", #specify where to save the generated files,required 36 | "basic":{...},#stores settings related to the canvas. Will be stored as self.basicSettings. 37 | "spec":{...},#stores any user-defined specs. Will be stored as self.currentSpec 38 | } 39 | ``` 40 | 41 | The "basic" key stores canvas-related specs, such as width, height, and the margins of the canvas. The content of this key will be linked in a variable called "basicSettings". 42 | ```python 43 | "basic":{ 44 | #stores settings related to the canvas 45 | "width":10,#inches 46 | "height":10,#inches 47 | "toolsCt": 3,#how many colors 48 | "margins":{"l":2,"r":2,"t":2,"b":2},#in inches 49 | "unit": "inch", # support px, inch and mm. 50 | "displayInnerRect": False, #adding a boundary rectangle to the svg file (will not append to the python files) 51 | "displayOuterRect": False, 52 | "plotterDefaultSetting":{ 53 | "model":2 #according to https://axidraw.com/doc/py_api/#model 54 | } 55 | }, 56 | ``` 57 | 58 | The "spec" key stores everything that user want to define. The content of this key will be linked in a variable called "currentSpec". Users can add additional settings for their project but the following settings are required. 59 | ```python 60 | "spec":{ 61 | #stores any user-defined specs 62 | "segmentLength":0.04,#inches 63 | "trailStitchLength":0.15, 64 | "trailLoopDepthPerc":35, 65 | "plotterSettingRange":{ 66 | "speedPercRange":[20,80], 67 | "depthPercRange":[35,100],#The range(%) that the z axis can move. 100% corresponds to the longest stitch whereas 55% corresponds to the shortest stitch 68 | "distanceRange":[0.03,0.15] #inches 69 | } 70 | } 71 | ``` 72 | 73 | When you are making your first embroidery, you might want to play with these settings to get an optimal spec for your plotter and fabrication goal. 74 | 75 | With the basic settings done, you can write your patterns. The most basic example is to build a boundary test -- a "boring" rectangle that goes around the edge of your frame. 76 | To do that, you need four steps: 77 | 78 | 1. Initiate a threadPlotter instance with the settings object. It will generate a "virtual" canvas for you with x randomly selected thread colors (x is controlled by "toolsCt" in your settings). It will also perform the canvas size calculations for you and store the converted dimensions (in px) into a variable called "wh_m" (short for width_height_within_margin). 79 | 80 | 2. Generate a path list: a list of points. e.g.[[0,0],[100,100]] is a line. The helper script shapeEditing.py contains several functions for generating basic shapes such as polygons and rectangles. 81 | 82 | 3. Store the path list into the ThreadPlotter instance using the "initPunchGroup" function. It requires a thread number (if you have 3 thread, your index can be 0, 1, or 2), and a path list. 83 | 84 | 4. When you finish appending all the path lists, us ".saveFiles()" to export the result. 85 | 86 | Here's the script. You can also check the [full script here](../projects/tp00_boundaryTester/tp00_boundaryTester.py). 87 | 88 | ```python 89 | from threadPlotter.Utils import shapeEditing as SHAPE 90 | 91 | from threadPlotter.ThreadPlotter import ThreadPlotter as TP 92 | 93 | settings={...} 94 | 95 | testPlotter=TP(settings) #create an instance 96 | boundaryRect = SHAPE.makeRectPoints( 97 | 0, 98 | 0, 99 | testPlotter.wh_m[0], 100 | testPlotter.wh_m[1], 101 | closed=True 102 | ) #making points for a rectangle. 103 | # wh_m is a list that stores the width and height within 104 | # the plotable area (exclude margin). 105 | # wh_m[0] is the width, and wh_m[1] is the height. 106 | # The value stored is in pixels. 107 | # If you use inch or mm, threadPlotter will convert your settings into px. 108 | 109 | testPlotter.initPunchGroup(0, boundaryRect) 110 | #pattern information are going to be stored as 111 | # PunchGroup instances. The initPunchGroup function takes 112 | # an id of the thread, and a list of (unprocessed) points 113 | 114 | 115 | testPlotter.saveFiles() 116 | #ThreadPlotter will process the path you provided 117 | # by segmenting it and connecting multiple punch groups. 118 | # Then, it will export svg and python to your selected directory. 119 | 120 | ``` 121 | In the folder you selected, you should see something like this: 122 | 123 | ![export](../assets/exportSample.png "export") 124 | 125 | 126 | 127 | ## Thread Color 128 | By default, the ThreadPlotter class is picking random thread colors for you according to the value of "toolsCt" that you have specified in the basic setting. This is achieved through the function __ThreadColor.pickRandomThreadColors()__. 129 | 130 | Alternatively, you can produce your own color (in rgb tuples) and try to math to the thread 131 | 132 | Let's look at an example where we fabricate a circle with 3 colors. The full code is available at [here](../projects/tp01_circleTester/tp01_circleTester.py). 133 | 134 | ```python 135 | 136 | from threadPlotter.Utils import shapeEditing as SHAPE 137 | from threadPlotter.ThreadPlotter import ThreadPlotter as TP 138 | import random 139 | settings={...}#the setting is the same as the previous example except for the name. 140 | testPlotter=TP(settings) #create an instance 141 | #construct a list of random colors 142 | colorList=[] 143 | for i in range(3): 144 | rgb=[random.randint(0,255) for j in range(3)] 145 | colorList.append(rgb) 146 | 147 | testPlotter.matchColor(colorList) 148 | 149 | maxSize=min(testPlotter.wh_m)/2 #largest circle radius 150 | minSize=10 #smallest circle radius 151 | gap=8 #gap between each circle 152 | circleCt=int((maxSize-minSize)/gap)# how many circles we are going to draw 153 | 154 | for i in range(circleCt): 155 | r=minSize+i*gap 156 | circle=SHAPE.makeUniformPolygon(testPlotter.wh_m[0]/2,testPlotter.wh_m[1]/2,r,50,closed=True) #approximate a circle with a 50 side polygon 157 | colorId=testPlotter.getRandomToolId() 158 | testPlotter.initPunchGroup(colorId, circle) #get a random color and store it 159 | 160 | testPlotter.saveFiles()#export 161 | 162 | ``` 163 | In this example, we randomly generated 3 color (in rgb) and tried to pick thread colors that are closest to the given color. The color result is stored in the _tool.svg file. We have an experimental feature of supporting thread color mixing. Nevertheless, real-world color works dramatically different than RGB computation. If you wish to avoid thread color mixing, you can use turn it off using the additional parameter "allowMix". 164 | 165 | ```python 166 | testPlotter.matchColor(colorList,allowMix=False) 167 | ``` 168 | 169 | ### editing the color database 170 | We process thread colors stored in the file "PunchNeedle/embroidery_thread_color.csv" and utilize that information in the thread color matching process. If you wish to change that database, you can directly edit that file. 171 | 172 | After editing, run the python file "updateColor" and it will update the thread-related files for you. 173 | 174 | 175 | ## Converting an image 176 | 177 | We developed another class called "GridImgConverter" to help you to convert a raster image into an embroidery patter. For example, we want to convert the following image into a patter: 178 | 179 | ![alt text](https://upload.wikimedia.org/wikipedia/commons/8/86/Apple-tree_blossoms_2017_G3.jpg "Apple-tree blossoms 2017 G3.jpg") 180 | 181 | The sample image is created by George Chernilevsky and released into the public domain. You can see the original image on [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Apple-tree_blossoms_2017_G3.jpg). 182 | 183 | The first step is to ensure the aspect ratio is the same with your final pattern 184 | 's aspect ratio. If your final pattern is 1:1, you might want to crop the image so that the width-height ratio is 1:1. You can also reduce the quality of your original image to speed up the process. 185 | 186 | ![edited-img](../projects/tp02_convertImg/1200px-Apple-tree_blossoms_2017_G3.jpg "Apple-tree blossoms 2017 G3.jpg") 187 | 188 | 189 | 190 | You can initiate a ThreadPlotter instance just like the previous examples. [Full Code Here](../projects/tp02_convertImg/tp02_convertImg.py) 191 | 192 | ```python 193 | 194 | import ... 195 | from threadPlotter.PunchNeedle.GridImgConverter import GridImgConverter 196 | settings={...} 197 | testPlotter=TP(settings) #create an instance 198 | #construct a list of random colors 199 | imageAddr="1200px-Apple-tree_blossoms_2017_G3.jpg" 200 | 201 | testPlotter.saveFiles()#export 202 | 203 | ``` 204 | 205 | -------------------------------------------------------------------------------- /tutorial/step4_advancedExamples.md: -------------------------------------------------------------------------------- 1 | # Advanced Examples 2 | todo 3 | ## Converting an image into a 3D pattern 4 | ## Adjusting height dynamically 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------