1.0 commit
This commit is contained in:
parent
603745072a
commit
cfe552387f
7 changed files with 511 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -130,3 +130,6 @@ dmypy.json
|
|||
|
||||
# Bundle Script
|
||||
*.bat
|
||||
|
||||
# Pycharm
|
||||
.idea/
|
||||
|
|
|
|||
BIN
icon.bmp
Normal file
BIN
icon.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
BIN
icon.ico
Normal file
BIN
icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
icon.png
Normal file
BIN
icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
icon.xcf
Normal file
BIN
icon.xcf
Normal file
Binary file not shown.
BIN
icon_big.png
Normal file
BIN
icon_big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
508
main.py
Normal file
508
main.py
Normal file
|
|
@ -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('<KeyRelease>', calcDim)
|
||||
layerThicknessEntry.bind('<KeyRelease>', 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue