"""Build the canister station after the user's picture (reference/ref_user.png), headless: blender.exe -b --factory-startup --python cs_build.py -> ../CanisterStation.blend (rebuilt from scratch) The picture is one product shot. Its camera (cs_lib.py CAM) was estimated from three things that do not depend on depth: the canister cap's ellipse (minor/major about 0.22), the hub's ellipse (about 0.94) and the foot plate's end edge (it recedes up and to the right): about 10 degrees off the side, 8 degrees from above, a long lens (100 mm at 5 m; 1 px is about 1.5 mm at the machine's middle plane). Parts are traced in the picture's pixels (u, v) from the ruler crops reference/z3_*.png and put on the plane of their front face; depths are chosen (the picture has none): y -0.17 .. 0.17 the black frame (tower and base); green panels 2 cm proud, cream strips 3.5 cm y -0.20 .. -0.07 the green box under the screen; the screen column's core -0.15 .. 0.15 y 0 the canister's axis (radius 0.20, 46 degrees up); the piston's axis at y -0.27 The canister is its own object: it slides out of its drum along its axis (one slider CanisterStation_Root["eject"]). """ import bpy, bmesh, math, os, sys, json from mathutils import Vector, Matrix HERE = os.path.dirname(os.path.abspath(__file__)) FOLDER = os.path.dirname(HERE) exec(open(os.path.join(HERE, "cs_lib.py")).read()) # the camera: the ground under the green box's front edge shows at v 1244; x 0 under the base's footprint middle fit_ground(700, 1244, -0.20) _x0 = CAM.on_y(471, 1244, 0.0).x CAM = Camera(CAM.T.z, -_x0) BODY_MATS = ["M_CS_Black", "M_CS_Green", "M_CS_Grey", "M_CS_Cream", "M_CS_Steel", "M_CS_Chrome", "M_CS_Rubber", "M_CS_Gap", "M_CS_Screen", "M_CS_PanelDark"] CAN_MATS = ["M_CS_Green", "M_CS_Black", "M_CS_Cream", "M_CS_Gap", "M_CS_Glow", "M_CS_Decal"] YC = 0.17 # half depth of the black frame YG0, YG1 = -0.19, -0.165 # green panels YW0, YW1 = -0.205, -0.15 # cream strips and plates # ------------------------------------------------------------------ the canister's axis. Scanned in the picture across # the canister at 1 px steps: its lower edge (against the white ground) runs at 46.5 degrees, 268 px from the upper # edge near the cap; the axis is that edge moved 134 px up-left, the ring's centre on it (972, 363) AX0 = CAM.on_y(972.4, 363.3, 0.0) # the cap ring's centre, t = 0 AX1 = CAM.on_y(637.0, 716.7, 0.0) CA = (AX0 - AX1).normalized() # up the canister CD1 = (Vector((0, 0, 1)) - CA * CA.z).normalized() # angle 0: the canister's top side (up-left in the picture) CD2 = CA.cross(CD1).normalized() # angle 90: its near side (toward the viewer, -y) R_CAN = 0.200 T_BOTTOM = -0.935 # the canister's bottom end (inside the drum) EJECT = 0.32 # m it slides out def AXP(t, th=0.0, r=0.0): return AX0 + CA * t + r * (math.cos(th) * CD1 + math.sin(th) * CD2) def along(t, p): """(x, z) of the point t along the axis, p off it toward the top (in the axis's own plane y 0)""" q = AX0 + CA * t + CD1 * p return (q.x, q.z) def at_p(x, p): """(x, z) on the line parallel to the axis at p off it (plane y 0), at world x""" q0 = AX0 + CD1 * p t = (x - q0.x) / CA.x q = q0 + CA * t return (q.x, q.z) # ------------------------------------------------------------------ outlines traced in the picture (pixels) LEFT_MASS = [(233, 268), (300, 243), (373, 230), (443, 220), (492, 224), (482, 385), (482, 420), (480, 495), (462, 515), (462, 640), (440, 668), (405, 700), (405, 890), (480, 905), (560, 960), (612, 975), (612, 1240), (158, 1240), (158, 985), (243, 985), (243, 338), (220, 313)] HEAD = [(447, 217), (480, 212), (528, 232), (545, 257), (545, 368), (535, 376), (510, 392), (478, 378), (466, 350)] TOWER_PANEL = [(251, 273), (395, 248), (407, 255), (407, 310), (390, 327), (350, 323), (325, 400), (325, 590), (250, 585)] TOWER_BOLTS = [(255, 292), (375, 270), (258, 397), (296, 392), (257, 557), (294, 552)] BRACKET = [(203, 280), (222, 280), (222, 560), (203, 560)] BRACKET_IN = [(222, 330), (243, 330), (243, 560), (222, 560)] BRACKET_BITS = [[(225, 358), (240, 358), (240, 375), (225, 375)], [(226, 405), (240, 405), (240, 440), (226, 440)]] BRACKET_PIN = [(232, 500), (258, 500), (263, 512), (258, 525), (232, 525)] TAB = [(428, 428), (468, 432), (470, 465), (430, 462)] COLUMN = [(330, 332), (352, 326), (392, 330), (392, 840), (330, 840)] # raised black column, strips on its edge BULGE = [(400, 385), (482, 385), (482, 420), (480, 495), (462, 515), (400, 505)] # the block under the head EDGE_STRIPS = [[(383, 527), (393, 527), (393, 600), (383, 600)], [(383, 717), (393, 717), (393, 760), (383, 760)]] FITTING = (450, 535) PISTON_TOP, PISTON_LOW, Y_PISTON = (178, 597), (285, 760), -0.27 LINK_PANEL_HEAD = [(262, 830), (350, 828), (470, 962)] LINK_PANEL_TAIL = [(320, 1218), (320, 1000), (262, 960)] HUB_C, HUB_R = (480, 1112), 112 # its lowest ring clear of the ground BASE_BOLTS = [(276, 848), (322, 847), (346, 978), (414, 993), (344, 1140)] WEDGE = [(160, 1005), (200, 1005), (205, 925), (188, 910), (160, 985)] STEP = [(205, 1122), (250, 1120), (282, 1150), (282, 1162), (207, 1162)] CHANNEL = [(210, 1162), (350, 1157), (350, 1232), (330, 1232), (330, 1177), (210, 1180)] FITTING_BAR = [(277, 1050), (308, 1050), (308, 1120), (277, 1120)] BASE_LEFT_PANEL = [(166, 1012), (238, 1006), (246, 1112), (170, 1116)] GREEN_BOX = [(700, 950), (845, 1022), (845, 1240), (700, 1240)] GREEN_BOX_IN = [(758, 1060), (838, 1060), (838, 1222), (758, 1222)] PILLAR = [(612, 925), (628, 925), (640, 945), (640, 1240), (604, 1240), (604, 1085), (612, 1075)] PLATE = [(647, 1007), (685, 1007), (712, 1037), (712, 1237), (647, 1237)] SCREEN_MOD = [(688, 812), (705, 795), (800, 795), (800, 950), (688, 950)] SCREEN_GLASS = [(705, 795), (790, 795), (790, 880), (705, 880)] SCREEN_LOW = [(700, 884), (790, 884), (790, 946), (700, 946)] HANDLE_L = ([(677, 795), (690, 790), (700, 800), (700, 942), (690, 950), (677, 945)], [(683, 812), (690, 808), (694, 815), (694, 930), (690, 935), (683, 932)]) HANDLE_R = ([(800, 822), (812, 820), (822, 830), (822, 940), (812, 948), (800, 945)], [(806, 838), (812, 835), (816, 842), (816, 930), (812, 934), (806, 931)]) # the canister along its axis: (r, t) from the bottom end to the cap's recessed face; t 0 is the body / ring joint CAN_PROF = [(0.168, T_BOTTOM), (0.192, T_BOTTOM), (0.200, T_BOTTOM + 0.008), (0.200, -0.006), (0.197, -0.003), (0.193, -0.003), (0.193, 0.000), (0.2015, 0.000), (0.2015, 0.020), (0.193, 0.020), (0.193, 0.024), (0.203, 0.026), (0.208, 0.031), (0.208, 0.080), (0.204, 0.088), (0.196, 0.094), (0.170, 0.096), (0.166, 0.092), (0.160, 0.086), (0.152, 0.086), (0.152, 0.093), (0.146, 0.097), (0.134, 0.097), (0.128, 0.093), (0.128, 0.086), (0.122, 0.080)] CAN_MATS_SEG = (["M_CS_Green"] * 6 + ["M_CS_Gap", "M_CS_Glow", "M_CS_Gap", "M_CS_Gap"] + ["M_CS_Black"] * 8 + ["M_CS_Cream"] * 6 + ["M_CS_Black"]) GLOW_SEG = 7 # the profile segment that is the glow band LATCH_ANGLES = [math.radians(a) for a in (45, 135, 225, 315)] # the cup the canister's foot stands in (cream, a dark lip): scanned along the axis, the picture's cream runs from # 590 to 700 px down it, with a dark seam where the green body meets it; its lower side is behind the screen column. # The canister's foot goes 5 cm into it, 4.5 mm clear of the bore and 5 mm over its floor. DRUM_PROF = [(0.120, -1.070), (0.190, -1.070), (0.212, -1.050), (0.212, -0.900), (0.210, -0.893), (0.210, -0.888), (0.207, -0.885), (0.2045, -0.885), (0.2045, -0.940)] DRUM_MATS = ["M_CS_Cream"] * 3 + ["M_CS_Black"] * 4 + ["M_CS_Gap"] RAIL_S = (-0.84, -0.33) # the cradle rail along the axis (m from the ring) CLAMPS_T = [(-0.840, -0.770), (-0.475, -0.405)] SUPPORT_T = (-0.760, -0.600) # the screen column's cradle under the canister # ------------------------------------------------------------------ parts def build_body(): mb = MB() # the black frame: tower and base in one outline prism_px(mb, LEFT_MASS, -YC, YC, "M_CS_Black", ch=0.010) prism_px(mb, HEAD, -0.150, 0.150, "M_CS_Grey", ch=0.016) # the side's detail is traced on the near side and copied onto the far side (the picture shows one side; the # machine is taken to be the same on both): panels, strips, bracket, piston, cable, hub, the base's fittings, # the green box, the cream pillar and plate. The screen, its handles and the saddle stay on the near side only. mk = mb.mark() prism_px(mb, TOWER_PANEL, YG0, YG1, "M_CS_Green", ch=0.006) prism_px(mb, COLUMN, -0.188, -0.160, "M_CS_Black", ch=0.007) prism_px(mb, BULGE, -0.192, -0.160, "M_CS_Black", ch=0.009) for u, v in TOWER_BOLTS + BASE_BOLTS: bolt(mb, Pw(u, v, YG0), Vector((0, -1, 0)), 0.0085, 0.006, "M_CS_Chrome") prism_px(mb, BRACKET, -0.200, -0.080, "M_CS_Cream", ch=0.004) prism_px(mb, BRACKET_IN, -0.190, -0.100, "M_CS_Cream", ch=0.004) for b in BRACKET_BITS: prism_px(mb, b, -0.205, -0.188, "M_CS_Cream", ch=0.003) prism_px(mb, BRACKET_PIN, -0.215, -0.150, "M_CS_Cream", ch=0.004) prism_px(mb, TAB, -0.205, -0.180, "M_CS_Cream", ch=0.004) for s in EDGE_STRIPS: prism_px(mb, s, -0.200, -0.175, "M_CS_Cream", ch=0.003) # the round fitting on the tower and its rod to the rail o = Pw(FITTING[0], FITTING[1], -YC) mp = CAM.m_per_px(FITTING[0], FITTING[1], -YC) lathe(mb, [(22 * mp, -0.002), (22 * mp, 0.012), (19 * mp, 0.016), (13 * mp, 0.016), (13 * mp, 0.010), (8 * mp, 0.010), (8 * mp, 0.030), (6 * mp, 0.034)], o, (0, -1, 0), 16, ["M_CS_Chrome"] * 4 + ["M_CS_Gap", "M_CS_Black", "M_CS_Black"], cap1=True) a, b = Pw(468, 537, -0.185), Pw(505, 537, -0.185) lathe(mb, [(0.008, 0.0), (0.008, (b - a).length)], a, b - a, 8, "M_CS_Black", cap0=True, cap1=True) # piston on the tower's left pt, pl = Pw(*PISTON_TOP, Y_PISTON), Pw(*PISTON_LOW, Y_PISTON) pa = (pl - pt).normalized() # the top end is an open cream ring round a dark end plate, as in the picture lathe(mb, [(0.050, -0.010), (0.058, -0.024), (0.070, -0.036), (0.080, -0.034), (0.087, -0.024), (0.089, -0.010), (0.089, 0.010), (0.083, 0.022), (0.076, 0.026), (0.076, 0.262), (0.068, 0.290), (0.052, 0.302)], pt, pa, 24, ["M_CS_Black", "M_CS_Gap"] + ["M_CS_Cream"] * 6 + ["M_CS_Green"] * 3, cap0=True, cap1=True) # cables c1 = [(197, 455, -0.13), (193, 500, -0.15), (186, 545, -0.17), (170, 590, -0.19), (152, 650, -0.20), (150, 700, -0.20), (155, 760, -0.20), (170, 815, -0.20), (192, 880, -0.20), (205, 950, -0.195), (212, 1000, -0.15)] tube(mb, [Pw(u, v, y) for u, v, y in c1], 0.009, 8, "M_CS_Rubber") # base: foot plate, wedge, the green side panel round the hub, fittings prism_px(mb, WEDGE, -0.200, -0.100, "M_CS_Steel", ch=0.004) arc = [(HUB_C[0] + (HUB_R + 6) * math.cos(math.radians(a)), HUB_C[1] - (HUB_R + 6) * math.sin(math.radians(a))) for a in range(98, 232, 12)] prism_px(mb, LINK_PANEL_HEAD + arc + LINK_PANEL_TAIL, YG0, YG1, "M_CS_Green", ch=0.006) fo = Pw(292, 1050, -0.200) mp = CAM.m_per_px(292, 1025, -0.200) lathe(mb, [(0.0, 0.0), (17 * mp, 0.0), (17 * mp, 50 * mp), (14 * mp, 53 * mp), (6 * mp, 53 * mp), (6 * mp, 70 * mp), (0.0, 70 * mp)], fo, (0, 0, 1), 16, ["M_CS_Chrome"] * 4 + ["M_CS_Black"] * 2) prism_px(mb, FITTING_BAR, YG0, YG1, "M_CS_Green", ch=0.004) prism_px(mb, BASE_LEFT_PANEL, YG0, YG1, "M_CS_Green", ch=0.005) prism_px(mb, STEP, -0.200, -0.100, "M_CS_Cream", ch=0.004) prism_px(mb, CHANNEL, -0.195, -0.120, "M_CS_Steel", ch=0.004) # hub: stepped rings and the cream disc with its key ho = Pw(HUB_C[0], HUB_C[1], -YC) mp = CAM.m_per_px(HUB_C[0], HUB_C[1], -YC) hub = [(112, 0), (112, 8), (108, 16), (104, 18), (102, 12), (100, 12), (98, 22), (90, 28), (84, 28), (82, 22), (72, 22), (68, 30), (63, 34), (62, 40), (58, 44), (0, 44)] hub_m = ["M_CS_Green"] * 3 + ["M_CS_Gap"] * 2 + ["M_CS_Green"] * 4 + ["M_CS_Black"] * 2 + ["M_CS_Steel"] * 4 lathe(mb, [(r * mp, t * 0.001) for r, t in hub], ho, (0, -1, 0), 24, hub_m, e1=(0, 0, 1)) key = [(452, 1072), (475, 1072), (478, 1085), (476, 1150), (470, 1162), (456, 1162), (450, 1150), (449, 1085)] prism_px(mb, key, -YC - 0.052, -YC - 0.040, "M_CS_Black", ch=0.003) # the screen column's side: green box, cream pillar and plate, the round button prism_px(mb, GREEN_BOX, -0.200, -0.070, "M_CS_Green", ch=0.008) prism_px(mb, GREEN_BOX_IN, -0.206, -0.195, "M_CS_Green", ch=0.003) tube(mb, [Pw(797, 1110, -0.214), Pw(845, 1110, -0.214)], 0.006, 8, "M_CS_Steel", n_per=1) prism_px(mb, PILLAR, YW0, YW1, "M_CS_Cream", ch=0.005) prism_px(mb, PLATE, -0.215, -0.160, "M_CS_Cream", ch=0.006) bo = Pw(608, 893, -0.17) lathe(mb, [(0.019, 0.0), (0.019, 0.012), (0.015, 0.018), (0.0, 0.018)], bo, (0, -1, 0), 16, "M_CS_Chrome") mb.mirror_from(mk) c3 = [(470, 640, -0.10), (440, 680, -0.12), (422, 750, -0.13), (420, 820, -0.13), (432, 880, -0.12), (460, 915, -0.10)] tube(mb, [Pw(u, v, y) for u, v, y in c3], 0.008, 8, "M_CS_Rubber") # the foot plate under the base's left end, one plate across the machine's depth x0 = CAM.on_y(82, 1237, -0.22).x x1 = CAM.on_y(350, 1237, -0.22).x prism(mb, [(x0, 0.0), (x1, 0.0), (x1, 0.014), (x0, 0.014)], -0.22, 0.22, "M_CS_Steel", ch=0.003) # the screen column: core under the canister, screen module, handles, saddle xa, xb = CAM.on_y(600, 1240, -0.15).x, CAM.on_y(790, 1240, -0.15).x top = [at_p(x, -0.250) for x in (xb, xa)] prism(mb, [(xa, 0.0), (xb, 0.0)] + top, -0.15, 0.15, "M_CS_Black", ch=0.008) prism_px(mb, SCREEN_MOD, -0.195, -0.148, "M_CS_Black", ch=0.006) # shallow: behind it the canister comes down prism_px(mb, SCREEN_GLASS, -0.200, -0.190, "M_CS_Black", ch=0.002, front_mat="M_CS_Screen") prism_px(mb, SCREEN_LOW, -0.200, -0.190, "M_CS_PanelDark", ch=0.003) for outer, inner in (HANDLE_L, HANDLE_R): frame_prism(mb, [XZ(u, v, -0.215) for u, v in outer], [XZ(u, v, -0.215) for u, v in inner], -0.215, -0.190, "M_CS_Cream", ch=0.003, hole_mat="M_CS_Gap", depth_hole=-0.195) # the saddle plate in front of the column's top: its top edge 6 mm under the canister at the plate's back xa, xb = CAM.on_y(693, 792, -0.21).x, CAM.on_y(790, 792, -0.21).x zb = CAM.on_y(693, 792, -0.21).z while at_p(xa, -0.140)[1] - zb < 0.03: # its left end starts 3 cm tall (hidden behind the canister) xa += 0.002 prism(mb, [(xa, zb), (xb, zb)] + [at_p(x, -0.140) for x in (xb, xa)], -0.21, -0.15, "M_CS_Cream", ch=0.004) # the column's cradle under the canister (black), clear of it by 4 mm curved_box(mb, AX0, CA, CD1, CD2, SUPPORT_T[0], SUPPORT_T[1], math.radians(130), math.radians(230), R_CAN + 0.004, 0.252, 12, "M_CS_Black") # drum the canister stands in (cream), its seat is the black frame's notch lathe(mb, DRUM_PROF, AX0, CA, 48, DRUM_MATS, cap0=True, cap1=True, e1=CD1) # cradle rail on the canister's top side, held by two clamps O = AX0 Yv = Vector((0, 1, 0)) rs0, rs1 = RAIL_S # the picture's rail, from its top edge down: a wide cream bar, a black channel with two cream rods, a lower # cream bar on the near side, then open gaps to the canister box_frame(mb, O, CA, CD1, Yv, [(rs0 - 0.02, 0.232), (rs1 + 0.02, 0.232), (rs1 + 0.02, 0.274), (rs0 - 0.02, 0.274)], -0.050, 0.050, "M_CS_Black", ch=0.004) box_frame(mb, O, CA, CD1, Yv, [(rs0, 0.272), (rs1, 0.272), (rs1, 0.314), (rs0, 0.314)], -0.068, 0.068, "M_CS_Cream", ch=0.006) box_frame(mb, O, CA, CD1, Yv, [(rs0 + 0.03, 0.226), (rs1 - 0.03, 0.226), (rs1 - 0.03, 0.250), (rs0 + 0.03, 0.250)], -0.090, -0.050, "M_CS_Cream", ch=0.004) for y0, y1 in ((-0.068, -0.050), (0.050, 0.068)): box_frame(mb, O, CA, CD1, Yv, [(rs0 + 0.03, 0.252), (rs1 - 0.03, 0.252), (rs1 - 0.03, 0.270), (rs0 + 0.03, 0.270)], y0, y1, "M_CS_Cream", ch=0.003) for (t0, t1) in CLAMPS_T: box_frame(mb, O, CA, CD1, Yv, [(t0, 0.222), (t1, 0.222), (t1, 0.320), (t0, 0.320)], -0.094, 0.094, "M_CS_Cream", ch=0.005) curved_box(mb, AX0, CA, CD1, CD2, t0, t1, math.radians(-38), math.radians(55), R_CAN + 0.004, 0.234, 14, "M_CS_Cream") # the black block behind the rail's upper end, and the cable from the upper clamp into it box_frame(mb, O, CA, CD1, Yv, [(-0.345, 0.205), (-0.200, 0.205), (-0.200, 0.290), (-0.345, 0.290)], 0.0, 0.135, "M_CS_Black", ch=0.008) c2 = [AXP(-0.455, 0.0, 0.300) + Vector((0, -0.03, 0)), AXP(-0.43, 0.0, 0.326), AXP(-0.38, 0.0, 0.340), AXP(-0.33, 0.0, 0.328), AXP(-0.300, 0.0, 0.300), AXP(-0.285, 0.0, 0.285) + Vector((0, 0.05, 0))] tube(mb, c2, 0.010, 8, "M_CS_Rubber") return mb def build_canister(): mb = MB() n = 48 def glow_uv(k, i): if k != GLOW_SEG: return None return [(i / n, 0.0), ((i + 1) / n, 0.0), ((i + 1) / n, 1.0), (i / n, 1.0)] lathe(mb, CAN_PROF, AX0, CA, n, CAN_MATS_SEG, cap0=True, cap1=True, e1=CD1, uvf=glow_uv) # latches over the ring: a dark pocket plate, the hinge block, the bar over the ring, the hook on the cap for th in LATCH_ANGLES: w = 0.15 curved_box(mb, AX0, CA, CD1, CD2, -0.082, -0.016, th - 0.21, th + 0.21, 0.2003, 0.2030, 6, "M_CS_Black") curved_box(mb, AX0, CA, CD1, CD2, -0.064, -0.022, th - w, th + w, 0.2030, 0.2270, 4, "M_CS_Cream") curved_box(mb, AX0, CA, CD1, CD2, -0.022, 0.054, th - 0.13, th + 0.13, 0.2100, 0.2230, 4, "M_CS_Cream") curved_box(mb, AX0, CA, CD1, CD2, 0.054, 0.072, th - 0.13, th + 0.13, 0.2090, 0.2260, 4, "M_CS_Cream") # the mechanism on the cap's recessed face: four cream teeth for k in range(4): th = math.radians(20 + 90 * k) curved_box(mb, AX0, CA, CD1, CD2, 0.080, 0.090, th - 0.18, th + 0.18, 0.090, 0.120, 3, "M_CS_Cream") # the radiation sign: a decal patch 0.6 mm off the surface where the picture has it (705, 570) best = None for ti in range(-700, -300, 4): t = ti / 1000.0 for ai in range(0, 181, 3): th = math.radians(ai) p = AXP(t, th, R_CAN) nrm = (math.cos(th) * CD1 + math.sin(th) * CD2) if nrm.dot(CAM.C - p) <= 0: continue u, v = CAM.project(p) d = (u - 705) ** 2 + (v - 570) ** 2 if best is None or d < best[0]: best = (d, t, th) _, td, thd = best nrm = math.cos(thd) * CD1 + math.sin(thd) * CD2 eth = -math.sin(thd) * CD1 + math.cos(thd) * CD2 # the angle's direction on the surface up = Vector((0, 0, 1)) eb = (up - nrm * up.dot(nrm)).normalized() # the sign's up: world up on the surface ea = eb.cross(nrm).normalized() # its right half, N = 0.080, 8 grid = {} for i in range(N + 1): for j in range(N + 1): a, b = -half + 2 * half * i / N, -half + 2 * half * j / N wv = ea * a + eb * b t, th = td + wv.dot(CA), thd + wv.dot(eth) / R_CAN grid[(i, j)] = mb.v(AXP(t, th, R_CAN + 0.0006)) for i in range(N): for j in range(N): ids = [grid[(i, j)], grid[(i + 1, j)], grid[(i + 1, j + 1)], grid[(i, j + 1)]] uv = [(i / N, j / N), ((i + 1) / N, j / N), ((i + 1) / N, (j + 1) / N), (i / N, (j + 1) / N)] mb.f(ids, "M_CS_Decal", uv) return mb, dict(decal_t=round(td, 4), decal_angle_deg=round(math.degrees(thd), 1)) # ------------------------------------------------------------------ objects def coll(name, parent=None): c = bpy.data.collections.get(name) or bpy.data.collections.new(name) p = parent or bpy.context.scene.collection if c.name not in [x.name for x in p.children]: p.children.link(c) return c def obj(name, me, col, origin=None): ob = bpy.data.objects.get(name) if ob is None: ob = bpy.data.objects.new(name, me) col.objects.link(ob) ob.data = me if origin is not None: me.transform(Matrix.Translation(-origin)) ob.location = origin me.shade_smooth() try: me.set_sharp_from_angle(angle=math.radians(32)) except Exception: pass return ob def main(): for o in list(bpy.data.objects): bpy.data.objects.remove(o) for m in list(bpy.data.meshes): bpy.data.meshes.remove(m) for c in list(bpy.data.collections): bpy.data.collections.remove(c) for n in set(BODY_MATS + CAN_MATS): if n not in bpy.data.materials: bpy.data.materials.new(n) sc = bpy.context.scene sc.unit_settings.system = 'METRIC' top = coll("CanisterStation") c_static, c_moving = coll("CS | static", top), coll("CS | moving", top) coll("CS | collision", top) mb = build_body() me, bad = mb.to_mesh("SM_CanisterStation_Body", BODY_MATS) body = obj("SM_CanisterStation_Body", me, c_static) cm, info = build_canister() me_c, bad_c = cm.to_mesh("SM_CanisterStation_Canister", CAN_MATS) can = obj("SM_CanisterStation_Canister", me_c, c_moving, origin=AXP(T_BOTTOM)) root = bpy.data.objects.new("CanisterStation_Root", None) root.empty_display_type = 'PLAIN_AXES' root.empty_display_size = 0.2 top.objects.link(root) body.parent = root can.parent = root # the picture's camera, kept in the file cam = bpy.data.objects.new("CS_PictureCamera", bpy.data.cameras.new("CS_PictureCamera")) coll("Studio").objects.link(cam) cam.location = CAM.C cam.rotation_euler = CAM.f.to_track_quat('-Z', 'Y').to_euler() cam.data.lens, cam.data.sensor_width, cam.data.sensor_fit = LENS, SENSOR, 'HORIZONTAL' sc.camera = cam sc.render.resolution_x, sc.render.resolution_y = RES rep = dict(camera=dict(location=[round(c, 4) for c in CAM.C], target=[round(c, 4) for c in CAM.T]), axis=dict(ring_centre=[round(c, 4) for c in AX0], direction=[round(c, 4) for c in CA], elevation_deg=round(math.degrees(math.asin(CA.z)), 2)), faces_not_made=dict(body=bad, canister=bad_c), decal=info, audit={o.name: audit(o) for o in (body, can)}) exec(open(os.path.join(HERE, "cs_rig.py")).read(), {"__name__": "cs_rig", "bpy": bpy, "CA": CA, "EJECT": EJECT}) path = os.path.join(FOLDER, "CanisterStation.blend") bpy.ops.wm.save_as_mainfile(filepath=path) with open(os.path.join(HERE, "build_report.json"), "w") as fh: json.dump(rep, fh, indent=1) print("BUILD OK", json.dumps(rep)) if __name__ == "__main__": main()