singe/util/plaqueModel.py

75 lines
3.9 KiB
Python

# Builds assets/Plaque.obj (+ .mtl) and one STL per colour: a single printable plaque with the Singe
# logotype raised on a backing plate and the dragon lying above the word as a bas-relief (its top
# half, every part filled down to the plate). It prints face up with no supports: the plate is the
# XY plane and Z is up. Five bodies: dark, champagne and glass tiles, the frame (dragon cores, walls,
# grout and letter walls) and the plate, for a four-colour printer with the plate sharing a filament.
# Usage: python3 util/plaqueModel.py pieces.json assets/SingeText.jpeg assets/Plaque.obj [--width 160] [--gap 10] [--stl assets] [--check]
import argparse
import json
import numpy as np
from objWriter import ObjWriterT, DRAGON_MATERIALS
from outline import traceBoundary, simplifyClean, signedArea
from dragonModel import DragonT
from textModel import LogoT, UPSCALE, PLATE_MARGIN, plateMask, writeOutputs
parser = argparse.ArgumentParser()
parser.add_argument('pieces')
parser.add_argument('logo')
parser.add_argument('out')
parser.add_argument('--scale', type=float, default=0.01, help='model units per image pixel')
parser.add_argument('--gap', type=float, default=10.0, help='space between the letter tops and the dragon, image pixels')
parser.add_argument('--stl', default=None, help='directory to write one STL per body into')
parser.add_argument('--width', type=float, default=160.0, help='width of the plaque in the STL files, millimetres')
parser.add_argument('--check', action='store_true', help='voxelise the model and count separate pieces')
args = parser.parse_args()
scale = args.scale
logo = LogoT(args.logo, scale)
dragon = DragonT(json.load(open(args.pieces)), scale, printable=True, relief=True)
# Layout: the word rests on Y = 0, the dragon's feet a gap above the tallest letter, both centred on X.
letterTop = max(logo.toModel(x, y, 0.0)[1] for ring in logo.letters for x, y in ring)
lift = np.array([0.0, letterTop + args.gap * scale, 0.0])
def raise_(p):
return p + lift
# One plate under both: compose the logo ink and the dragon footprint on a common canvas at one
# pixel per model pixel, then outline it the way the logo plate is outlined.
canvasW = 2400
canvasH = 2600
cx = canvasW // 2
cy = canvasH - 400
canvas = np.zeros((canvasH, canvasW), dtype=bool)
def paste(mask, shiftX, shiftY):
h, w = mask.shape
canvas[shiftY:shiftY + h, shiftX:shiftX + w] |= mask
ink = logo.ink.reshape(logo.ink.shape[0] // UPSCALE, UPSCALE, logo.ink.shape[1] // UPSCALE, UPSCALE).max(axis=(1, 3))
paste(ink, int(round(cx - (logo.x0 + logo.x1) / 2)), int(round(cy - logo.y1)))
paste(dragon.footprint(), int(round(cx - (dragon.x0 + dragon.width / 2))), int(round(cy - dragon.y1 - lift[1] / scale)))
plate = plateMask(canvas, PLATE_MARGIN, 20)
ring = simplifyClean(traceBoundary(plate), 2.0)
# Canvas pixels back to logo image pixels, so the logo's own mapping places the plate.
ring = ring - np.array([cx - (logo.x0 + logo.x1) / 2, cy - logo.y1])
xy = np.array([logo.toModel(x, y, 0.0)[:2] for x, y in ring])
if signedArea(xy) < 0:
ring = ring[::-1]
writer = ObjWriterT()
logo.buildPlateRing(writer, ring, 0.0)
logo.buildLetters(writer, True, 0.0)
dragon.build(writer, raise_)
bodies = {name: name for name in ('dark', 'champagne', 'glass', 'frame', 'plate')}
materials = {name: DRAGON_MATERIALS[name] for name in bodies}
plateWidth = (ring[:, 0].max() - ring[:, 0].min()) * scale
print('%s: %d vertices, %d faces; plate %.2f x %.2f units' % (args.out, len(writer.vertices), writer.faceCount, plateWidth, (ring[:, 1].max() - ring[:, 1].min()) * scale))
header = ['Singe plaque: the logotype and the dragon in relief on one plate, built by util/plaqueModel.py',
'Units: %g per image pixel; plate in the XY plane resting on Y = 0, Z up (print orientation), centred on X = 0' % scale]
writeOutputs(writer, args.out, header, materials, bodies, args.stl, args.width, plateWidth, args.check, scale)