2019-06-08 17:10:47 -04:00
|
|
|
#!/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/>.
|
|
|
|
#
|
2019-06-11 10:33:13 -04:00
|
|
|
#! Runs all the tests in the tests directory and makes the readme file with a catalog of the results.
|
2019-06-08 17:10:47 -04:00
|
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import openscad
|
|
|
|
import subprocess
|
|
|
|
import bom
|
|
|
|
import times
|
2020-02-22 14:44:01 -05:00
|
|
|
import options
|
2019-06-08 17:10:47 -04:00
|
|
|
import time
|
|
|
|
import json
|
2019-06-09 08:19:08 -04:00
|
|
|
import shutil
|
2019-06-08 17:10:47 -04:00
|
|
|
from deps import *
|
|
|
|
from blurb import *
|
2019-06-09 08:19:08 -04:00
|
|
|
from colorama import Fore
|
2019-06-08 17:10:47 -04:00
|
|
|
|
|
|
|
w = 4096
|
|
|
|
h = w
|
2019-06-10 05:00:47 -04:00
|
|
|
threshold = 20 # Image comparison allowed number of different pixels
|
2019-06-10 14:57:13 -04:00
|
|
|
fuzz = 5 # Image comparison allowed percentage error in pixel value
|
|
|
|
|
|
|
|
colour_scheme = "--colorscheme=Nature"
|
|
|
|
background = "#F8F8F8"
|
2019-06-08 17:10:47 -04:00
|
|
|
|
|
|
|
def do_cmd(cmd, output = sys.stdout):
|
|
|
|
for arg in cmd:
|
|
|
|
print(arg, end = " ")
|
|
|
|
print()
|
2019-06-09 08:19:08 -04:00
|
|
|
return subprocess.call(cmd, stdout = output, stderr = output)
|
|
|
|
|
2019-06-10 05:00:47 -04:00
|
|
|
def compare_images(a, b, c):
|
|
|
|
if not os.path.isfile(a):
|
|
|
|
return -1
|
|
|
|
log_name = 'magick.log'
|
|
|
|
with open(log_name, 'w') as output:
|
|
|
|
do_cmd(("magick compare -metric AE -fuzz %d%% %s %s %s" % (fuzz, a, b, c)).split(), output = output)
|
|
|
|
with open(log_name, 'r') as f:
|
|
|
|
pixels = int(f.read().strip())
|
|
|
|
os.remove(log_name)
|
|
|
|
return pixels
|
|
|
|
|
2019-06-09 08:19:08 -04:00
|
|
|
def update_image(tmp_name, png_name):
|
|
|
|
"""Update an image only if different, otherwise just change the mod time"""
|
2019-06-10 05:00:47 -04:00
|
|
|
diff_name = png_name.replace('.png', '_diff.png')
|
|
|
|
pixels = compare_images(png_name, tmp_name, diff_name)
|
|
|
|
if pixels < 0 or pixels > threshold:
|
|
|
|
shutil.copyfile(tmp_name, png_name)
|
|
|
|
print(Fore.YELLOW + png_name + " updated" + Fore.WHITE, pixels if pixels > 0 else '')
|
|
|
|
else:
|
|
|
|
os.utime(png_name, None)
|
|
|
|
os.remove(diff_name)
|
|
|
|
os.remove(tmp_name)
|
2019-06-09 08:19:08 -04:00
|
|
|
|
2019-06-08 17:10:47 -04:00
|
|
|
|
|
|
|
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()
|
2020-02-22 14:44:01 -05:00
|
|
|
options.check_options(deps_dir)
|
2019-06-08 17:10:47 -04:00
|
|
|
#
|
|
|
|
# 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):
|
2019-06-10 14:57:13 -04:00
|
|
|
openscad.run(colour_scheme, "--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", background, "-border", "10", png_name])
|
2019-06-08 17:10:47 -04:00
|
|
|
#
|
|
|
|
# List of individual part files
|
|
|
|
#
|
2019-08-18 09:26:14 -04:00
|
|
|
scads = [i for i in sorted(os.listdir(scad_dir)) if i[-5:] == ".scad"]
|
2019-06-08 17:10:47 -04:00
|
|
|
|
|
|
|
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'),
|
2019-06-11 18:20:28 -04:00
|
|
|
('printed/' + base_name + '.scad', 'Printed'),
|
2019-06-08 17:10:47 -04:00
|
|
|
('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
|
|
|
|
|
2019-08-21 13:50:15 -04:00
|
|
|
vsplit = "M"
|
2019-06-08 17:10:47 -04:00
|
|
|
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]
|
|
|
|
|
2019-06-13 14:47:02 -04:00
|
|
|
for t in types:
|
|
|
|
if not t in bodies:
|
|
|
|
bodies[t] = []
|
|
|
|
index[t] = []
|
|
|
|
|
2019-06-08 17:10:47 -04:00
|
|
|
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:
|
2019-06-09 03:11:00 -04:00
|
|
|
for thing, heading in [("properties", "Function"), ("functions", "Function"), ("modules", "Module")]:
|
2019-06-08 17:10:47 -04:00
|
|
|
things = doc[thing]
|
|
|
|
if things:
|
2019-06-09 03:11:00 -04:00
|
|
|
body += ['### %s\n| %s | Description |\n|:--- |:--- |' % (thing.title(), heading)]
|
2019-06-08 17:10:47 -04:00
|
|
|
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)
|
2019-08-18 05:53:22 -04:00
|
|
|
oldest = png_name if mtime(png_name) < mtime(bom_name) else bom_name
|
2019-06-08 17:10:47 -04:00
|
|
|
changed = check_deps(oldest, dname)
|
|
|
|
changed = times.check_have_time(changed, scad_name)
|
2020-02-22 14:44:01 -05:00
|
|
|
changed = options.have_changed(changed, oldest)
|
2019-06-08 17:10:47 -04:00
|
|
|
if changed:
|
|
|
|
print(changed)
|
|
|
|
t = time.time()
|
2019-06-09 08:19:08 -04:00
|
|
|
tmp_name = 'tmp.png'
|
2020-02-22 14:44:01 -05:00
|
|
|
openscad.run_list(options.list() + ["-D$bom=2", colour_scheme, "--projection=p", "--imgsize=%d,%d" % (w, h), "--camera=0,0,0,70,0,315,500", "--autocenter", "--viewall", "-d", dname, "-o", tmp_name, scad_name]);
|
2019-06-08 17:10:47 -04:00
|
|
|
times.add_time(scad_name, t)
|
2019-06-10 14:57:13 -04:00
|
|
|
do_cmd(["magick", tmp_name, "-trim", "-resize", "1000x600", "-bordercolor", background, "-border", "10", tmp_name])
|
2019-06-09 08:19:08 -04:00
|
|
|
update_image(tmp_name, png_name)
|
2019-06-08 17:10:47 -04:00
|
|
|
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)
|
2019-06-09 03:11:00 -04:00
|
|
|
for thing, heading in [("vitamins", "Module call | BOM entry") , ("printed", "Filename"), ("routed", "Filename"), ("assemblies", "Name")]:
|
2019-06-08 17:10:47 -04:00
|
|
|
things = BOM[thing]
|
|
|
|
if things:
|
2019-06-09 03:11:00 -04:00
|
|
|
body += ['### %s\n| Qty | %s |\n| ---:|:--- |%s' % (thing.title(), heading, ':---|' if '|' in heading else '')]
|
2019-06-08 17:10:47 -04:00
|
|
|
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]
|
2019-06-09 03:11:00 -04:00
|
|
|
body += ['| %3d | %s | %s |' % (things[item], name, desc)]
|
|
|
|
else:
|
|
|
|
body += ['| %3d | %s |' % (things[item], name)]
|
2019-06-08 17:10:47 -04:00
|
|
|
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),
|
2019-06-10 05:38:57 -04:00
|
|
|
STL files for all the printed parts, DXF files for CNC routed parts in a project and a manual containing assembly
|
2019-06-11 10:33:13 -04:00
|
|
|
instructions and exploded views by scraping markdown embedded in OpenSCAD comments, [see scripts](scripts/readme.md). A simple example project can be found [here](examples/MainsBreakOutBox/readme.md).
|
2019-06-08 17:10:47 -04:00
|
|
|
|
2019-06-10 11:14:44 -04:00
|
|
|
For more examples of what it can make see the [gallery](gallery/readme.md).
|
2019-06-08 17:27:59 -04:00
|
|
|
|
|
|
|
The license is GNU General Public License v3.0, see [COPYING](COPYING).
|
|
|
|
|
2019-06-12 06:44:59 -04:00
|
|
|
See [usage](docs/usage.md) for requirements, installation instructions and a usage guide.
|
2019-06-11 18:20:28 -04:00
|
|
|
|
2019-06-08 17:10:47 -04:00
|
|
|
<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]):
|
2019-08-18 09:26:14 -04:00
|
|
|
name = sorted(index[type])[i]
|
2019-06-08 17:10:47 -04:00
|
|
|
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:])
|