Пример #1
0
def gallery(force):
    if not os.path.isdir(target_dir):
        os.makedirs(target_dir)

    paths = sorted([
        pdir + '/' + i for pdir in project_dirs for i in os.listdir(pdir)
        if os.path.isdir(pdir + '/' + i + '/assemblies')
    ],
                   key=lambda s: os.path.basename(s))
    with open(output_name, 'wt') as output_file:
        print("# A gallery of projects made with NopSCADlib", file=output_file)
        for path in paths:
            project = os.path.basename(path)
            print(project)
            document = path + '/readme.md'
            if force:
                os.system('cd %s && make_all' % path)
            if os.path.isfile(document):
                with open(document, 'rt') as readme:
                    for line in readme.readlines():
                        match = re.search(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)
                                tmp_name = 'tmp.png'
                                target_name = '%s/%s.png' % (target_dir,
                                                             project)
                                copyfile(path + '/' + file, tmp_name)
                                update_image(tmp_name, target_name)
                            else:
                                line = line.replace(image, '')
                        else:
                            match = re.match(r"^(#+).*$", line)
                            if match:
                                line = '#' + line
                        if line == '---\n' or line == '<span></span>\n':
                            break
                        if line != '<a name="TOP"></a>\n':
                            print(line[:-1], file=output_file)
            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)
Пример #2
0
def render(target, type):
    #
    # Make the target directory
    #
    top_dir = set_config(target, usage)
    target_dir = top_dir + type + 's'
    bom_dir = top_dir + 'bom'
    if not os.path.isdir(target_dir):
        os.makedirs(target_dir)
    #
    # Find all the parts
    #
    parts = bom_to_parts(bom_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([0, 146/255, 0]) 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"
            tmp_name = 'tmp.png'
            openscad.run(colour_scheme, "--projection=p",
                         "--imgsize=4096,4096", cam, render, "--autocenter",
                         "--viewall", "-o", tmp_name, png_maker_name)
            do_cmd((
                "magick " + tmp_name +
                " -trim -resize 280x280 -background %s -gravity Center -extent 280x280 -bordercolor %s -border 10 %s"
                % (background, background, tmp_name)).split())
            update_image(tmp_name, png_name)
            os.remove(png_maker_name)
Пример #3
0
def doc_scripts():
    doc_name = dir + '/readme.md'
    with open(doc_name, 'wt') as doc_file:
        print('''
# Python scripts
These are located in the `scripts` subdirectory, which needs to be added to the program search path.

They should work with both Python 2 and Python 3.

| Script  | Function  |
|:---|:---|''',
              file=doc_file)
        for file in os.listdir('scripts'):
            if file.endswith('.py'):
                blurb = ''
                with open(dir + '/' + file, 'rt') as f:
                    lines = f.readlines()
                for line in lines:
                    if line == "if __name__ == '__main__':\n":
                        break
                else:
                    continue
                for line in lines[1:]:
                    if line.startswith('#! '):
                        line = line.replace('~\n', '  \n')
                        blurb = blurb + line[3:-1]
                    if line.startswith("def "):
                        break
                if not blurb:
                    print("Missing description for", file)
                else:
                    print("| `%s` | %s |" % (file, blurb), file=doc_file)

    with open(dir + "/readme.html", "wt") as html_file:
        do_cmd(("python -m markdown -x tables " + doc_name).split(), html_file)

    with open("docs/usage.html", "wt") as html_file:
        do_cmd(("python -m markdown -x tables docs/usage.md").split(),
               html_file)
    #
    # Spell check
    #
    do_cmd(('codespell -L od ' + doc_name).split())
    do_cmd(('codespell -L od docs/usage.md').split())
Пример #4
0
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)
    bounds_fname = top_dir + 'stls/bounds.json'
    with open(bounds_fname) as json_file:
        bounds_map = json.load(json_file)
    #
    # Find all the assemblies
    #
    assemblies = bom_to_assemblies(bom_dir, bounds_map)
    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/printed'
    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 th 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(png_name, dname)
                                        changed = times.check_have_time(
                                            changed, png_name)
                                        tmp_name = 'tmp.png'
                                        if changed:
                                            print(changed)
                                            t = time.time()
                                            openscad.run(
                                                "-D$pose=1",
                                                "-D$explode=%d" % explode,
                                                colour_scheme,
                                                "--projection=p",
                                                "--imgsize=4096,4096",
                                                "--autocenter", "--viewall",
                                                "-d", dname, "-o", tmp_name,
                                                png_maker_name)
                                            times.add_time(png_name, t)
                                            do_cmd([
                                                "magick", tmp_name, "-trim",
                                                "-resize", "1004x1004",
                                                "-bordercolor", background,
                                                "-border", "10", tmp_name
                                            ])
                                            update_image(tmp_name, 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 "
                                                + background +
                                                " -gravity Center -extent 280x280 -bordercolor "
                                                + background + " -border 10 " +
                                                tmp_name).split())
                                            update_image(tmp_name, tn_name)
                                    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('<a name="TOP"></a>\n# %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, doc_file, first=True)
            #
            # Build TOC
            #
            print('## Table of Contents', file=doc_file)
            print('1. [Parts list](#Parts_list)', file=doc_file)
            for ass in flat_bom:
                name = ass["name"]
                cap_name = name.replace('_', ' ').title()
                print('1. [%s](#%s)' % (cap_name, name), file=doc_file)
            print(file=doc_file)
            eop(print_mode, doc_file)
            #
            # Global BOM
            #
            print('<a name="Parts_list"></a>\n## Parts list', file=doc_file)
            types = ["vitamins", "printed", "routed"]
            headings = {
                "vitamins": "vitamins",
                "printed": "3D printed parts",
                "routed": "CNC routed parts"
            }
            things = {}
            for t in types:
                things[t] = {}
            for ass in flat_bom:
                for t in types:
                    for thing in ass[t]:
                        if thing in things[t]:
                            things[t][thing] += ass[t][thing]
                        else:
                            things[t][thing] = ass[t][thing]
            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 t in types:
                if things[t]:
                    totals = {}
                    heading = headings[t][0:1].upper() + headings[t][1:]
                    print(('|  ' * len(flat_bom) + '| | **%s** |') % heading,
                          file=doc_file)
                    for thing in sorted(things[t],
                                        key=lambda s: s.split(":")[-1]):
                        for ass in flat_bom:
                            count = ass[t][thing] if thing in ass[t] else 0
                            print('| %s ' % pad(count if count else '.', 2, 1),
                                  file=doc_file,
                                  end='')
                            name = ass["name"]
                            if name in totals:
                                totals[name] += count
                            else:
                                totals[name] = count
                        print('|  %s | %s |' % (pad(things[t][thing], 2, 1),
                                                pad(thing.split(":")[-1], 2)),
                              file=doc_file)

                    grand_total = 0
                    for ass in flat_bom:
                        name = ass["name"]
                        total = totals[name] if name in totals else 0
                        print('| %s ' % pad(total if total else '.', 2, 1),
                              file=doc_file,
                              end='')
                        grand_total += total
                    print("| %s | %s |" %
                          (pad(grand_total, 2,
                               1), pad('Total %s count' % headings[t], 2)),
                          file=doc_file)

            print(file=doc_file)
            eop(print_mode, doc_file)
            #
            # Assembly instructions
            #
            for ass in flat_bom:
                name = ass["name"]
                cap_name = name.replace('_', ' ').title()

                if ass["count"] > 1:
                    print('<a name="%s"></a>\n## %d x %s' %
                          (name, ass["count"], cap_name),
                          file=doc_file)
                else:
                    print('<a name="%s"></a>\n## %s' % (name, 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 sorted(vitamins, key=lambda s: s.split(":")[-1]):
                        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)
                    keys = sorted(list(printed.keys()))
                    for i in range(len(keys)):
                        p = keys[i]
                        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 = 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)
                    print('\n', file=doc_file)

                routed = ass["routed"]
                if routed:
                    print("### CNC Routed parts", file=doc_file)
                    keys = sorted(list(routed.keys()))
                    for i in range(len(keys)):
                        r = keys[i]
                        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 = 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)
                    print('\n', file=doc_file)

                sub_assemblies = ass["assemblies"]
                if sub_assemblies:
                    print("### Sub-assemblies", file=doc_file)
                    keys = sorted(list(sub_assemblies.keys()))
                    for i in range(len(keys)):
                        a = keys[i]
                        print('%s %d x %s |' % ('\n|' if not (i % 3) else '',
                                                sub_assemblies[a], a),
                              file=doc_file,
                              end='')
                        if (i % 3) == 2 or i == len(keys) - 1:
                            n = (i % 3) + 1
                            print('\n|%s' % ('--|' * n), file=doc_file)
                            for j in range(n):
                                a = keys[i - n + j + 1].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)
                    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,
                    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, 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 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)
Пример #5
0
def render(target, type):
    #
    # Make the target directory
    #
    top_dir = set_config(target, usage)
    tmp_dir = mktmpdir(top_dir)
    target_dir = top_dir + type + 's'
    bom_dir = top_dir + 'bom'
    if not os.path.isdir(target_dir):
        os.makedirs(target_dir)
    #
    # Find all the parts
    #
    parts = bom_to_parts(bom_dir, type)
    #
    # Read the json bom to get the colours
    #
    bom_file = bom_dir + "/bom.json"
    with open(bom_file) as json_file:
        flat_bom = json.load(json_file)

    things = {'stl': 'printed', 'dxf': 'routed'}[type]
    colours = {}
    for ass in flat_bom:
        for part in ass[things]:
            obj = ass[things][part]
            if "colour" in obj:
                colours[part] = obj["colour"]
    #
    # 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)
    #
    # Render the parts
    #
    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 = tmp_dir + "/png.scad"
            pp1 = [0, 146 / 255, 0]
            colour = pp1
            if part in colours:
                colour = colours[part]
                if not '[' in colour:
                    colour = '"' + colour + '"'
            with open(png_maker_name, "w") as f:
                f.write('color(%s) import("%s");\n' %
                        (colour, reltmp(part_file, target)))
            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' or colour != pp1 else "--render"
            tmp_name = tmp_dir + '/' + part[:-4] + '.png'
            openscad.run("-o", tmp_name, png_maker_name, colour_scheme,
                         "--projection=p", "--imgsize=4096,4096", cam, render,
                         "--autocenter", "--viewall")
            do_cmd((
                "magick " + tmp_name +
                " -trim -resize 280x280 -background %s -gravity Center -extent 280x280 -bordercolor %s -border 10 %s"
                % (background, background, tmp_name)).split())
            update_image(tmp_name, png_name)
            os.remove(png_maker_name)
    #
    # Remove tmp dir
    #
    rmtmpdir(tmp_dir)
Пример #6
0
                # Find the previous tagged commit
                j = i + 1
                diff = ''
                while j < len(commits):
                    if commits[j].tag:
                        last_ver = tag_version(commits[j].tag)
                        diff = '[...](%s "diff with %s")' % (
                            url + '/compare/' + last_ver + '...' + ver,
                            last_ver)
                        break
                    j += 1

                # Print version info
                print('%s [%s](%s "show release") %s %s' %
                      ('#' * (level + 1), ver, url + '/releases/tag/' + ver,
                       type, diff),
                      file=file)

            # Print commits excluding merges

            if not c.comment.startswith(
                    'Merge branch') and not c.comment.startswith(
                        'Merge pull') and not re.match(r'U..ated ch.*log.*',
                                                       c.comment):
                print('* %s [`%s`](%s "show commit") %s %s\n' %
                      (c.date, c.hash[:7], url + '/commit/' + c.hash,
                       initials(c.author), fixup_comment(c.comment, url)),
                      file=file)
    do_cmd(('codespell -w -L od ' + filename).split())
Пример #7
0
def views(target, do_assemblies=None):
    done_assemblies = []
    #
    # Make the target directory
    #
    top_dir = set_config(target, usage)
    tmp_dir = mktmpdir(top_dir)
    target_dir = top_dir + 'assemblies'
    deps_dir = target_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)
    options.check_options(deps_dir)
    bounds_fname = top_dir + 'stls/bounds.json'
    with open(bounds_fname) as json_file:
        bounds_map = json.load(json_file)
    #
    # Find all the assemblies and remove any old views
    #
    assemblies = bom_to_assemblies(bom_dir, bounds_map)
    lc_assemblies = [ass.lower() for ass in assemblies]
    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
    main_assembly, main_file = bom.main_assembly(target)
    pngs = []
    for dir in source_dirs(bom_dir):
        if os.path.isdir(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):
                                lc_module = module.lower()
                                if lc_module in lc_assemblies:
                                    real_name = assemblies[lc_assemblies.index(
                                        lc_module)]
                                    #
                                    # Scrape the assembly instructions
                                    #
                                    for ass in flat_bom:
                                        if ass["name"] == real_name:
                                            zoomed = ass['zoomed']
                                            if not "blurb" in ass:
                                                ass["blurb"] = blurb.scrape_module_blurb(
                                                    lines[:line_no])
                                            break

                                    #
                                    # Run openscad on the created file
                                    #
                                    dname = deps_name(deps_dir, filename)
                                    for explode in [0, 1]:
                                        #
                                        # Generate png name
                                        #
                                        png_name = target_dir + '/' + real_name + '.png'
                                        if not explode:
                                            png_name = png_name.replace(
                                                '_assembly', '_assembled')
                                        pngs.append(png_name)

                                        if not do_assemblies or real_name in do_assemblies:
                                            changed = check_deps(
                                                png_name, dname)
                                            changed = times.check_have_time(
                                                changed, png_name)
                                            changed = options.have_changed(
                                                changed, png_name)
                                            tmp_name = tmp_dir + '/' + real_name + '.png'
                                            if changed:
                                                print(changed)
                                                #
                                                # make a file to use the module
                                                #
                                                png_maker_name = tmp_dir + '/png.scad'
                                                with open(png_maker_name,
                                                          "w") as f:
                                                    f.write(
                                                        "include <NopSCADlib/global_defs.scad>\n"
                                                    )
                                                    f.write(
                                                        "use <%s/%s>\n" %
                                                        (reltmp(dir, target),
                                                         filename))
                                                    f.write("%s();\n" % module)
                                                t = time.time()
                                                target_def = [
                                                    '-D$target="%s"' % target
                                                ] if target else []
                                                cwd_def = [
                                                    '-D$cwd="%s"' %
                                                    os.getcwd().replace(
                                                        '\\', '/')
                                                ]
                                                view_def = [
                                                    '--viewall', '--autocenter'
                                                ] if not (
                                                    zoomed & (1 << explode)
                                                ) else [
                                                    '--camera=0,0,0,55,0,25,140'
                                                ]
                                                openscad.run_list(
                                                    [
                                                        "-o", tmp_name,
                                                        png_maker_name
                                                    ] + options.list() +
                                                    target_def + cwd_def +
                                                    view_def + [
                                                        "-D$pose=1",
                                                        "-D$explode=%d" %
                                                        explode, colour_scheme,
                                                        "--projection=p",
                                                        image_size, "-d", dname
                                                    ])
                                                times.add_time(png_name, t)
                                                do_cmd([
                                                    "magick", tmp_name,
                                                    "-trim", "-resize",
                                                    "1004x1004",
                                                    "-bordercolor", background,
                                                    "-border", "10", tmp_name
                                                ])
                                                update_image(
                                                    tmp_name, png_name)
                                                os.remove(png_maker_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 "
                                                    + background +
                                                    " -gravity Center -extent 280x280 -bordercolor "
                                                    + background +
                                                    " -border 10 " +
                                                    tmp_name).split())
                                                update_image(tmp_name, tn_name)
                                    done_assemblies.append(real_name)
                                else:
                                    if module == main_assembly:
                                        main_blurb = blurb.scrape_module_blurb(
                                            lines[:line_no])
                        line_no += 1
    #
    # Build the document
    #
    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('<a name="TOP"></a>', file=doc_file)
        print('# %s' % project, file=doc_file)
        text = blurb.scrape_blurb(source_dir + '/' + main_file)
        blurbs = blurb.split_blurb(text)
        if len(text):
            print(blurbs[0], file=doc_file)
        else:
            print(Fore.MAGENTA + "Missing project description" + Fore.WHITE)
        #
        # Only add the image if the first blurb section doesn't contain one.
        #
        if not re.search(r'\!\[.*\]\(.*\)', blurbs[0], re.MULTILINE):
            print('![Main Assembly](assemblies/%s.png)\n' %
                  flat_bom[-1]["name"].replace('_assembly', '_assembled'),
                  file=doc_file)
        eop(doc_file, first=True)
        #
        # Build TOC
        #
        print('## Table of Contents', file=doc_file)
        print('1. [Parts list](#Parts_list)', file=doc_file)
        for ass in flat_bom:
            name = ass["name"]
            cap_name = titalise(name)
            print('1. [%s](#%s)' % (cap_name, name), file=doc_file)
        print(file=doc_file)
        if len(blurbs) > 1:
            print(blurbs[1], file=doc_file)
        eop(doc_file)
        #
        # Global BOM
        #
        global_bom = [merged(ass) for ass in flat_bom if not ass['ngb']]
        print('<a name="Parts_list"></a>\n## Parts list', file=doc_file)
        headings = {
            "vitamins": "vitamins",
            "printed": "3D printed parts",
            "routed": "CNC routed parts"
        }
        things = {}
        for t in types:
            things[t] = {}
        for ass in flat_bom:
            for t in types:
                for thing in ass[t]:
                    if thing in things[t]:
                        things[t][thing] += ass[t][thing]["count"]
                    else:
                        things[t][thing] = ass[t][thing]["count"]
        for ass in global_bom:
            name = titalise(ass["name"][:-9]).replace(' ', '&nbsp;')
            if ass["count"] > 1:
                name = "%d x %s" % (ass["count"], name)
            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(global_bom) + '|---:|:---|'), file=doc_file)

        for t in types:
            if things[t]:
                totals = {}
                grand_total2 = 0
                heading = headings[t][0].upper() + headings[t][1:]
                print(('|  ' * len(global_bom) + '| | **%s** |') % heading,
                      file=doc_file)
                for thing in sorted(things[t], key=lambda s: s.split(":")[-1]):
                    for ass in global_bom:
                        count = ass[t][thing]["count"] if thing in ass[t] else 0
                        print('| %s ' % pad(count if count else '.', 2, 1),
                              file=doc_file,
                              end='')
                        name = ass["name"]
                        if name in totals:
                            totals[name] += count
                        else:
                            totals[name] = count
                        grand_total2 += count
                    print('|  %s | %s |' % (pad(
                        things[t][thing], 2, 1), pad(thing.split(":")[-1], 2)),
                          file=doc_file)

                grand_total = 0
                for ass in global_bom:
                    name = ass["name"]
                    total = totals[name] if name in totals else 0
                    print('| %s ' % pad(total if total else '.', 2, 1),
                          file=doc_file,
                          end='')
                    grand_total += total
                print("| %s | %s |" % (pad(grand_total, 2, 1),
                                       pad('Total %s count' % headings[t], 2)),
                      file=doc_file)
                assert grand_total == grand_total2
        print(file=doc_file)
        if len(blurbs) > 2:
            print(blurbs[2], file=doc_file)
        eop(doc_file)
        #
        # Assembly instructions
        #
        for ass in flat_bom:
            name = ass["name"]
            cap_name = titalise(name)

            print('<a name="%s"></a>' % name, file=doc_file)
            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 sorted(vitamins, key=lambda s: s.split(":")[-1]):
                    print("|%d|%s|" % (vitamins[v]["count"], v.split(":")[1]),
                          file=doc_file)
                print("\n", file=doc_file)

            printed = ass["printed"]
            if printed:
                print('### 3D Printed parts', file=doc_file)
                keys = sorted(list(printed.keys()))
                for i, p in enumerate(keys):
                    print(
                        '%s %d x %s |' %
                        ('\n|' if not (i % 3) else '', printed[p]["count"], 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 = 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)
                print('\n', file=doc_file)

            routed = ass["routed"]
            if routed:
                print("### CNC Routed parts", file=doc_file)
                keys = sorted(list(routed.keys()))
                for i, r in enumerate(keys):
                    print(
                        '%s %d x %s |' %
                        ('\n|' if not (i % 3) else '', routed[r]["count"], 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 = 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)
                print('\n', file=doc_file)

            sub_assemblies = ass["assemblies"]
            if sub_assemblies:
                print("### Sub-assemblies", file=doc_file)
                keys = sorted(list(sub_assemblies.keys()))
                for i, a in enumerate(keys):
                    print('%s %d x %s |' %
                          ('\n|' if not (i % 3) else '', sub_assemblies[a], a),
                          file=doc_file,
                          end='')
                    if (i % 3) == 2 or i == len(keys) - 1:
                        n = (i % 3) + 1
                        print('\n|%s' % ('---|' * n), file=doc_file)
                        for j in range(n):
                            a = keys[i - n + j + 1].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)
                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:
                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(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(doc_file, last=True)
    #
    # Convert to HTML
    #
    html_name = top_dir + 'readme.html'
    t = time.time()
    with open(html_name, "wt") as html_file:
        do_cmd(
            ("python -m markdown -x tables -x sane_lists " + doc_name).split(),
            html_file)
    times.add_time(html_name, t)
    times.print_times(pngs + [html_name])
    #
    # Make the printme.html by replacing empty spans that invisibly mark the page breaks by page break divs.
    #
    with open(html_name, 'rt') as src:
        lines = src.readlines()

    i = 0
    with open(top_dir + 'printme.html', 'wt') as dst:
        while i < len(lines):
            line = lines[i]
            if line.startswith(
                    '<p><span></span>'):  # Empty span used to mark page breaks
                i += 1
                if lines[i].startswith(
                        '<a href="#TOP">Top</a>'
                ):  # The first page break won't have one
                    i += 1
                if i < len(lines) and lines[
                        i] == '<hr />\n':  # The last page break doesn't have one
                    dst.write(
                        '<div style="page-break-after: always;"></div>\n')
                    i += 1
            else:
                dst.write(line)
                i += 1
    #
    # Remove tmp dir
    #
    rmtmpdir(tmp_dir)
    #
    # Spell check
    #
    do_cmd(('codespell -L od ' + top_dir + '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)