mirror of
https://github.com/nophead/NopSCADlib.git
synced 2025-08-13 19:03:58 +02:00
Added source code
This commit is contained in:
80
scripts/blurb.py
Normal file
80
scripts/blurb.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Capture Markup lines in OpenSCAD source code denoted by '//!'.
|
||||
"""
|
||||
import re
|
||||
|
||||
def parse_line(line):
|
||||
""" process a line, add blurb to text and return true if got to a module or function """
|
||||
if line[:3] == '//!':
|
||||
line = line.replace('~\n', ' \n')
|
||||
start = 4 if line[3] == ' ' else 3
|
||||
return False, line[start :]
|
||||
else:
|
||||
words = line.split()
|
||||
return len(words) and (words[0] == "module" or words[0] == "function"), ""
|
||||
|
||||
def _scrape_blurb(lines):
|
||||
""" Find Markup lines before the first function or module given a list of lines."""
|
||||
text = ""
|
||||
for line in lines:
|
||||
b, t = parse_line(line)
|
||||
if b:
|
||||
break
|
||||
text += t
|
||||
if len(text):
|
||||
text += '\n'
|
||||
return text
|
||||
|
||||
def scrape_blurb(scad_file):
|
||||
""" Find Markup lines before the first function or module."""
|
||||
with open(scad_file, "rt") as file:
|
||||
lines = file.readlines()
|
||||
return _scrape_blurb(lines)
|
||||
|
||||
def scrape_module_blurb(lines):
|
||||
""" Find the Markup lines before the last function or module. """
|
||||
text = ""
|
||||
for line in lines:
|
||||
b, t = parse_line(line)
|
||||
text = "" if b else text + t
|
||||
return text
|
||||
|
||||
def scrape_code(scad_file):
|
||||
""" Find the Markup lines on the first line of functions and modules. """
|
||||
with open(scad_file, "rt") as file:
|
||||
lines = file.readlines()
|
||||
blurb = _scrape_blurb(lines)
|
||||
properties = {}
|
||||
functions = {}
|
||||
modules = {}
|
||||
for line in lines:
|
||||
match = re.match(r'^function (.*\(type\)|.*\(type ?= ?.*?\)) *= *type\[.*\].*?(?://! ?(.*))?$', line)
|
||||
if match:
|
||||
properties[match.group(1)] = match.group(2)
|
||||
else:
|
||||
match = re.match(r'^function (.*?\(.*?\)).*?(?://! ?(.*))$', line)
|
||||
if match:
|
||||
functions[match.group(1)] = match.group(2)
|
||||
match = re.match(r'^module (.*?\(.*?\)).*?(?://! ?(.*))$', line)
|
||||
if match:
|
||||
modules[match.group(1)] = match.group(2)
|
||||
|
||||
return { "blurb" : blurb, "properties" : properties, "functions" : functions, "modules": modules}
|
248
scripts/bom.py
Normal file
248
scripts/bom.py
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import openscad
|
||||
from time import *
|
||||
from set_config import *
|
||||
import json
|
||||
|
||||
def find_scad_file(mname):
|
||||
for filename in os.listdir(source_dir):
|
||||
if filename[-5:] == ".scad":
|
||||
#
|
||||
# look for module which makes the assembly
|
||||
#
|
||||
with open(source_dir + "/" + filename, "r") as f:
|
||||
for line in f.readlines():
|
||||
words = line.split()
|
||||
if len(words) and words[0] == "module":
|
||||
module = words[1].split('(')[0]
|
||||
if module == mname:
|
||||
return filename
|
||||
return None
|
||||
|
||||
class BOM:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.count = 1
|
||||
self.vitamins = {}
|
||||
self.printed = {}
|
||||
self.routed = {}
|
||||
self.assemblies = {}
|
||||
|
||||
def data(self, main):
|
||||
return {
|
||||
"name" : self.name,
|
||||
"count" : self.count,
|
||||
"assemblies" : [main.assemblies[ass].data(main) for ass in self.assemblies],
|
||||
"vitamins" : self.vitamins,
|
||||
"printed" : self.printed,
|
||||
"routed" : self.routed
|
||||
}
|
||||
|
||||
def flat_data(self):
|
||||
assemblies = {}
|
||||
for ass in self.assemblies:
|
||||
assemblies[ass] = self.assemblies[ass].count
|
||||
return {
|
||||
"assemblies" : assemblies,
|
||||
"vitamins" : self.vitamins,
|
||||
"printed" : self.printed,
|
||||
"routed" : self.routed
|
||||
}
|
||||
|
||||
def add_part(self, s):
|
||||
if s[-4:] == ".stl":
|
||||
parts = self.printed
|
||||
else:
|
||||
if s[-4:] == ".dxf":
|
||||
parts = self.routed
|
||||
else:
|
||||
parts = self.vitamins
|
||||
if s in parts:
|
||||
parts[s] += 1
|
||||
else:
|
||||
parts[s] = 1
|
||||
|
||||
def add_assembly(self, ass):
|
||||
if ass in self.assemblies:
|
||||
self.assemblies[ass].count += 1
|
||||
else:
|
||||
self.assemblies[ass] = BOM(ass)
|
||||
|
||||
def make_name(self, ass):
|
||||
if self.count == 1:
|
||||
return ass
|
||||
return ass.replace("assembly", "assemblies")
|
||||
|
||||
def print_bom(self, breakdown, file = None):
|
||||
if self.vitamins:
|
||||
print("Vitamins:", file=file)
|
||||
if breakdown:
|
||||
longest = 0
|
||||
for ass in self.assemblies:
|
||||
name = ass.replace("_assembly","")
|
||||
longest = max(longest, len(name))
|
||||
for i in range(longest):
|
||||
line = ""
|
||||
for ass in sorted(self.assemblies):
|
||||
name = ass.replace("_assembly","").replace("_"," ").capitalize()
|
||||
index = i - (longest - len(name))
|
||||
if index < 0:
|
||||
line += " "
|
||||
else:
|
||||
line += (" %s " % name[index])
|
||||
print(line[:-1], file=file)
|
||||
|
||||
for part in sorted(self.vitamins):
|
||||
if ': ' in part:
|
||||
part_no, description = part.split(': ')
|
||||
else:
|
||||
part_no, description = "", part
|
||||
if breakdown:
|
||||
for ass in sorted(self.assemblies):
|
||||
bom = self.assemblies[ass]
|
||||
if part in bom.vitamins:
|
||||
file.write("%2d|" % bom.vitamins[part])
|
||||
else:
|
||||
file.write(" |")
|
||||
print("%3d" % self.vitamins[part], description, file=file)
|
||||
|
||||
if self.printed:
|
||||
if self.vitamins:
|
||||
print(file=file)
|
||||
print("Printed:", file=file)
|
||||
for part in sorted(self.printed):
|
||||
if breakdown:
|
||||
for ass in sorted(self.assemblies):
|
||||
bom = self.assemblies[ass]
|
||||
if part in bom.printed:
|
||||
file.write("%2d|" % bom.printed[part])
|
||||
else:
|
||||
file.write(" |")
|
||||
print("%3d" % self.printed[part], part, file=file)
|
||||
|
||||
if self.routed:
|
||||
print(file=file)
|
||||
print("CNC cut:", file=file)
|
||||
for part in sorted(self.routed):
|
||||
if breakdown:
|
||||
for ass in sorted(self.assemblies):
|
||||
bom = self.assemblies[ass]
|
||||
if part in bom.routed:
|
||||
file.write("%2d|" % bom.routed[part])
|
||||
else:
|
||||
file.write(" |")
|
||||
print("%3d" % self.routed[part], part, file=file)
|
||||
|
||||
if self.assemblies:
|
||||
print(file=file)
|
||||
print("Assemblies:", file=file)
|
||||
for ass in sorted(self.assemblies):
|
||||
print("%3d %s" % (self.assemblies[ass].count, self.assemblies[ass].make_name(ass)), file=file)
|
||||
|
||||
def parse_bom(file = "openscad.log", name = None):
|
||||
main = BOM(name)
|
||||
stack = []
|
||||
|
||||
for line in open(file):
|
||||
pos = line.find('ECHO: "~')
|
||||
if pos > -1:
|
||||
s = line[pos + 8 : line.rfind('"')]
|
||||
if s[-1] == '{':
|
||||
ass = s[:-1]
|
||||
if stack:
|
||||
main.assemblies[stack[-1]].add_assembly(ass) #add to nested BOM
|
||||
stack.append(ass)
|
||||
main.add_assembly(ass) #add to flat BOM
|
||||
else:
|
||||
if s[0] == '}':
|
||||
if s[1:] != stack[-1]:
|
||||
raise Exception("Mismatched assembly " + s[1:] + str(stack))
|
||||
stack.pop()
|
||||
else:
|
||||
main.add_part(s)
|
||||
if stack:
|
||||
main.assemblies[stack[-1]].add_part(s)
|
||||
return main
|
||||
|
||||
def boms(target = None, assembly = None):
|
||||
bom_dir = set_config(target) + "bom"
|
||||
if assembly:
|
||||
bom_dir += "/accessories"
|
||||
if not os.path.isdir(bom_dir):
|
||||
os.makedirs(bom_dir)
|
||||
else:
|
||||
assembly = "main_assembly"
|
||||
if os.path.isdir(bom_dir):
|
||||
shutil.rmtree(bom_dir)
|
||||
sleep(0.1)
|
||||
os.makedirs(bom_dir)
|
||||
#
|
||||
# Find the scad file that makes the module
|
||||
#
|
||||
scad_file = find_scad_file(assembly)
|
||||
if not scad_file:
|
||||
raise Exception("can't find source for " + assembly)
|
||||
#
|
||||
# make a file to use the module
|
||||
#
|
||||
bom_maker_name = source_dir + "/bom.scad"
|
||||
with open(bom_maker_name, "w") as f:
|
||||
f.write("use <%s>\n" % scad_file)
|
||||
f.write("%s();\n" % assembly);
|
||||
#
|
||||
# Run openscad
|
||||
#
|
||||
openscad.run("-D","$bom=2","-D","$preview=true","-o", "openscad.echo", bom_maker_name)
|
||||
os.remove(bom_maker_name)
|
||||
print("Generating bom ...", end=" ")
|
||||
|
||||
main = parse_bom("openscad.echo", assembly)
|
||||
|
||||
if assembly == "main_assembly":
|
||||
main.print_bom(True, open(bom_dir + "/bom.txt","wt"))
|
||||
|
||||
for ass in sorted(main.assemblies):
|
||||
with open(bom_dir + "/" + ass + ".txt", "wt") as f:
|
||||
bom = main.assemblies[ass]
|
||||
print(bom.make_name(ass) + ":", file=f)
|
||||
bom.print_bom(False, f)
|
||||
|
||||
with open(bom_dir + "/bom.json", 'w') as outfile:
|
||||
json.dump(main.assemblies[assembly].data(main), outfile, indent = 4)
|
||||
|
||||
print("done")
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = len(sys.argv)
|
||||
if args > 1:
|
||||
if args > 2:
|
||||
boms(sys.argv[1], sys.argv[2])
|
||||
else:
|
||||
boms(sys.argv[1])
|
||||
else:
|
||||
boms();
|
108
scripts/c14n_stl.py
Normal file
108
scripts/c14n_stl.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
#
|
||||
# OpenSCAD produces randomly ordered STL files so source control like GIT can't tell if they have changed or not.
|
||||
# This scrip orders each triangle to start with the lowest vertex first (comparing x, then y, then z)
|
||||
# It then sorts the triangles to start with the one with the lowest vertices first (comparing first vertex, second, then third)
|
||||
# This has no effect on the model but makes the STL consistent. I.e. it makes a canonical form.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
def cmz(x):
|
||||
''' Convert "-0" to "0". '''
|
||||
return '0' if x == '-0' else x
|
||||
|
||||
class Vertex:
|
||||
def __init__(self, x, y, z):
|
||||
self.x, self.y, self.z = x, y, z
|
||||
self.key = (float(x), float(y), float(z))
|
||||
|
||||
class Normal:
|
||||
def __init__(self, dx, dy, dz):
|
||||
self.dx, self.dy, self.dz = dx, dy, dz
|
||||
|
||||
class Facet:
|
||||
def __init__(self, normal, v1, v2, v3):
|
||||
self.normal = normal
|
||||
if v1.key < v2.key:
|
||||
if v1.key < v3.key:
|
||||
self.vertices = (v1, v2, v3) #v1 is the smallest
|
||||
else:
|
||||
self.vertices = (v3, v1, v2) #v3 is the smallest
|
||||
else:
|
||||
if v2.key < v3.key:
|
||||
self.vertices = (v2, v3, v1) #v2 is the smallest
|
||||
else:
|
||||
self.vertices = (v3, v1, v2) #v3 is the smallest
|
||||
|
||||
def key(self):
|
||||
return (self.vertices[0].x, self.vertices[0].y, self.vertices[0].z,
|
||||
self.vertices[1].x, self.vertices[1].y, self.vertices[1].z,
|
||||
self.vertices[2].x, self.vertices[2].y, self.vertices[2].z)
|
||||
|
||||
class STL:
|
||||
def __init__(self, fname):
|
||||
self.facets = []
|
||||
|
||||
with open(fname) as f:
|
||||
words = [cmz(s.strip()) for s in f.read().split()]
|
||||
|
||||
if words[0] == 'solid' and words[1] == 'OpenSCAD_Model':
|
||||
i = 2
|
||||
while words[i] == 'facet':
|
||||
norm = Normal(words[i + 2], words[i + 3], words[i + 4])
|
||||
v1 = Vertex(words[i + 8], words[i + 9], words[i + 10])
|
||||
v2 = Vertex(words[i + 12], words[i + 13], words[i + 14])
|
||||
v3 = Vertex(words[i + 16], words[i + 17], words[i + 18])
|
||||
i += 21
|
||||
self.facets.append(Facet(norm, v1, v2, v3))
|
||||
|
||||
self.facets.sort(key = Facet.key)
|
||||
else:
|
||||
print("Not an OpenSCAD ascii STL file")
|
||||
sys.exit(1)
|
||||
|
||||
def write(self, fname):
|
||||
with open(fname,"wt") as f:
|
||||
print('solid OpenSCAD_Model', file=f)
|
||||
for facet in self.facets:
|
||||
print(' facet normal %s %s %s' % (facet.normal.dx, facet.normal.dy, facet.normal.dz), file=f)
|
||||
print(' outer loop', file=f)
|
||||
for vertex in facet.vertices:
|
||||
print(' vertex %s %s %s' % (vertex.x, vertex.y, vertex.z), file=f)
|
||||
print(' endloop', file=f)
|
||||
print(' endfacet', file=f)
|
||||
print('endsolid OpenSCAD_Model', file=f)
|
||||
|
||||
def canonicalise(fname):
|
||||
stl = STL(fname)
|
||||
stl.write(fname)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
canonicalise(sys.argv[1])
|
||||
else:
|
||||
print("usage: c14n_stl file")
|
||||
sys.exit(1)
|
49
scripts/deps.py
Normal file
49
scripts/deps.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
|
||||
def mtime(file):
|
||||
if os.path.isfile(file):
|
||||
return os.path.getmtime(file)
|
||||
return 0
|
||||
|
||||
def deps_name(dir, scad_name):
|
||||
return dir + '/' + scad_name[:-5] + '.deps'
|
||||
|
||||
def read_deps(dname):
|
||||
with open(dname, "rt") as file:
|
||||
lines = file.readlines()
|
||||
deps = []
|
||||
for line in lines:
|
||||
if line.startswith('\t'):
|
||||
dep = line[1 : -1].rstrip(' \\')
|
||||
if not dep in ['stl.scad', 'dxf.scad', 'svf.scad', 'png.scad']:
|
||||
deps.append(dep)
|
||||
return deps
|
||||
|
||||
def check_deps(target_mtime, dname):
|
||||
if not target_mtime:
|
||||
return "target missing"
|
||||
if not os.path.isfile(dname):
|
||||
return "no deps"
|
||||
deps = read_deps(dname)
|
||||
for dep in deps:
|
||||
if mtime(dep) > target_mtime:
|
||||
return dep + ' changed'
|
||||
return None
|
32
scripts/dxfs.py
Normal file
32
scripts/dxfs.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
|
||||
from exports import make_parts
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1 and not '.' in sys.argv[1]:
|
||||
target, parts = sys.argv[1], sys.argv[2:]
|
||||
else:
|
||||
target, parts = None, sys.argv[1:]
|
||||
make_parts(target, 'dxf', parts)
|
133
scripts/exports.py
Normal file
133
scripts/exports.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import openscad
|
||||
import sys
|
||||
import c14n_stl
|
||||
from set_config import *
|
||||
import time
|
||||
import times
|
||||
from deps import *
|
||||
|
||||
def bom_to_parts(target_dir, part_type, assembly = None):
|
||||
#
|
||||
# Make a list of all the parts in the BOM
|
||||
#
|
||||
part_files = []
|
||||
bom = assembly + '.txt' if assembly else "bom.txt"
|
||||
suffix = ".dxf" if part_type == 'svg' else '.' + part_type
|
||||
with open(target_dir + "/../bom/" + bom, "rt") as f:
|
||||
for line in f.readlines():
|
||||
words = line.split()
|
||||
if words:
|
||||
last_word = words[-1]
|
||||
if last_word.endswith(suffix):
|
||||
part_files.append(last_word[:-4] + '.' + part_type)
|
||||
return part_files
|
||||
|
||||
def make_parts(target, part_type, parts = None):
|
||||
#
|
||||
# Make the target directory
|
||||
#
|
||||
top_dir = set_config(target)
|
||||
target_dir = top_dir + part_type + 's'
|
||||
deps_dir = top_dir + "deps"
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
if not os.path.isdir(deps_dir):
|
||||
os.makedirs(deps_dir)
|
||||
times.read_times(target_dir)
|
||||
#
|
||||
# Decide which files to make
|
||||
#
|
||||
if parts:
|
||||
targets = list(parts) #copy the list so we dont modify the list passed in
|
||||
else:
|
||||
targets = bom_to_parts(target_dir, part_type)
|
||||
for file in os.listdir(target_dir):
|
||||
if file.endswith('.' + part_type):
|
||||
if not file in targets:
|
||||
print("Removing %s" % file)
|
||||
os.remove(target_dir + '/' + file)
|
||||
#
|
||||
# Find all the scad files
|
||||
#
|
||||
lib_dir = os.environ['OPENSCADPATH'] + '/NopSCADlib'
|
||||
used = []
|
||||
module_suffix = '_dxf' if part_type == 'svg' else '_' + part_type
|
||||
for dir in [source_dir, lib_dir]:
|
||||
for filename in os.listdir(dir):
|
||||
if filename[-5:] == ".scad":
|
||||
#
|
||||
# find any modules ending in _<part_type>
|
||||
#
|
||||
with open(dir + "/" + filename, "r") as f:
|
||||
for line in f.readlines():
|
||||
words = line.split()
|
||||
if(len(words) and words[0] == "module"):
|
||||
module = words[1].split('(')[0]
|
||||
if module.endswith(module_suffix):
|
||||
base_name = module[:-4]
|
||||
part = base_name + '.' + part_type
|
||||
if part in targets:
|
||||
#
|
||||
# make a file to use the module
|
||||
#
|
||||
part_maker_name = part_type + ".scad"
|
||||
with open(part_maker_name, "w") as f:
|
||||
f.write("use <%s/%s>\n" % (dir, filename))
|
||||
f.write("%s();\n" % module);
|
||||
#
|
||||
# Run openscad on the created file
|
||||
#
|
||||
part_file = target_dir + "/" + part
|
||||
dname = deps_name(deps_dir, filename)
|
||||
changed = check_deps(mtime(part_file), dname)
|
||||
changed = times.check_have_time(changed, part)
|
||||
if changed:
|
||||
print(changed)
|
||||
t = time.time()
|
||||
openscad.run("-D$bom=1", "-d", dname, "-o", part_file, part_maker_name)
|
||||
times.add_time(part, t)
|
||||
if part_type == 'stl':
|
||||
c14n_stl.canonicalise(part_file)
|
||||
targets.remove(part)
|
||||
os.remove(part_maker_name)
|
||||
#
|
||||
# Add the files on the BOM to the used list for plates.py
|
||||
#
|
||||
for line in open("openscad.log"):
|
||||
if line[:7] == 'ECHO: "' and line[-6:] == '.' + part_type + '"\n':
|
||||
used.append(line[7:-2])
|
||||
#
|
||||
# List the ones we didn't find
|
||||
#
|
||||
if targets:
|
||||
for part in targets:
|
||||
if part[-4:] != '.' + part_type:
|
||||
print(part, "is not a", part_type, "file")
|
||||
else:
|
||||
print("Could not find a module called", part[:-4] + module_suffix, "to make", part)
|
||||
sys.exit(1)
|
||||
times.print_times()
|
||||
return used
|
68
scripts/gallery.py
Normal file
68
scripts/gallery.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
#
|
||||
# Find projects and add them to the gallery
|
||||
#
|
||||
from __future__ import print_function
|
||||
import os
|
||||
from colorama import Fore, init
|
||||
from tests import do_cmd
|
||||
import re
|
||||
from shutil import copyfile
|
||||
|
||||
project_dir = '../..'
|
||||
target_dir = 'gallery'
|
||||
output_name = target_dir + '/readme.md'
|
||||
|
||||
def gallery():
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
projects = [i for i in os.listdir(project_dir) if os.path.isdir(project_dir + '/' + i + '/assemblies')]
|
||||
with open(output_name, 'wt') as output_file:
|
||||
for project in projects:
|
||||
path = project_dir + '/' + project
|
||||
print(project)
|
||||
document = path + '/readme.md'
|
||||
if os.path.isfile(document):
|
||||
with open(document, 'rt') as readme:
|
||||
for line in readme.readlines():
|
||||
match = re.match(r"^.*!(\[.*\]\(.*\)).*$", line)
|
||||
if match:
|
||||
image = match.group(0)
|
||||
if image.startswith(':
|
||||
file = image[17 : -1]
|
||||
line = line.replace(image, '' % project)
|
||||
copyfile(path + '/' + file, '%s/%s.png' %(target_dir, project))
|
||||
else:
|
||||
line = line.replace(image, '')
|
||||
print(line[:-1], file = output_file)
|
||||
if line == '---\n':
|
||||
break;
|
||||
else:
|
||||
print(Fore.MAGENTA + "Can't find", document, Fore.WHITE);
|
||||
with open(target_dir + "/readme.html", "wt") as html_file:
|
||||
do_cmd(("python -m markdown -x tables " + output_name).split(), html_file)
|
||||
|
||||
if __name__ == '__main__':
|
||||
init()
|
||||
gallery()
|
35
scripts/make_all.py
Normal file
35
scripts/make_all.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
from exports import make_parts
|
||||
from bom import boms
|
||||
from render import render
|
||||
from views import views
|
||||
|
||||
if __name__ == '__main__':
|
||||
target = None if len(sys.argv) == 1 else sys.argv[1]
|
||||
boms(target)
|
||||
for part in ['stl', 'dxf']:
|
||||
make_parts(target, part)
|
||||
render(target, part)
|
||||
views(target)
|
38
scripts/openscad.py
Normal file
38
scripts/openscad.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
#
|
||||
# Run openscad
|
||||
#
|
||||
from __future__ import print_function
|
||||
|
||||
import subprocess, sys
|
||||
|
||||
def run(*args):
|
||||
cmd = ["openscad"] + list(args)
|
||||
for arg in cmd:
|
||||
print(arg, end=" ")
|
||||
print()
|
||||
with open("openscad.log", "w") as log:
|
||||
rc = subprocess.call(cmd, stdout = log, stderr = log)
|
||||
for line in open("openscad.log", "rt"):
|
||||
if 'ERROR:' in line or 'WARNING:' in line:
|
||||
print(line[:-1])
|
||||
if rc:
|
||||
sys.exit(rc)
|
69
scripts/render.py
Normal file
69
scripts/render.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
from set_config import *
|
||||
from exports import bom_to_parts
|
||||
import os
|
||||
import openscad
|
||||
from tests import do_cmd
|
||||
from deps import mtime
|
||||
|
||||
def render(target, type):
|
||||
#
|
||||
# Make the target directory
|
||||
#
|
||||
target_dir = set_config(target) + type + 's'
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
#
|
||||
# Find all the parts
|
||||
#
|
||||
parts = bom_to_parts(target_dir, type)
|
||||
#
|
||||
# Remove unused png files
|
||||
#
|
||||
for file in os.listdir(target_dir):
|
||||
if file.endswith('.png'):
|
||||
if not file[:-4] + '.' + type in parts:
|
||||
print("Removing %s" % file)
|
||||
os.remove(target_dir + '/' + file)
|
||||
|
||||
for part in parts:
|
||||
part_file = target_dir + '/' + part
|
||||
png_name = target_dir + '/' + part[:-4] + '.png'
|
||||
#
|
||||
# make a file to import the stl
|
||||
#
|
||||
if mtime(part_file) > mtime(png_name):
|
||||
png_maker_name = "png.scad"
|
||||
with open(png_maker_name, "w") as f:
|
||||
f.write('color("lime") import("%s");\n' % part_file)
|
||||
cam = "--camera=0,0,0,70,0,315,500" if type == 'stl' else "--camera=0,0,0,0,0,0,500"
|
||||
render = "--preview" if type == 'stl' else "--render"
|
||||
openscad.run("--projection=p", "--imgsize=4096,4096", cam, render, "--autocenter", "--viewall", "-o", png_name, png_maker_name);
|
||||
do_cmd(("magick "+ png_name + " -trim -resize 280x280 -background #ffffe5 -gravity Center -extent 280x280 -bordercolor #ffffe5 -border 10 " + png_name).split())
|
||||
os.remove(png_maker_name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
render(target, 'stl')
|
||||
render(target, 'dxf')
|
84
scripts/set_config.py
Normal file
84
scripts/set_config.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
#
|
||||
# Set target configuration for multi-target projects that have variable configurations.
|
||||
#
|
||||
from __future__ import print_function
|
||||
|
||||
source_dir = 'scad'
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def valid_targets():
|
||||
return [i[7:-5] for i in os.listdir(source_dir) if i[0:7] == "config_" and i[-5:] == ".scad"]
|
||||
|
||||
def valid_targets_string():
|
||||
result = ''
|
||||
targets = valid_targets()
|
||||
for t in targets:
|
||||
if result:
|
||||
if t == targets[-1]:
|
||||
result += ' and '
|
||||
else:
|
||||
result += ', '
|
||||
result += t
|
||||
return result
|
||||
|
||||
|
||||
def set_config(target):
|
||||
targets = valid_targets()
|
||||
if not target:
|
||||
if not targets:
|
||||
return ""
|
||||
print("Must specify a configuration: " + valid_targets_string())
|
||||
sys.exit(1)
|
||||
|
||||
if not targets:
|
||||
print("Not a muli-configuration project (no config_<target>.scad files found)")
|
||||
sys.exit(1)
|
||||
|
||||
if not target in targets:
|
||||
print(target + " is not a configuration, avaliable configurations are: " + valid_targets_string())
|
||||
sys.exit(1)
|
||||
|
||||
fname = source_dir + "/target.scad"
|
||||
text = "include <config_%s.scad>\n" % target;
|
||||
line = ""
|
||||
try:
|
||||
with open(fname,"rt") as f:
|
||||
line = f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
if line != text:
|
||||
with open(fname,"wt") as f:
|
||||
f. write(text);
|
||||
return target + "/"
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = len(sys.argv)
|
||||
if args == 2:
|
||||
set_config(sys.argv[1])
|
||||
else:
|
||||
print("usage: set_config config_name")
|
||||
sys.exit(1)
|
32
scripts/stls.py
Normal file
32
scripts/stls.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
|
||||
from exports import make_parts
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1 and not '.' in sys.argv[1]:
|
||||
target, parts = sys.argv[1], sys.argv[2:]
|
||||
else:
|
||||
target, parts = None, sys.argv[1:]
|
||||
make_parts(target, 'stl', parts)
|
32
scripts/svgs.py
Normal file
32
scripts/svgs.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
|
||||
from exports import make_parts
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1 and not '.' in sys.argv[1]:
|
||||
target, parts = sys.argv[1], sys.argv[2:]
|
||||
else:
|
||||
target, parts = None, sys.argv[1:]
|
||||
make_parts(target, 'svg', parts)
|
230
scripts/tests.py
Normal file
230
scripts/tests.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import openscad
|
||||
import subprocess
|
||||
import bom
|
||||
import times
|
||||
import time
|
||||
import json
|
||||
from deps import *
|
||||
from blurb import *
|
||||
|
||||
w = 4096
|
||||
h = w
|
||||
|
||||
def do_cmd(cmd, output = sys.stdout):
|
||||
for arg in cmd:
|
||||
print(arg, end = " ")
|
||||
print()
|
||||
subprocess.call(cmd, stdout = output)
|
||||
|
||||
def depluralise(name):
|
||||
if name[-3:] == "ies" and name != "zipties":
|
||||
return name[:-3] + 'y'
|
||||
if name[-3:] == "hes":
|
||||
return name[:-2]
|
||||
if name[-1:] == 's':
|
||||
return name[:-1]
|
||||
return name
|
||||
|
||||
def is_plural(name):
|
||||
return name != depluralise(name)
|
||||
|
||||
def tests(tests):
|
||||
scad_dir = "tests"
|
||||
deps_dir = scad_dir + "/deps"
|
||||
png_dir = scad_dir + "/png"
|
||||
bom_dir = scad_dir + "/bom"
|
||||
for dir in [deps_dir, png_dir, bom_dir]:
|
||||
if not os.path.isdir(dir):
|
||||
os.makedirs(dir)
|
||||
doc_name = "readme.md"
|
||||
index = {}
|
||||
bodies = {}
|
||||
times.read_times()
|
||||
#
|
||||
# Make cover pic if does not exist as very slow. Delete it to force an update.
|
||||
#
|
||||
png_name = "libtest.png"
|
||||
scad_name = "libtest.scad"
|
||||
if not os.path.isfile(png_name):
|
||||
openscad.run("--projection=p", "--imgsize=%d,%d" % (w, h), "--camera=0,0,0,50,0,340,500", "--autocenter", "--viewall", "-o", png_name, scad_name);
|
||||
do_cmd(["magick", png_name, "-trim", "-resize", "1280", "-bordercolor", "#ffffe5", "-border", "10", png_name])
|
||||
#
|
||||
# List of individual part files
|
||||
#
|
||||
scads = [i for i in os.listdir(scad_dir) if i[-5:] == ".scad"]
|
||||
|
||||
for scad in scads:
|
||||
base_name = scad[:-5]
|
||||
if not tests or base_name in tests:
|
||||
print(base_name)
|
||||
cap_name = base_name[0].capitalize() + base_name[1:]
|
||||
scad_name = scad_dir + '/' + scad
|
||||
png_name = png_dir + '/' + base_name + '.png'
|
||||
bom_name = bom_dir + '/' + base_name + '.json'
|
||||
|
||||
objects_name = None
|
||||
vits_name = 'vitamins/' + base_name + '.scad'
|
||||
if is_plural(base_name) and os.path.isfile(vits_name):
|
||||
objects_name = vits_name
|
||||
|
||||
locations = [
|
||||
('vitamins/' + depluralise(base_name) + '.scad', 'Vitamins'),
|
||||
(base_name + '.scad', 'Printed'),
|
||||
('utils/' + base_name + '.scad', 'Utilities'),
|
||||
('utils/core/' + base_name + '.scad', 'Core Utilities'),
|
||||
]
|
||||
|
||||
for name, type in locations:
|
||||
if os.path.isfile(name):
|
||||
impl_name = name
|
||||
break
|
||||
else:
|
||||
print("Can't find implementation!")
|
||||
continue
|
||||
|
||||
vsplit = "N"
|
||||
vtype = locations[0][1]
|
||||
types = [vtype + ' A-' + vsplit[0], vtype + ' ' + chr(ord(vsplit) + 1) + '-Z'] + [loc[1] for loc in locations[1 :]]
|
||||
if type == vtype:
|
||||
type = types[0] if cap_name[0] <= vsplit else types[1]
|
||||
|
||||
if not type in bodies:
|
||||
bodies[type] = []
|
||||
index[type] = []
|
||||
body = bodies[type]
|
||||
|
||||
index[type] += [cap_name]
|
||||
body += ['<a name="%s"></a>' % cap_name]
|
||||
body += ["## " + cap_name]
|
||||
|
||||
doc = None
|
||||
if impl_name:
|
||||
doc = scrape_code(impl_name)
|
||||
blurb = doc["blurb"]
|
||||
else:
|
||||
blurb = scrape_blurb(scad_name)
|
||||
|
||||
if not len(blurb):
|
||||
print("Blurb not found!")
|
||||
else:
|
||||
body += [ blurb ]
|
||||
|
||||
if objects_name:
|
||||
body += ["[%s](%s) Object definitions.\n" % (objects_name, objects_name)]
|
||||
|
||||
if impl_name:
|
||||
body += ["[%s](%s) Implementation.\n" % (impl_name, impl_name)]
|
||||
|
||||
body += ["[%s](%s) Code for this example.\n" % (scad_name.replace('\\','/'), scad_name)]
|
||||
|
||||
if doc:
|
||||
for thing in ["properties", "functions", "modules"]:
|
||||
things = doc[thing]
|
||||
if things:
|
||||
body += ['### %s\n| | |\n|:--- |:--- |' % thing.title()]
|
||||
for item in sorted(things):
|
||||
body += ['| ```%s``` | %s |' % (item, things[item])]
|
||||
body += ['']
|
||||
|
||||
body += ["\n" %(base_name, png_name)]
|
||||
|
||||
dname = deps_name(deps_dir, scad)
|
||||
oldest = min(mtime(png_name), mtime(bom_name))
|
||||
changed = check_deps(oldest, dname)
|
||||
changed = times.check_have_time(changed, scad_name)
|
||||
if changed:
|
||||
print(changed)
|
||||
t = time.time()
|
||||
openscad.run("-D", "$bom=2", "--projection=p", "--imgsize=%d,%d" % (w, h), "--camera=0,0,0,70,0,315,500", "--autocenter", "--viewall", "-d", dname, "-o", png_name, scad_name);
|
||||
times.add_time(scad_name, t)
|
||||
do_cmd(["magick", png_name, "-trim", "-resize", "1000x600", "-bordercolor", "#ffffe5", "-border", "10", png_name])
|
||||
BOM = bom.parse_bom()
|
||||
with open(bom_name, 'wt') as outfile:
|
||||
json.dump(BOM.flat_data(), outfile, indent = 4)
|
||||
|
||||
with open(bom_name, "rt") as bom_file:
|
||||
BOM = json.load(bom_file)
|
||||
for thing in ["vitamins", "printed", "routed", "assemblies"]:
|
||||
things = BOM[thing]
|
||||
if things:
|
||||
body += ['### %s\n| | | |\n| ---:|:--- |:---|' % thing.title()]
|
||||
for item in sorted(things, key = lambda s: s.split(":")[-1]):
|
||||
name = item
|
||||
desc = ''
|
||||
if thing == "vitamins":
|
||||
vit = item.split(':')
|
||||
name = '```' + vit[0] + '```' if vit[0] else ''
|
||||
while '[[' in name and ']]' in name:
|
||||
i = name.find('[[')
|
||||
j = name.find(']]') + 2
|
||||
name = name.replace(name[i : j], '[ ... ]')
|
||||
desc = vit[1]
|
||||
body += ['| %3d | %s | %s |' % (things[item], name, desc)]
|
||||
body += ['']
|
||||
|
||||
body += ['\n<a href="#top">Top</a>']
|
||||
body += ["\n---"]
|
||||
|
||||
with open(doc_name, "wt") as doc_file:
|
||||
print('# NopSCADlib', file = doc_file)
|
||||
print('''\
|
||||
An ever expanding library of parts modelled in OpenSCAD useful for 3D printers and enclosures for electronics, etc.
|
||||
|
||||
It contains lots of vitamins (the RepRap term for non-printed parts), some general purpose printed parts and
|
||||
some utilities. There are also Python scripts to generate Bills of Materials (BOMs),
|
||||
STL files for all the printed parts and DXF files for CNC routed parts in a project.
|
||||
|
||||
<img src="libtest.png" width="100%"/>\n
|
||||
''', file = doc_file)
|
||||
|
||||
print('## Table of Contents<a name="top"/>', file = doc_file)
|
||||
print('<table><tr>', file = doc_file)
|
||||
n = 0
|
||||
for type in types:
|
||||
print('<th align="left"> %s </th>' % type, end = '', file = doc_file)
|
||||
n = max(n, len(index[type]))
|
||||
print('</tr>', file = doc_file)
|
||||
for i in range(n):
|
||||
print('<tr>', file = doc_file, end = '')
|
||||
for type in types:
|
||||
if i < len(index[type]):
|
||||
name = index[type][i]
|
||||
print('<td> <a href = "#' + name + '">' + name + '</a> </td>', file = doc_file, end = '')
|
||||
else:
|
||||
print('<td></td>', file = doc_file, end = '')
|
||||
print('</tr>', file = doc_file)
|
||||
print('</table>\n\n---', file = doc_file)
|
||||
for type in types:
|
||||
for line in bodies[type]:
|
||||
print(line, file = doc_file)
|
||||
with open("readme.html", "wt") as html_file:
|
||||
do_cmd("python -m markdown -x tables readme.md".split(), html_file)
|
||||
times.print_times()
|
||||
do_cmd('codespell -L od readme.md'.split())
|
||||
|
||||
if __name__ == '__main__':
|
||||
tests(sys.argv[1:])
|
71
scripts/times.py
Normal file
71
scripts/times.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# show execution times and how they have changed.
|
||||
|
||||
import json
|
||||
import time
|
||||
from colorama import Fore, init
|
||||
|
||||
def read_times(dir = '.'):
|
||||
global times, last_times, times_fname
|
||||
times_fname = dir + '/times.txt'
|
||||
init()
|
||||
try:
|
||||
with open(times_fname) as json_file:
|
||||
times = json.load(json_file)
|
||||
last_times = dict(times)
|
||||
except:
|
||||
times = {}
|
||||
last_times = {}
|
||||
|
||||
def write_times():
|
||||
with open(times_fname, 'w') as outfile:
|
||||
json.dump(times, outfile, indent = 4)
|
||||
|
||||
def got_time(name):
|
||||
return name in last_times
|
||||
|
||||
def check_have_time(changed, name):
|
||||
if not changed and not got_time(name):
|
||||
changed = "no previous time"
|
||||
return changed
|
||||
|
||||
def add_time(name, start):
|
||||
times[name] = round(time.time() - start, 3)
|
||||
|
||||
def print_times():
|
||||
write_times()
|
||||
sorted_times = sorted(times.items(), key=lambda kv: kv[1])
|
||||
total = 0
|
||||
for entry in sorted_times:
|
||||
colour = Fore.WHITE
|
||||
key = entry[0]
|
||||
new = entry[1]
|
||||
delta = 0
|
||||
if key in last_times:
|
||||
old = last_times[key]
|
||||
delta = new - old
|
||||
if delta > 0.3:
|
||||
colour = Fore.RED
|
||||
if delta < -0.3:
|
||||
colour = Fore.GREEN
|
||||
print(colour + "%5.1f %5.1f %s" % (new, delta, key))
|
||||
total += new
|
||||
print(Fore.WHITE + "%5.1f" % total)
|
345
scripts/views.py
Normal file
345
scripts/views.py
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# NopSCADlib Copyright Chris Palmer 2018
|
||||
# nop.head@gmail.com
|
||||
# hydraraptor.blogspot.com
|
||||
#
|
||||
# This file is part of NopSCADlib.
|
||||
#
|
||||
# NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with NopSCADlib.
|
||||
# If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
#
|
||||
#: Generate assembly views and intructions
|
||||
#
|
||||
from __future__ import print_function
|
||||
from set_config import *
|
||||
import openscad
|
||||
from tests import do_cmd
|
||||
import time
|
||||
import times
|
||||
from deps import *
|
||||
import os
|
||||
import json
|
||||
import blurb
|
||||
import bom
|
||||
from colorama import Fore
|
||||
|
||||
def is_assembly(s):
|
||||
return s[-9:] == '_assembly' or s[-11:] == '_assemblies'
|
||||
|
||||
def add_assembly(flat_bom, bom):
|
||||
if not bom in flat_bom:
|
||||
big = False
|
||||
for ass in bom["assemblies"]:
|
||||
add_assembly(flat_bom, ass)
|
||||
if ass["routed"]:
|
||||
big = True
|
||||
bom["big"] = big or bom["routed"]
|
||||
flat_bom.append(bom)
|
||||
|
||||
def bom_to_assemblies(bom_dir):
|
||||
global flat_bom
|
||||
#
|
||||
# Make a list of all the parts in the BOM
|
||||
#
|
||||
bom = {}
|
||||
bom_file = bom_dir + "/bom.json"
|
||||
with open(bom_file) as json_file:
|
||||
bom = json.load(json_file)
|
||||
flat_bom = []
|
||||
add_assembly(flat_bom, bom)
|
||||
ass = flat_bom[-1]
|
||||
if len(ass["assemblies"]) < 2 and not ass["vitamins"] and not ass["printed"] and not ass["routed"]:
|
||||
flat_bom = flat_bom[:-1]
|
||||
return [assembly["name"] for assembly in flat_bom]
|
||||
|
||||
def eop(print_mode, project, doc_file, last = False, first = False):
|
||||
if print_mode:
|
||||
print('\n<div style="page-break-after: always;"></div>', file = doc_file)
|
||||
else:
|
||||
if not first:
|
||||
print('[Top](#%s)' % project.lower().replace(' ', '-'), file = doc_file)
|
||||
if not last:
|
||||
print("\n---", file = doc_file)
|
||||
|
||||
def pad(s, before, after = 0):
|
||||
return ' ' * before + str(s) + ' ' * after
|
||||
|
||||
def views(target, do_assemblies = None):
|
||||
done_assemblies = []
|
||||
#
|
||||
# Make the target directory
|
||||
#
|
||||
top_dir = set_config(target)
|
||||
target_dir = top_dir + 'assemblies'
|
||||
deps_dir = top_dir + "deps"
|
||||
bom_dir = top_dir + "bom"
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
if not os.path.isdir(deps_dir):
|
||||
os.makedirs(deps_dir)
|
||||
times.read_times(target_dir)
|
||||
#
|
||||
# Find all the assemblies
|
||||
#
|
||||
assemblies = bom_to_assemblies(bom_dir)
|
||||
for file in os.listdir(target_dir):
|
||||
if file.endswith('.png'):
|
||||
assembly = file[:-4].replace('_assembled', '_assembly')
|
||||
if assembly.endswith('_tn'):
|
||||
assembly = assembly[:-3]
|
||||
if not assembly in assemblies:
|
||||
print("Removing %s" % file)
|
||||
os.remove(target_dir + '/' + file)
|
||||
#
|
||||
# Find all the scad files
|
||||
#
|
||||
main_blurb = None
|
||||
lib_dir = os.environ['OPENSCADPATH'] + '/NopSCADlib'
|
||||
for dir in [source_dir, lib_dir]:
|
||||
for filename in os.listdir(dir):
|
||||
if filename.endswith('.scad'):
|
||||
#
|
||||
# find any modules with names ending in _assembly
|
||||
#
|
||||
with open(dir + "/" + filename, "r") as f:
|
||||
lines = f.readlines()
|
||||
line_no = 0
|
||||
for line in lines:
|
||||
words = line.split()
|
||||
if len(words) and words[0] == "module":
|
||||
module = words[1].split('(')[0]
|
||||
if is_assembly(module):
|
||||
if module in assemblies:
|
||||
#
|
||||
# Scrape the assembly instructions
|
||||
#
|
||||
for ass in flat_bom:
|
||||
if ass["name"] == module:
|
||||
if not "blurb" in ass:
|
||||
ass["blurb"] = blurb.scrape_module_blurb(lines[:line_no])
|
||||
break
|
||||
if not do_assemblies or module in do_assemblies:
|
||||
#
|
||||
# make a file to use the module
|
||||
#
|
||||
png_maker_name = 'png.scad'
|
||||
with open(png_maker_name, "w") as f:
|
||||
f.write("use <%s/%s>\n" % (dir, filename))
|
||||
f.write("%s();\n" % module);
|
||||
#
|
||||
# Run openscad on the created file
|
||||
#
|
||||
dname = deps_name(deps_dir, filename)
|
||||
for explode in [0, 1]:
|
||||
png_name = target_dir + '/' + module + '.png'
|
||||
if not explode:
|
||||
png_name = png_name.replace('_assembly', '_assembled')
|
||||
changed = check_deps(mtime(png_name), dname)
|
||||
changed = times.check_have_time(changed, png_name)
|
||||
if changed:
|
||||
print(changed)
|
||||
t = time.time()
|
||||
openscad.run("-D$pose=1", "-D$explode=%d" % explode, "--projection=p", "--imgsize=4096,4096", "--autocenter", "--viewall", "-d", dname, "-o", png_name, png_maker_name);
|
||||
times.add_time(png_name, t)
|
||||
do_cmd(["magick", png_name, "-trim", "-resize", "1004x1004", "-bordercolor", "#ffffe5", "-border", "10", png_name])
|
||||
tn_name = png_name.replace('.png', '_tn.png')
|
||||
if mtime(png_name) > mtime(tn_name):
|
||||
do_cmd(("magick "+ png_name + " -trim -resize 280x280 -background #ffffe5 -gravity Center -extent 280x280 -bordercolor #ffffe5 -border 10 " + tn_name).split())
|
||||
os.remove(png_maker_name)
|
||||
done_assemblies.append(module)
|
||||
else:
|
||||
if module == 'main_assembly':
|
||||
main_blurb = blurb.scrape_module_blurb(lines[:line_no])
|
||||
line_no += 1
|
||||
times.print_times()
|
||||
#
|
||||
# Build the document
|
||||
#
|
||||
for print_mode in [True, False]:
|
||||
doc_name = top_dir + "readme.md"
|
||||
with open(doc_name, "wt") as doc_file:
|
||||
#
|
||||
# Title, description and picture
|
||||
#
|
||||
project = ' '.join(word[0].upper() + word[1:] for word in os.path.basename(os.getcwd()).split('_'))
|
||||
print('# %s' % project, file = doc_file)
|
||||
main_file = bom.find_scad_file('main_assembly')
|
||||
if not main_file:
|
||||
raise Exception("can't find source for main_assembly")
|
||||
text = blurb.scrape_blurb(source_dir + '/' + main_file)
|
||||
if len(text):
|
||||
print(text, file = doc_file, end = '')
|
||||
else:
|
||||
if print_mode:
|
||||
print(Fore.MAGENTA + "Missing project description" + Fore.WHITE)
|
||||
print('\n' % flat_bom[-1]["name"].replace('_assembly', '_assembled'), file = doc_file)
|
||||
eop(print_mode, project, doc_file, first = True)
|
||||
#
|
||||
# Build TOC
|
||||
#
|
||||
print('## Table of Contents', file = doc_file)
|
||||
print('[TOC]\n', file = doc_file)
|
||||
eop(print_mode, project, doc_file)
|
||||
#
|
||||
# Global BOM
|
||||
#
|
||||
print('## Parts list', file = doc_file)
|
||||
vitamins = {}
|
||||
printed = {}
|
||||
routed = {}
|
||||
for ass in flat_bom:
|
||||
for v in ass["vitamins"]:
|
||||
if v in vitamins:
|
||||
vitamins[v] += ass["vitamins"][v]
|
||||
else:
|
||||
vitamins[v] = ass["vitamins"][v]
|
||||
for p in ass["printed"]:
|
||||
if p in printed:
|
||||
printed[p] += ass["printed"][p]
|
||||
else:
|
||||
printed[p] = ass["printed"][p]
|
||||
for ass in flat_bom:
|
||||
name = ass["name"][:-9].replace('_', ' ').title().replace(' ',' ')
|
||||
print('| <span style="writing-mode: vertical-rl; text-orientation: mixed;">%s</span> ' % name, file = doc_file, end = '')
|
||||
print('| <span style="writing-mode: vertical-rl; text-orientation: mixed;">TOTALS</span> | |', file = doc_file)
|
||||
print(('|--:' * len(flat_bom) + '|--:|:--|'), file = doc_file)
|
||||
for v in sorted(vitamins, key = lambda s: s.split(":")[-1]):
|
||||
for ass in flat_bom:
|
||||
count = ass["vitamins"][v] if v in ass["vitamins"] else '.'
|
||||
print('| %s ' % pad(count, 2, 1), file = doc_file, end = '')
|
||||
print('| %s | %s |' % (pad(vitamins[v], 2, 1), pad(v.split(":")[1], 2)), file = doc_file)
|
||||
print(('| ' * len(flat_bom) + '| | **3D Printed parts** |'), file = doc_file)
|
||||
for p in sorted(printed):
|
||||
for ass in flat_bom:
|
||||
count = ass["printed"][p] if p in ass["printed"] else '.'
|
||||
print('| %s ' % pad(count, 2, 1), file = doc_file, end = '')
|
||||
print('| %s | %s |' % (pad(printed[p], 2, 1), pad(p, 3)), file = doc_file)
|
||||
eop(print_mode, project, doc_file)
|
||||
#
|
||||
# Assembly instructions
|
||||
#
|
||||
for ass in flat_bom:
|
||||
name = ass["name"]
|
||||
cap_name = name.replace('_', ' ').title()
|
||||
if ass["count"] > 1:
|
||||
print("## %d x %s" % (ass["count"], cap_name), file = doc_file)
|
||||
else:
|
||||
print("## %s" % cap_name, file = doc_file)
|
||||
vitamins = ass["vitamins"]
|
||||
if vitamins:
|
||||
print("### Vitamins", file = doc_file)
|
||||
print("|Qty|Description|", file = doc_file)
|
||||
print("|--:|:----------|", file = doc_file)
|
||||
for v in vitamins:
|
||||
print("|%d|%s|" % (vitamins[v], v.split(":")[1]), file = doc_file)
|
||||
print("\n", file = doc_file)
|
||||
|
||||
printed = ass["printed"]
|
||||
if printed:
|
||||
print('### 3D Printed parts', file = doc_file)
|
||||
i = 0
|
||||
for p in printed:
|
||||
print('%s %d x %s |' % ('\n|' if not (i % 3) else '', printed[p], p), file = doc_file, end = '')
|
||||
if (i % 3) == 2 or i == len(printed) - 1:
|
||||
n = (i % 3) + 1
|
||||
print('\n|%s' % ('--|' * n), file = doc_file)
|
||||
for j in range(n):
|
||||
part = list(printed.keys())[i - n + j + 1]
|
||||
print('|  %s' % (part, part.replace('.stl','.png'), '|\n' if j == j - 1 else ''), end = '', file = doc_file)
|
||||
print('\n', file = doc_file)
|
||||
i += 1
|
||||
print('\n', file = doc_file)
|
||||
|
||||
routed = ass["routed"]
|
||||
if routed:
|
||||
print("### CNC Routed parts", file = doc_file)
|
||||
i = 0
|
||||
for r in routed:
|
||||
print('%s %d x %s |' % ('\n|' if not (i % 3) else '', routed[r], r), file = doc_file, end = '')
|
||||
if (i % 3) == 2 or i == len(routed) - 1:
|
||||
n = (i % 3) + 1
|
||||
print('\n|%s' % ('--|' * n), file = doc_file)
|
||||
for j in range(n):
|
||||
part = list(routed.keys())[i - n + j + 1]
|
||||
print('|  %s' % (part, part.replace('.dxf','.png'), '|\n' if j == j - 1 else ''), end = '', file = doc_file)
|
||||
print('\n', file = doc_file)
|
||||
i += 1
|
||||
print('\n', file = doc_file)
|
||||
|
||||
sub_assemblies = ass["assemblies"]
|
||||
if sub_assemblies:
|
||||
print("### Sub-assemblies", file = doc_file)
|
||||
i = 0
|
||||
for a in sub_assemblies:
|
||||
print('%s %d x %s |' % ('\n|' if not (i % 3) else '', a["count"], a["name"]), file = doc_file, end = '')
|
||||
if (i % 3) == 2 or i == len(sub_assemblies) - 1:
|
||||
n = (i % 3) + 1
|
||||
print('\n|%s' % ('--|' * n), file = doc_file)
|
||||
for j in range(n):
|
||||
a = sub_assemblies[i - n + j + 1]["name"].replace('_assembly', '_assembled')
|
||||
print('|  %s' % (a, a + '_tn.png', '|\n' if j == j - 1 else ''), end = '', file = doc_file)
|
||||
print('\n', file = doc_file)
|
||||
i += 1
|
||||
print('\n', file = doc_file)
|
||||
|
||||
small = not ass["big"]
|
||||
suffix = '_tn.png' if small else '.png'
|
||||
print('### Assembly instructions', file = doc_file)
|
||||
print('\n' % (name, name + suffix), file = doc_file)
|
||||
|
||||
if "blurb" in ass and ass["blurb"]:
|
||||
print(ass["blurb"], file = doc_file)
|
||||
else:
|
||||
if print_mode:
|
||||
print(Fore.MAGENTA + "Missing instructions for %s" % name, Fore.WHITE)
|
||||
|
||||
name = name.replace('_assembly', '_assembled')
|
||||
print('\n' % (name, name + suffix), file = doc_file)
|
||||
eop(print_mode, project, doc_file, last = ass == flat_bom[-1] and not main_blurb)
|
||||
#
|
||||
# If main module is suppressed print any blurb here
|
||||
#
|
||||
if main_blurb:
|
||||
print(main_blurb, file = doc_file)
|
||||
eop(print_mode, project, doc_file, last = True)
|
||||
#
|
||||
# Convert to HTML
|
||||
#
|
||||
html_name = "printme.html" if print_mode else "readme.html"
|
||||
with open(top_dir + html_name, "wt") as html_file:
|
||||
do_cmd(("python -m markdown -x tables -x toc -x sane_lists " + doc_name).split(), html_file)
|
||||
#
|
||||
# Spell check
|
||||
#
|
||||
do_cmd('codespell -L od readme.md'.split())
|
||||
#
|
||||
# List the ones we didn't find
|
||||
#
|
||||
missing = set()
|
||||
for assembly in assemblies + (do_assemblies if do_assemblies else []):
|
||||
if assembly not in done_assemblies:
|
||||
missing.add(assembly)
|
||||
if missing:
|
||||
for assembly in missing:
|
||||
print(Fore.MAGENTA + "Could not find a module called", assembly, Fore.WHITE)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1 and sys.argv[1][-9:] != "_assembly":
|
||||
target, assemblies = sys.argv[1], sys.argv[2:]
|
||||
else:
|
||||
target, assemblies = None, sys.argv[1:]
|
||||
|
||||
views(target, assemblies)
|
Reference in New Issue
Block a user