1
0
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:
Chris Palmer
2019-06-08 22:10:47 +01:00
parent d80facf2ca
commit 34a9b0e87b
246 changed files with 18858 additions and 0 deletions

80
scripts/blurb.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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('![Main Assembly](assemblies/'):
file = image[17 : -1]
line = line.replace(image, '![](%s.png)' % 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 += ["![%s](%s)\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
View 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
View 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 '&nbsp;' * before + str(s) + '&nbsp;' * 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('![Main Assembly](assemblies/%s.png)\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(' ','&nbsp;')
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](stls/%s) %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](dxfs/%s) %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](assemblies/%s) %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('![%s](assemblies/%s)\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('![%s](assemblies/%s)\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)