From cfe552387fa0e49a8f706c948517648fb357b3f3 Mon Sep 17 00:00:00 2001 From: bMorgan01 Date: Mon, 4 Oct 2021 13:31:43 -0600 Subject: [PATCH] 1.0 commit --- .gitignore | 3 + icon.bmp | Bin 0 -> 262282 bytes icon.ico | Bin 0 -> 2780 bytes icon.png | Bin 0 -> 1993 bytes icon.xcf | Bin 0 -> 10934 bytes icon_big.png | Bin 0 -> 2174 bytes main.py | 508 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 511 insertions(+) create mode 100644 icon.bmp create mode 100644 icon.ico create mode 100644 icon.png create mode 100644 icon.xcf create mode 100644 icon_big.png create mode 100644 main.py diff --git a/.gitignore b/.gitignore index c0f50ae..df1f9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # Bundle Script *.bat + +# Pycharm +.idea/ diff --git a/icon.bmp b/icon.bmp new file mode 100644 index 0000000000000000000000000000000000000000..9bfbb3b244cb29f1ef855872f1d027e184722f7b GIT binary patch literal 262282 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 G1pW_TJ}hhi literal 0 HcmV?d00001 diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c2df64ac2f639ea08b8350f08170e2510e9ba08f GIT binary patch literal 2780 zcmYjS3pkWnA3yJS@s`2+%4I~1K`4z&7c6OJj2Mb8YFNcEOC`6`GTDrAANOG^Y!}tq zvJb0~mD{dH6dOfKyVYn0-CRb43L!h~e&6$b|MQ&Z|2zNR`QOfSo^t?z6yu9PfC%oc z1VC4Tvv>J;sIOeDSOIm0CwJ2KhAmCpm%}TkB~WJgSmB7 zTTFm#bD{Fb{=M-^-o$_zXCxT(r`h>05OZG#)$r+xSre%tjd)Vlr31^(rREW(aX+J+A4 za}T6|oe~}@UowG5{7dx-TnW+^X59tjxrTHpNp}ROY$Mb9SlD*k#VHKJ#jZ#z_kE34 z3{IKglY;_G1;?*t*)DbwQcgIFJEcFa4}|x~B8}S%P0C1uaE}E7*Ra?eXBB^=aHvyT zpM-K%6{a3+EADbajl!f=WQxXjej;$slOkZ+q@TiZGd)i<@XP>Qx)|#|J~C*0G$&pM zjX0#(a~ie6g~UJvuHwl5BNSSQ(c!nl`rx$oDD{%9bARYuZH6U=ESM2_BQ(G2J1yI8 zY7ea=_UM3i%^tnAK=g$4+h{sMNmqg%qa;At@IrFRsOop<+B6q<1x!OaN6QKqeC?c$ z82sc~6rOZ#>eDJ{mIaZT|z&PMEN<7=)B zzPRw=p^9l?eq&D<;55``BXcA6pp0X%F6Md5mY5hH7*ur|LB0qt1h3^~3M^AO^HMT+ zFxiL-LXJkJw-2U z^!>jkK;W@iX+}vWqv3Y@M#qen@3R2j6!yP8$Z!J%rYfcPUS-qZ*Cq&IWV(Y0u9+Z* zn_MtN)Qr%A$eJ#YWufIu=`?W_PllzJijDk8yY#61HW}3n+(hAWvC$?R%YZ6Z7)YQM zh0h%@)+R~ysKT}nR3flL(-OrtE*L+H7}>c*TgCf}PYsyVD*-R5`2STXtwgsmUj^_^ zan`cdnCKRPm)r4Y)#_sDAf44}tF|5Vx#O>{sQ09Uq;#n;HrEZdO-J!+4VEy-LQ`{T zWED(gmwpM0$N1oCmr-g}y#uJUS}<^b`zdT&W2wc+0A+5?T3qS+y~v&kOH6bifFuaD zFemZm?diltjd%>Q#+=}ggvg%yy1HlQK-PnVmS^X7(VKxHWj8&Ka_?V|OL_J$$f1mZ zUlHhajuwOydcd~KbOhyTka$hMc@Pf*jxJEke~|MAsZit5VWEE`Qi1;mR3a7X|A11Y zLW7cvvQpZfngAa~dZ{Z8zLbWrpg%DpP(q;a=1m?j5zu)02e$KKlwFC_dQ{f8WQnz7 zo5gShSaq<}YMcqdthF?rDMC?62da(P0CL1kMEvFOTwq2)`bLJ=@j!JRdZ~SJAAl_R zgbEL^OGCnXnR`c3ECor8g26cwauWr99@=9!bRnF;kouP{#5)3Cw#ESmK2`rLN$xLG z@8VHY_rWX!7xZ+=XjSAR13k=F^(y<*xm1K=o=()MyG5V_JW*u`13NrWn!P@RV;R~Z zX+XNEL$nzucvyP~_&8xms`KksW8NE3AY4R!m*454aF32FIIGHuf}yykMrFQhM_`bK zR>oM3rx=%)<2q4)>rv+d;;4&qTNT zf@JdOcZ}JqpH-_3W1^whVrjX)Sn&RAg@bvLhaB=rpYz7EK7lE`ZO_#%gh z$@>z?tyaMakByVP-gWkiT1@cT|0|~_H>hxpcVbZgcya6gnIntA#h;7XY`&>Kk+)NY zvQj67ZE@)j$ts)6!yDSxFdPOthMXA<=1Y~Ym40`Qn4C>6d7o9W5aIfAAuz*LNq8b} zh3ME%zRxy6f}SL@t$Pv2-|2xqj%1 z7F5-6WO?JPiL^`V;Tp1It3knl3wd;O=_o&&&8`2ag6C|Kd)?`i`C>~FJIYpFC4fPZ zY5zW0SAvzhx{_kUVNvG8UZ*SDiDTT8Y%&lV1nEZkUy&6adX3|Sq-14EnRH;7jiw+A z=n6>4?w`@PsMuv^s|Fc*w4MPq&tn-@PJes&f*$Vh-PK z^;ZK-5U;az1ttG!vEbLQRvffX5jkNj`av@SBBte&%a1>Q5PvR@`FXwtCmUb2nTaD~ z;Q3s$?ROW&w=T*STY}A!s&hl;cKmLmQXfBWoUGAjpL|}I&pnX5cV-q|&b>V! zJ-()sa01iZWvjVpDS2@sxY)BOj*X zYrx`^3*}?F_;qiNi$-y0lt!;Ruh-(Xol@tKFGey!%KITL%b! zsQOxcXdT$k`EIrZ2UAG6%H9#Vw-^V0ZmNEQ*5G#K{+W<$wF{V$c(Rj5-0VQWvU}HV zquhcMG_DkFA>nUn%U2Tbr9d~)c>hK%idE5zZ*EU?p(I|t`)aNg=ZoV9x&hZpdI(Tf zU|GRlvC70_I=n~pHC!vk7dDKfdF!+#(85G64;8d7$VQ}%_%w-E-H%D>z|CI$)@jEt ztozf^^Um`TA zaoVFQdRaOsoY7S2qWr1u{A%a_P- L_o3I&SgC&lY#xem literal 0 HcmV?d00001 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..be07b12d9f5fd4961afe7c4314e83338d0d2d7aa GIT binary patch literal 1993 zcmV;)2R8VLP)EX>4Tx04R}tkv&MmKpe$iQ?*j64t7v+$WWauh>AE$6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;o12rOi`@MG$^;p%;S^GxbDzAp_6xbq^n3@1i`*``n+SN6DKE@QK8;OgAjzb>itw zOXs{#9A+g+AwDM_Gw6cEk6f2se&bwpSm2pqBa@mV4ik&THkR9%l?;`5ia4UE8s!UF zmle)ioYiubHSft^7|LtQX|B^8MjVSsAPEsNs@Omo7NWFjq?kz2e$2x^_CX>@2HM@dakSAh-}000G`NkljM5gdMXk1rh9YU3QCuiZZPgfTLmIUz zUt`irrqpCIF76i@hdl24-bMd#8HRgj&OQJ8J^trA-^~#t9AoAH%YoIvd|-*5S^~_Q z=4S(Xi~uKq?|@UlfE6i6eUG^qCfx))3T)KL&xzn~%Sy@t;BDZH6{%#30G;^jz{5a8 zjD}SPx`CIi$dS|uaEz$~o&sJ38c7aiUm3Sx8t$C!FxFK~4R=d<04 z?2p?8o=-XI&x8ezF$b;4cq{}s#xww1L)_pv@F8$Sl^rUHmjdhcGfZl30UiQ&#bp6^ z19O3~05>-Rtg|BDPI2xt$Cx(Y4&b91)(Kv)!4CmBpB7LDU|`1CBEaT+oGCv;;WD5Dw?dwN#MDF z<1_$Q0W0IMfO=JcL2%LM`h5@3AHofmWGx1)aExj3da(wv10lw{F|IWDPl%h^?HKcb z74fRok7%9Y0`PrJkSx{&%M+SrCvi4%0<6fGW6b=JmN4Pt+JivBZ(8$+^W zuLKla@#IVuD1$&#NDJ5t><7LChN3ENCL$UfW4ctXMuBnMa`!RZ(sz-VO5ahsKzD=$ z)T^#os^81NfMZNiNlAndzZTE}`~=h`@Ng#g0uE|MIAle}(zbvzxOKpM3huhFCLN|) z5z3JcCZcwUAOy4nkEKMwpvvT3R-`{I0-Au{km)dr46+A!)ry=>lmI1R6R`#ps=GWGkc=|! z-awzOR(}RMtcYZufGNn>s+yv1ngI6;ZguLEPPA8#^T4>DO0(axc&jApX?~Opi58>|jeXc@guUYaD_a=!7Zns&h7sVHALBuhpSqm5&iT~}<|6ZL1j)rZs zi?kx73T^{%ua>wPBN}6}rS5<#(*Df7JUvaITnyX00000NkvXXu0mjfW7LQL literal 0 HcmV?d00001 diff --git a/icon.xcf b/icon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..1ab5b27244a5ebbc15139e82670a015e0c939bfe GIT binary patch literal 10934 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 KktpzqNao+F27@&K literal 0 HcmV?d00001 diff --git a/icon_big.png b/icon_big.png new file mode 100644 index 0000000000000000000000000000000000000000..f898dfc6b4fac4703585c135616947e2809620a7 GIT binary patch literal 2174 zcmYjS3s4hT7QLNsOQLiXNgM+N2o{S9QE>=@5FmUa4zQN^89}l-itZo=L=Z&i1O=2) zgeX|ZC#bcsjvoYFU|>iT5D|45*%A;V;iIe~5by(T_-tpjtnI4q+xOn{&U@#+ce_u9 z1o_W5`OX9Y=1T&^n*b2v6#jN4^ z+~$amQ=1%2;<1?jSTD+ARkiBE2SzkQ84cY52kbv44_w>k$@ zVa1D%4m#zysedllJBwp_#xOqma$wYg=j^a?EqM`sb-SN~<)W(E&|8MZUX=E1^ge&@ z+mj!5(TWF#Ej=1u`X=U0)_VHi%yn6!TXb^hLiDz#rA=!t<~>O)5kA_G>zy3r9^Fno zX;tf3H+{eM-xZoQjeg7426LC0XIZRB5G-HfFzy^9{`1<0>`KSwh1J4e&wux`R;d=8 z>)i4*tjzTnVcuEMLYtrd72-!VzjXVrS1ttnk@Bd~b8t#InA;jkvuz<=4Y77j(R)s5 zuW!Fl5U2EBr5bMjho>Jt8m`)2Ju&DF)&FY_S$^p`G__!nj6YV@4LCulvVXkVOLSU)1*gw{}&{|T8!BQh%X2< z5;O)^U^!dl=8cmH=0uYs2TTv#Mz}|m_F5i-Fm1L_+rIGUC$!vQKqifA{dz&4YmBfAG zq7`H?3HC57&|G*DoA&T%ccdNsP9B>nnll`vYl7G>$5YjuE<|W>8$j=h+yJTQs-J6S z>HmcLZb^8y8wcm&W3PoWn=jJ>2u!n%WnLcfQq_9O25#y0aLa}g&AKjK4VzjageTwI z;1+o4#QSxw^T38s%EzE7OHS6&c+bi$ARa-12VGPi;J|F0A54LKq<H< zYs@s?S6!z+dX%K!q(^kY=P<3_1aFgDvw!QOeqDpmEt}+2m=TG=aX>^&jyu@!h!O%y zBH=t9WExEf1}uz15dj53L>LAXc`;-fvm_qvHcdn!!WgU)0Y?5piYNIb-*~$n_b8r+ z8A&N33PvIzO)3{c2zHMC)jzIU`^{GHb;)DghU?wA6`BI znc)3((Prg_FEQ|!+WYLI5) za9FE^BodEg<}ZTdct4>M;}MzD-!c>wyhXc-F-)1$*O`EW(-%9+>58PsGk@VB7@0z_ ztRh%wC@8*Q30;Ipto+pf6)lwD^uj0z@c=CCD*DciTNqbra&InnP3y44<2G3{D%}{Y zOul_A{D)ZERxRJOv%|SQFme5Fw>(;=Y&Zyu9LwZs8yCm*VV%K|f z-BW+#f29r(-KJVgKPX%*0@)HAfTZ&P6l&hFh@3r&FZGeq0ma)OUC(dz`aWX8>ZJ`! z(7SF{!Ah>3tNW*Q4_}V~cUkS)7SDy1mBQD5f93H&ab{dSe`?%XUE8Gg+(#D-+?B0~ zGj9l(_%XLy8MY(1VY9YG)7zs?L8VoFl>Sg^z+)sg9vr_b=( zYJL(G-eC6+MKuKLOYDhqYu3ZtG5R_2Of%oJO<8@rwilf;VEpTL_ zioHV&$(9Y%UaO}akrk?^!ldHe4J{AsaINOVY}CC2blt{H@qddc%&YQw&()~vQ`aI^P}0Y!7~!)*S7IUBN+WdlPkWe~D%I!lXmQKeHOfIqXq!--E-Jcx!D zXdEGO&e$pGy>`(48@Ym^=~fdhjrS$YgLL&_x|EIgIrSn_26;$5xnhw<;z#sNW3MH! zH2Qok85)U+2?*QhGjLQoMHNv5Q^t;BstMYUjS*JBQXSS)MQG++f*8aa9Rs`x7_bCW zBpl-;BO;X906ZEk0x*(~6J*Lu+0bN!DWp0ocnS0*+@2GUh`7u;;;1sxeh*7Uu$PU` To9!JDW7kQ1gTyzyrHcOou69Zz literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..9ed6481 --- /dev/null +++ b/main.py @@ -0,0 +1,508 @@ +import os +import sys +from copy import copy +from math import sqrt, ceil, floor +import numpy as np +import tkinter as tk +from tkinter import filedialog, LEFT, X +from rectpack import newPacker, float2dec, PackingMode, PackingBin, SORT_NONE +from ctypes import windll + +GWL_EXSTYLE = -20 +WS_EX_APPWINDOW = 0x00040000 +WS_EX_TOOLWINDOW = 0x00000080 + +import trimesh +from trimesh.path.entities import Text +from trimesh.path.exchange.export import export_path +from trimesh.path.creation import rectangle +from trimesh.transformations import scale_matrix + +# declare "globals" for use across functions +mesh = None +unit = None +sections = None +pdfSections = None +combined = None +bins = None +blocked = False +saveBinsButton = None +showKeyButton = None + + +# shows dimensions and layer count in main gui window +# updates live when model has been selected +def calcDim(args=None): + scale = 1 + + # if scale is number set scale, else reset text entry + try: + scale = float(scaleFactorEntry.get()) + except ValueError: + scaleFactorEntry.config(text="1") + + # get and display scaled bounds + dimensions = mesh.bounds + + dimensionsAfterLabel.config(text="Scaled Dimensions (" + unit + ")") + xAfterlabel.config(text="x: " + str(round((dimensions[1][0] - dimensions[0][0]) * scale, 2))) + yAfterlabel.config(text="y: " + str(round((dimensions[1][1] - dimensions[0][1]) * scale, 2))) + zAfterlabel.config(text="z: " + str(round((dimensions[1][2] - dimensions[0][2]) * scale, 2))) + + # get and display number of layers + try: + thickness = float(layerThicknessEntry.get()) + if thickness == 0: + numLayersLabel.config(text="Layers: N/A") + else: + numLayersLabel.config( + text="Layers: " + str(ceil(((dimensions[1][2] - dimensions[0][2]) * scale) / thickness))) + except ValueError: + numLayersLabel.config(text="Layers: N/A") + + +# opens 3D model and fills gui elements with appropriate info (base unit, model name, model dimensions, etc) +def getFile(): + # maps base unit result to abbreviation + unitDict = { + "millimeters": "mm", + "inches": "in" + } + + # ask for file + filename = tk.filedialog.askopenfilename(initialdir="/", + title="Select a File", + filetypes=((".stl files", "*.stl"), ("all files", ".*"))) + + # add model file path to gui + modelEntry.config(text=filename) + + # load the mesh from filename + global mesh, unit + mesh = trimesh.load_mesh(filename) + + # get base unit, add to gui + unit = trimesh.units.units_from_metadata(mesh, guess=True) + try: + unit = unitDict[unit] + except KeyError: + pass + + layerThicknessLabel.config(text="Layer Thickness (" + unit + "): ") + + # add model dimensions to gui + calcDim() + + # now that model is open, listen for changes in relevant text entries to update dimensions shown in gui + scaleFactorEntry.bind('', calcDim) + layerThicknessEntry.bind('', calcDim) + + +def focus_results(): + # bring results window forward + if results is not None and 'normal' == results.state(): + results.focus_set() + return + + +# run slices, show export window +def go(): + global combined, sections, results, saveBinsButton, showKeyButton, showKey + + focus_results() + + # scale model according to text entry in main window + mesh.apply_transform(scale_matrix(float(scaleFactorEntry.get()), [0, 0, 0])) + + # slice the mesh into evenly spaced chunks along z + # this takes the (2,3) bounding box and slices it into [minz, maxz] + z_extents = mesh.bounds[:, 2] + # slice every x (text entry) model units (eg, inches) + z_levels = np.arange(*z_extents, step=float(layerThicknessEntry.get())) + + # create cross section outlines + sections = mesh.section_multiplane(plane_origin=mesh.bounds[0], + plane_normal=[0, 0, 1], + heights=z_levels) + + # in the case that slice did not intersect with model, remove section + sections = list(filter(lambda element: element is not None, sections)) + + combined = np.sum(sections) + + # GUI stuff + results = tk.Toplevel(root) + results.title("Results") + results.protocol("WM_DELETE_WINDOW", clearResults) + + buttonFrame = tk.Frame(results) + viewFrame = tk.Frame(buttonFrame) + sectionsButton = tk.Button(viewFrame, text="View Layers", command=plot) + modelButton = tk.Button(viewFrame, text="View Model", command=model) + exportButton = tk.Button(buttonFrame, text="Export Layers to SVG", command=export) + + heightFrame = tk.Frame(results) + exportHeight = tk.Label(heightFrame, text="Height (" + unit + "): ") + exportHeightEntry = tk.Entry(heightFrame, width=10) + + widthFrame = tk.Frame(results) + exportWidth = tk.Label(widthFrame, text="Width (" + unit + "): ") + exportWidthEntry = tk.Entry(widthFrame, width=10) + + kerfFrame = tk.Frame(results) + exportKerf = tk.Label(kerfFrame, text="Kerf (" + unit + "): ") + exportKerfEntry = tk.Entry(kerfFrame, width=10) + + exportFileButton = tk.Button(results, text="Prepare Cuts", command=( + lambda *args: exportFile(float(exportHeightEntry.get()), float(exportWidthEntry.get()), + float(exportKerfEntry.get())))) + + exportFrame = tk.Frame(results) + saveBinsButton = tk.Button(exportFrame, text="Export Cut Files", command=saveFiles) + showKeyButton = tk.Checkbutton(exportFrame, text='Key?', variable=showKey, onvalue=True, offvalue=False) + + buttonFrame.pack(pady=10, padx=5) + viewFrame.pack(pady=5) + sectionsButton.pack(side=LEFT, padx=1) + modelButton.pack(side=LEFT, padx=1) + exportButton.pack() + + heightFrame.pack() + exportHeight.pack(side=LEFT) + exportHeightEntry.pack(side=LEFT) + widthFrame.pack() + exportWidth.pack(side=LEFT) + exportWidthEntry.pack(side=LEFT) + kerfFrame.pack() + exportKerf.pack(side=LEFT) + exportKerfEntry.pack(side=LEFT) + + exportFileButton.pack(padx=1) + exportFrame.pack(pady=5, padx=5) + + results.update() + results.geometry(f'+{root.winfo_rootx()+int((root.winfo_width()-results.winfo_width())/2)}+{root.winfo_rooty()+int((root.winfo_height()-results.winfo_height())/2)}') + results.mainloop() + + +# empty results, hide window so process can be run again +def clearResults(): + global results + + results.destroy() + results = None + + +# show top-down view of stacked layers +def plot(): + global blocked + + if not blocked: + blocked = True + # summing the array of Path2D objects will put all of the curves + # into one Path2D object, which can be plotted easily + combined.show() + + blocked = False + + +# shows unsliced model +def model(): + global blocked + + if not blocked: + blocked = True + + scene = mesh.scene() + camera = scene.camera + camera.resolution = (640, 520) + scene.base_frame = "Model" + scene.camera = camera + scene.show() + + blocked = False + + +# save layer svg's to selected directory +def export(): + global blocked + + if not blocked: + blocked = True + + # ask for directory + directory = filedialog.askdirectory() + + for i in range(len(sections)): + export_path(sections[i], file_type="svg", file_obj=directory + "/layer" + str(i) + ".svg") + + focus_results() + + blocked = False + + +# prepares bins for export, performs bin packing +# arguments: bin height, bin width, kerf +def exportFile(h, w, k): + global blocked, bins, pdfSections + + # if other window is not open + if not blocked: + blocked = True + + # prepare packers, one with rotation enabled, one disabled, results will be compared later + packer = newPacker(mode=PackingMode.Offline, bin_algo=PackingBin.Global, sort_algo=SORT_NONE, rotation=False) + rotPacker = newPacker(mode=PackingMode.Offline, bin_algo=PackingBin.Global, sort_algo=SORT_NONE, rotation=True) + + # add rectangles to packers + for i in range(len(sections)): + dimensions = sections[i].bounds + + sections[i].apply_translation((-dimensions[0][0], -dimensions[0][1])) + + dimensions = sections[i].bounds + + # set dimensions to bounds plus kerf + r = (float2dec((dimensions[1][0]) + k, 5), float2dec((dimensions[1][1]) + k, 5)) + packer.add_rect(*r, i) + rotPacker.add_rect(*r, i) + + # add bins to packers + packer.add_bin(float2dec(w, 5), float2dec(h, 5), count=float("inf")) + rotPacker.add_bin(float2dec(w, 5), float2dec(h, 5), count=float("inf")) + + # run pack + packer.pack() + rotPacker.pack() + + # init var to largest bin area of packers, will be overridden to find minimum area used, minimizing material wastage + leastBinSum = w * h * max(len(packer), len(rotPacker)) + # loop through packers + for pack in (packer, rotPacker): + binSum = 0 + # loop through packed bins + for i in range(len(pack)): + leastX = w + leastY = h + greatestX = 0 + greatestY = 0 + # loop through packed rectangles + for rect in pack.rect_list(): + # if rectangles is in bin + if rect[0] == i: + # check to see if rectangle is closer to the top corner than the previous closest + if rect[1] < leastX: + leastX = rect[1] + if rect[2] < leastY: + leastY = rect[2] + # check to see if rectangle is closer to the bottom corner than the previous closest + if rect[3] + rect[1] > greatestX: + greatestX = rect[3] + rect[1] + if rect[4] + rect[2] > greatestY: + greatestY = rect[4] + rect[2] + # add used area of bin to total + binSum += ((greatestX - leastX) * (greatestY - leastY)) + # override packer least used area if needed + if binSum < leastBinSum: + leastBinSum = binSum + leastPacker = pack + + # use algo that resulted in the least wastage + packer = leastPacker + bins = list() + displaySections = list() + pdfSections = list() + # create visual representation of bins for display + for i in range(len(packer)): + bins.append(list()) + + rowColLen = ceil(sqrt(len(packer))) + row = floor(i / rowColLen) + col = i % rowColLen + + displayBin = rectangle(((0, 0), (w, h))) + displayBin.apply_translation( + ((packer[i].width * 11 / 10) * col, -((packer[i].height * 11 / 10) * row + packer[i].height))) + + displaySections.append(displayBin) + pdfSections.append(displayBin) + + # add rectangles to their designated bins + for rect in packer.rect_list(): + section = sections[rect[5]] + bin = rect[0] + + # find position of bin for display purposes (bins displayed in grid, find row & col) + rowColLen = ceil(sqrt(len(packer))) + row = floor((bin) / rowColLen) + col = bin % rowColLen + + # check if rectangle has rotated, if so, rotate + dimensions = section.bounds + if rect[3] != float2dec((dimensions[1][0]) + k, 5) and rect[4] != float2dec((dimensions[1][1]) + k, 5): + section.apply_transform(((0, -1, 0), (1, 0, 0), (0, 0, 1))) + dimensions = section.bounds + section.apply_translation((-dimensions[0][0], -dimensions[0][1])) + + # find x, y translation to move rectangle to designated spot in bin + section.apply_translation((rect[1] + float2dec(k / 2, 5), rect[2] + float2dec(k / 2, 5))) + bins[bin].append(section) + + # find x, y translation to offset display rectangle to the proper bin in the bin grid + displaySection = copy(section) + displaySection.apply_translation( + ((packer[bin].width * 11 / 10) * col, -((packer[bin].height * 11 / 10) * row + packer[bin].height))) + displaySections.append(displaySection) + + # for display only, add rectangle bounding box, for visualization of packing performance + displayRect = rectangle(((displaySection.bounds[0][0], displaySection.bounds[0][1]), + (displaySection.bounds[1][0], displaySection.bounds[1][1]))) + displaySections.append(displayRect) + + # also add bounding box to key display, real shape outline not added for readability + pdfRect = copy(displayRect) + pdfRect.entities = np.append(pdfRect.entities, Text(0, str(rect[5] + 1), align=('left', 'bottom'))) + pdfSections.append(pdfRect) + + # show packed rectangles and bins in human understandable format + displayCombine = np.sum(displaySections) + displayCombine.show() + + # now that export has been prepared, show save buttons + saveBinsButton.pack(side=LEFT, padx=1) + showKeyButton.pack(side=LEFT) + + blocked = False + + +# save bins and show key +def saveFiles(): + global blocked + + if not blocked: + blocked = True + + # ask for directory selection + directory = filedialog.askdirectory() + + # export bins + for i in range(len(bins)): + exportCombine = np.sum(bins[i]) + export_path(exportCombine, file_type="svg", file_obj=directory + "/bin" + str(i) + ".svg") + + focus_results() + + # show key + if (showKey.get()): + pdfCombine = np.sum(pdfSections) + pdfCombine.show() + + blocked = False + + +# forces window to appear in taskbar, windows specific +def set_appwindow(root): + hwnd = windll.user32.GetParent(root.winfo_id()) + style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) + style = style & ~WS_EX_TOOLWINDOW + style = style | WS_EX_APPWINDOW + res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style) + # re-assert the new window style + root.wm_withdraw() + root.after(10, lambda: root.wm_deiconify()) + + +# for newer versions of pyinstaller, needed to retrieve icon image +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + + +results = None + +# create base window +root = tk.Tk() +root.title("3DSlicer") +root.option_add('*Font', '19') +root.attributes("-toolwindow", 1) +p1 = tk.PhotoImage(file=resource_path('icon.png')) +# Icon set for program window +root.iconphoto(True, p1) + +# clear children of base window +clearList = root.winfo_children() + +for child in clearList: + child.destroy() + +# set default state of key checkbox +showKey = tk.BooleanVar() + +# generate window +menubar = tk.Menu(root) +filemenu = tk.Menu(menubar, tearoff=0) + +# TODO: filemenu.add_command(label="About", command=about) +filemenu.add_command(label="Exit", command=root.destroy) +menubar.add_cascade(label="File", menu=filemenu) + +modelFrame = tk.Frame(root) +topModelFrame = tk.Frame(modelFrame) +modelLabel = tk.Label(topModelFrame, text="Model: ") +modelEntry = tk.Label(topModelFrame, text="N/A") +modelBrowse = tk.Button(modelFrame, text="Browse", command=getFile) + +entriesFrame = tk.Frame(root) +layerEntryFrame = tk.Frame(entriesFrame) +layerThicknessLabel = tk.Label(layerEntryFrame, text="Layer Thickness (N/A): ") +layerThicknessEntry = tk.Entry(layerEntryFrame, width=5) + +scaleEntryFrame = tk.Frame(entriesFrame) +scaleFactorLabel = tk.Label(scaleEntryFrame, text="Model Scale Factor: ") +scaleFactorEntry = tk.Entry(scaleEntryFrame, width=5) +scaleFactorEntry.insert(0, "1") + +numbersFrame = tk.Frame(root) +dimensionsAfterLabel = tk.Label(numbersFrame, text="Scaled Dimensions (N/A)") +xAfterlabel = tk.Label(numbersFrame, text="x: N/A") +yAfterlabel = tk.Label(numbersFrame, text="y: N/A") +zAfterlabel = tk.Label(numbersFrame, text="z: N/A") + +numLayersLabel = tk.Label(root, text="Layers: N/A") + +goButton = tk.Button(root, text="Go", command=go) + +root.config(menu=menubar) + +modelFrame.pack(pady=10) +topModelFrame.pack() +modelLabel.pack(side=LEFT) +modelEntry.pack(side=LEFT) +modelBrowse.pack() + +entriesFrame.pack() +numbersFrame.pack(pady=10) +layerEntryFrame.pack() +scaleEntryFrame.pack(fill=X) +scaleFactorLabel.pack(side=LEFT) +scaleFactorEntry.pack(side=LEFT) +layerThicknessLabel.pack(side=LEFT) +layerThicknessEntry.pack(side=LEFT) + +dimensionsAfterLabel.pack() +xAfterlabel.pack() +yAfterlabel.pack() +zAfterlabel.pack() + +numLayersLabel.pack() + +goButton.pack(ipadx=10, pady=5) + +root.after(10, lambda: set_appwindow(root)) +root.mainloop()