├── .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 | [](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 | [](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 |
53 |
--------------------------------------------------------------------------------
/assets/thread feeder-04.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | [](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 | [](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("" + rep + ">"))
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("" + rep + ">"))
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------