"""Build the nutrition vat after the user's picture (reference/ref_user.jpg, one pod from three sides), headless: blender.exe -b --factory-startup --python nv_build.py -> ../NutritionVat.blend (rebuilt from scratch) Numbers are pixels of the picture's middle view (the pod's right side, square on): u to the rear, v down, x across the pod (+x is the side the picture shows); 1 px = 4.62 mm (see nv_lib.py). Measured from the ruler crops reference/z_*.png: head u 408..563, v 70..111; a groove at v 111..112.5 over the lip lip the top bezel over the tank, u 408..493, v 112.5..148, its rear corner curving down (r 22) into the column column the C-frame beside the tank: u 470..493 down to v 274, then forward to u 448 and round (r 45) at the bottom; its inner side wraps the glass. The C-groove behind it (u 493..498) holds a vertical rod with pins at v 164 and 336 middle a raised panel u 498..535, its top along the diagonal (490,111)-(547,150), down to v 355 rear a plate u 539..563 with two pills (542..560 x 154..201, 207..300) tank axis u 471, two glass chambers r 44 (front at u 427) with rounded ends, v 148.6..220.4 and 232.4..379.4; mid band r 52 at v 219..233; bottom bezel r 52 at v 372..393; a glowing disc r 26 under the top lower the lower housing, front u 410, top v 393, its rear along the arm's inner edge down to the plinth; hex knobs (539,372) and (522,392) in round bezels; a slot (483..508, 408..418); a keypad on the front plinth u 400..637, v 470..503 (a dark toe under it); a rear block under the arm arm a C-shaped arm behind the pod: its centreline an arc about (480,330), r 122, from -62 to 59 degrees, then straight down and forward into the plinth; band 54 px, 60 px thick across; five windows canisters one at each rear corner, axis (x +-47, u 580), r 15, v 188..299, a valve under each, clamp rings ports six hose ports on the head's top round (x 0, u 495), r 45, tilted out 22 degrees handles a U handle out of the head's front left corner, a rail round the lower housing's front left corner hoses (optional) three out of each port, up and back over the head, down behind the arm to the floor The pod's width (110 px) is from the left view's head (93 px front face beside 83 px side face, about 32 degrees). The left view shows no arm, hoses or second canister and a shorter base; the model follows the middle and right views there (where the views disagree the middle, side view rules). """ 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, "nv_lib.py")).read()) HW = 55.0 # half width of the shell CW = 48.0 # half width of the core TANK_U, TANK_R = 471.0, 44.0 ARM_C, ARM_RC, ARM_HW = (480.0, 330.0), 122.0, 27.0 # the arm: centre and radius of its centreline, half its band ARM_T = 30.0 # half the arm's thickness across the pod ARM_WINS, ARM_WH = (-47.0, -22.0, 3.0, 28.0, 53.0), 6.5 # windows: middles and half lengths (degrees) CAN_X, CAN_U = 47.0, 580.0 # the canister's axis MATS = ["M_NV_Shell", "M_NV_ShellDark", "M_NV_Gap", "M_NV_Glass", "M_NV_Glow", "M_NV_Rubber", "M_NV_Chrome", "M_NV_Brass", "M_NV_Keypad", "M_NV_Grille", "M_NV_Decal", "M_NV_CanGlass"] ALL_MATS = MATS + ["M_NV_Fluid", "M_NV_Hose"] def prof_box(x_out, x_in, b, nb=3): """a closed slab: a cap at x_out with a bevel b, straight back to a sharp cap at x_in""" pr = prof_plate(x_out, x_in, b, nb) return pr def arm_inner(th): """a point of the arm's inner edge at th degrees (side px)""" t = math.radians(th) r = ARM_RC - ARM_HW return (ARM_C[0] + r * math.cos(t), ARM_C[1] + r * math.sin(t)) def arm_inner_v(v): """the arm's inner edge where it crosses height v (its rear half)""" r = ARM_RC - ARM_HW return (ARM_C[0] + math.sqrt(r * r - (v - ARM_C[1]) ** 2), v) def arm_inner_u(u): """height of the arm's inner edge at u (its lower half)""" r = ARM_RC - ARM_HW return ARM_C[1] + math.sqrt(r * r - (u - ARM_C[0]) ** 2) def arm_outer(th): t = math.radians(th) r = ARM_RC + ARM_HW return (ARM_C[0] + r * math.cos(t), ARM_C[1] + r * math.sin(t)) def capsule(v0, v1, r, r_end, wall): """a glass chamber's wall as a closed lathe profile: the outer surface from v0 down to v1 with rounded ends, then the inner surface back up (wall thick)""" def side(rr, a, b, rc): pts = [] for k in range(5): # top fillet t = math.radians(90 * k / 4) pts.append((r_end + (rr - r_end) * math.sin(t), a + rc * (1 - math.cos(t)))) for k in range(5): # bottom fillet t = math.radians(90 * (1 - k / 4)) pts.append((r_end + (rr - r_end) * math.sin(t), b - rc * (1 - math.cos(t)))) return pts rc = 8.0 outer = side(r, v0, v1, rc) inner = side(r - wall, v0 + wall, v1 - wall, rc - wall) return outer + inner[::-1] def hexnut(mb, o, R, h): """a hexagonal nut on the +x side at o: 12 points round (corners and edge middles), a chamfered top""" mb.begin(hint=(1, 0, 0)) def ring(x, rs): pts = [] for k in range(12): a = math.radians(30 * k) r = R * rs * (1.0 if k % 2 == 0 else math.cos(math.radians(30))) pts.append(mb.v(o + Vector((x * S, r * S * math.cos(a), r * S * math.sin(a))))) return pts r0, r1, r2 = ring(-0.5, 1.0), ring(h - 0.5, 1.0), ring(h, 0.88) mb.bridge(r0, r1, "M_NV_Shell") mb.bridge(r1, r2, "M_NV_Shell") mb.coons(r2, [0, 3, 6, 9], "M_NV_Shell") def sides(fn): """build a side part on +x and on -x""" fn(Frame(n=(1, 0, 0)), 1.0) fn(Frame(n=(-1, 0, 0)), -1.0) # ------------------------------------------------------------------ the shell def build_shell(mb): sh = "M_NV_Shell" # head: full width, the bottom left sharp (the lip groove under it, the wedge plates flush at the rear) head = rpoly_px([(408, 70), (563, 70), (563, 111), (408, 111)], [14, 12, 0, 0], [4, 4, 0, 0], [16, 4, 20, 4], [1, 1, 0, 1]) slab(mb, head, SIDE, prof_full(HW, 6, 3), sh) # lip: the top bezel over the tank, full width; its bottom's rear part meets the columns (jog at u 470) lip = rpoly_px([(408, 112.5), (493, 112.5), (493, 148), (470, 148), (408, 148)], [1.5, 22, 0, 0, 3], [2, 6, 0, 1, 2], [10, 2, 3, 9, 3], [1, 1, 0, 1, 1]) slab(mb, lip, SIDE, prof_full(HW + 0.5, 6, 3), sh, corners=[lip.mid(0), lip.mid(1), lip.mid(2), lip.mid(4)]) def column(fr, sg): up = rpoly_px([(470, 148), (493, 148), (493, 274), (470, 274)], [0, 0, 0, 0], [0, 0, 0, 0], [3, 14, 3, 14], [0, 1, 0, 1]) slab(mb, up, fr, prof_box(HW + 0.5, 46, 6), sh, hint=fr.n, skip=(0, 2)) lo = rpoly_px([(470, 274), (493, 274), (493, 386), (445, 386), (448, 296)], [0, 0, 45, 2, 4], [0, 0, 8, 2, 2], [5, 7, 1, 4, 3], [0, 1, 1, 1, 1]) n0 = len(mb.co) slab(mb, lo, fr, prof_box(HW + 0.5, 34, 6), sh, corners=[lo.start[0], lo.start[1], lo.mid(2), lo.start[3]], hint=fr.n) rc = TANK_R + 0.6 # its inner side wraps the glass, 0.6 px (2.8 mm) off it for i in range(n0, len(mb.co)): c = mb.co[i] if abs(abs(c.x) - 34 * S) < 1e-7: du = c.y / S + U0 - TANK_U if abs(du) < rc: c.x = math.copysign(max(34.0, math.sqrt(rc * rc - du * du)) * S, c.x) # side plates on the core wedge = rpoly_px([(490, 111), (563, 111), (563, 150), (547, 150)], [0, 0, 2, 0], [0, 0, 2, 0], [5, 8, 4, 9], [0, 1, 1, 0.35]) # a small bevel on the diagonal: the label sits on the flat slab(mb, wedge, fr, prof_plate(HW, CW, 6), sh, caps=(True, False), corners=[wedge.start[0], wedge.start[1], wedge.mid(2), wedge.start[3]], hint=fr.n, skip=(0,)) mid = rpoly_px([(498, 120), (535, 145), (535, 355), (498, 355)], [3, 3, 6, 6], [2, 2, 2, 2], [4, 24, 4, 24]) slab(mb, mid, fr, prof_plate(HW + 1, CW, 3), sh, caps=(True, False), hint=fr.n) rear = rpoly_px([(539, 152), (563, 152), (563, 357), (539, 357)], [3, 0, 0, 3], [2, 0, 0, 2], [3, 32, 3, 30], [0.5, 1, 0.5, 0.5]) slab(mb, rear, fr, prof_plate(HW, CW, 6), sh, caps=(True, False), corners=[rear.mid(0), rear.start[1], rear.start[2], rear.mid(3)], hint=fr.n) for (v0, v1, nv) in ((154, 201, 6), (207, 300, 12)): pill = rpoly_px([(542, v0), (560, v0), (560, v1), (542, v1)], [8.5] * 4, [4] * 4, [1, nv, 1, nv]) slab(mb, pill, fr, prof_plate(HW + 1.5, HW - 0.5, 1.2), sh, caps=(True, False), hint=fr.n) a0, a1 = (563.0, arm_inner_u(563.0)), arm_inner_v(393.0) low = rpoly_px([(480, 360), (563, 360), a0, arm_inner(35.3), a1, (446, 393)], [0, 3, 0, 0, 0, 0], [0, 2, 0, 0, 0, 0], [11, 2, 1, 1, 12, 5], [1, 1, 1, 1, 0, 0]) slab(mb, low, fr, prof_plate(HW, CW, 6), sh, caps=(True, False), corners=[low.start[0], low.mid(1), low.start[4], low.start[5]], hint=fr.n, skip=(4,)) sides(column) # core: the spine behind and round the tank, a U in plan extruded from v 111 to 393 (no caps: closed by the head, # the lower housing, the plates); its rear face 2 px inside the plates' yr, yf = Y(561), Y(TANK_U) cr = (TANK_R + 2) * S pts = [Vector((CW * S, yf)), Vector((CW * S, yr)), Vector((-CW * S, yr)), Vector((-CW * S, yf))] narc = 24 for j in range(narc + 1): # the cradle, from x -46 round the back to x +46 a = math.pi + math.pi * j / narc pts.append(Vector((math.cos(a) * cr, yf - math.sin(a) * cr))) mb.begin() rb = [mb.v((p.x, p.y, Z(376))) for p in pts] rt = [mb.v((p.x, p.y, Z(111))) for p in pts] mb.bridge(rb, rt, "M_NV_Shell") # below v 376 the arm passes behind the lower plates: the core's last part stops at u 549, a lid closes the step pts2 = [Vector((p.x, Y(549))) if abs(p.y - yr) < 1e-9 else p for p in pts] mb.begin() rb = [mb.v((p.x, p.y, Z(393))) for p in pts2] rt = [mb.v((p.x, p.y, Z(376))) for p in pts2] mb.bridge(rb, rt, "M_NV_Shell") mb.begin(hint=(0, 0, -1)) lid = [mb.v((x * S, Y(u), Z(376))) for (x, u) in ((CW, 549), (CW, 561), (-CW, 561), (-CW, 549))] mb.f(lid, "M_NV_Shell") # lower housing: full width, its top's rear part under the core and the lower plates (jog at u 446) e59 = arm_inner(59.0) foot = (e59[0] - (470 - e59[1]) / math.tan(math.radians(31.0)), 470) # the straight inner edge meets the plinth lh = rpoly_px([(410, 393), (446, 393), arm_inner_v(393.0), arm_inner(47.3), arm_inner(53.1), e59, foot, (410, 470)], [4, 0, 0, 0, 0, 0, 2, 5], [2, 1, 0, 0, 0, 0, 2, 2], [4, 13, 3, 3, 3, 13, 3, 7], [1, 0, 1, 1, 1, 1, 1, 1]) slab(mb, lh, SIDE, prof_full(HW, 6, 3), sh, corners=[lh.mid(0), lh.start[2], lh.start[5], lh.mid(7)], skip=(6,)) # plinth and its toe pl = rpoly_px([(400, 470), (637, 470), (637, 496), (400, 496)], [6, 8, 0, 0], [3, 3, 0, 0], [30, 2, 33, 3], [1, 1, 0, 1]) slab(mb, pl, SIDE, prof_full(62, 5, 3), sh, corners=[pl.mid(0), pl.mid(1), pl.start[2], pl.start[3]]) toe = rpoly_px([(404, 496), (633, 496), (633, 503), (404, 503)], [0, 0, 0, 0], [0, 0, 0, 0], [20, 1, 20, 1], [0, 1, 0, 1]) slab(mb, toe, SIDE, prof_full(58, 0.01, 1), "M_NV_ShellDark", skip=(0,)) # rear block under the ring o59 = arm_outer(59.0) foot_o = (o59[0] - (470 - o59[1]) / math.tan(math.radians(31.0)), 470) rbk = rpoly_px([foot_o, o59, arm_outer(50.0), arm_outer(41.0), arm_outer(33.0), (630, 424), (634, 470)], [0, 0, 0, 0, 0, 6, 0], [0, 0, 0, 0, 0, 3, 0], [3, 3, 3, 3, 4, 4, 11], [1, 1, 1, 1, 1, 1, 0]) slab(mb, rbk, SIDE, prof_full(ARM_T + 2, 3, 3), sh, corners=[rbk.start[0], rbk.start[2], rbk.mid(5), rbk.start[6]], skip=(6,)) # ------------------------------------------------------------------ the tank (static parts) def build_tank(mb): o = P(0, TANK_U, 0) ax = (0, 0, -1) # glass: a thin wall, closed for (v0, v1) in ((148.6, 220.4), (232.4, 379.4)): lathe(mb, capsule(v0, v1, 44.0, 34.0, 1.5), o, ax, 64, "M_NV_Glass", closed=True) # top collar with the glowing disc under it lathe(mb, [(47.5, 144), (47.5, 147.5), (46.5, 148.5), (36.5, 148.5), (27, 148.6), (26, 149.2)], o, ax, 64, ["M_NV_Shell"] * 3 + ["M_NV_Gap"] * 2, cap_end=True, cap_mat="M_NV_Glow") # mid band and a fine light under it lathe(mb, [(43.8, 219), (49, 219), (51, 220.4), (52, 223), (52, 229), (51, 231.6), (49, 233), (43.8, 233)], o, ax, 64, "M_NV_Shell", closed=True) lathe(mb, [(44.1, 233), (47.5, 233), (47.5, 234.6), (44.1, 234.6)], o, ax, 64, "M_NV_Glow", closed=True) # bottom bezel and its light at the glass lathe(mb, [(43.8, 393), (43.8, 376), (45, 373.6), (48, 372.4), (50.5, 373.4), (52, 376), (52, 393)], o, ax, 64, "M_NV_Shell") lathe(mb, [(44.1, 373.2), (46.2, 372.6), (46.2, 375.2), (44.1, 375.8)], o, ax, 64, "M_NV_Glow", closed=True) def build_fluid(mb): o = P(0, TANK_U, 0) lo, up = capsule(232.4, 379.4, 44.0, 34.0, 1.9), capsule(148.6, 220.4, 44.0, 34.0, 1.9) n = len(lo) // 2 # the inner halves of the chambers, bottom to top prof = lo[n:] + [(33.4, 233.0), (33.4, 220.0)] + up[n:] prof = [(r - 0.4, v) for (r, v) in prof] lathe(mb, prof, o, (0, 0, -1), 64, "M_NV_Fluid", cap_start=True, cap_end=True) # ------------------------------------------------------------------ the arm behind the pod def arm_samples(): """centreline samples (u, v), outward normal, angle: an arc about ARM_C from -62 to 59 degrees, then straight down and forward into the plinth""" angs = set() a = -62.0 while a < 59.0: angs.add(round(a, 3)) a += 3.0 angs.add(59.0) for w in ARM_WINS: for d in (-ARM_WH - 1.6, -ARM_WH, ARM_WH, ARM_WH + 1.6): angs.add(w + d) angs = sorted(angs) clean = [angs[0]] for x in angs[1:]: if x - clean[-1] > 0.5: clean.append(x) cu, cv = ARM_C out = [] for th in clean: t = math.radians(th) out.append(((cu + ARM_RC * math.cos(t), cv + ARM_RC * math.sin(t)), (math.cos(t), math.sin(t)), th)) (u, v), nrm, _ = out[-1] d = (-nrm[1], nrm[0]) for L in (14, 28, 42, 56, 70, 84): out.append(((u + d[0] * L, v + d[1] * L), nrm, None)) return out def build_arm(mb): sm = arm_samples() ts = [-ARM_HW, -ARM_HW + 2, -11.0, -9.0, 16.0, 18.0, ARM_HW - 2, ARM_HW] win = set() for i in range(len(sm) - 1): a, b = sm[i][2], sm[i + 1][2] if a is None or b is None: continue m = 0.5 * (a + b) if any(abs(m - w) < ARM_WH for w in ARM_WINS): win.add((i, 3)) cells = {(i, j): "M_NV_Shell" for i in range(len(sm) - 1) for j in range(len(ts) - 1) if (i, j) not in win} edge = set() for (i, j) in win: edge |= {(i, j), (i + 1, j), (i, j + 1), (i + 1, j + 1)} ch = 2.5 def F(i, j, sd): (u, v), (nu, nv), _ = sm[i] x = ARM_T if sd else -ARM_T if j in (0, len(ts) - 1) or (i, j) in edge: x = math.copysign(ARM_T - ch, x) return P(x, u + ts[j] * nu, v + ts[j] * nv) cell_solid(mb, len(sm) - 1, len(ts) - 1, cells, F) # ------------------------------------------------------------------ details def build_details(mb): # hose ports: six on the head's top round (x 0, u 495), r 45, tilted out 22 degrees for base, axis in port_frames(): prof = [(22.5, -6), (22.5, 0.6), (21.4, 2.2), (20.2, 3.4), (20.2, 6), (21.2, 6.6), (21.2, 8.6), (20.2, 9.2), (20.2, 11), (21.2, 11.6), (21.2, 13.6), (20.2, 14.2), (20.2, 18), (21.4, 19), (21.4, 23.2), (20, 24.6), (13, 24.6), (11.2, 23), (11.2, 9)] lathe(mb, prof, base, axis, 32, ["M_NV_Rubber"] * (len(prof) - 2) + ["M_NV_Gap"], cap_end=True) # knobs on the +x side for (u, v) in ((539, 372), (522, 392)): lathe(mb, [(12.5, -1), (12.5, 0.8), (11.8, 1.8), (10.6, 2.2), (9.4, 1.8), (8.8, 0.9), (8.6, 0.3)], P(HW, u, v), (1, 0, 0), 32, "M_NV_Shell") hexnut(mb, P(HW, u, v), 7.0, 2.4) # a rounded slot on the lower housing's +x side sl = rpoly_px([(483, 408), (508, 408), (508, 418), (483, 418)], [2.5] * 4, [2] * 4, [4, 1, 4, 1]) slab(mb, sl, SIDE, prof_plate(HW + 0.8, HW - 0.5, 0.6, 2), "M_NV_Shell", caps=(True, False), hint=(1, 0, 0)) # grille on the +x side, near the top gr = rpoly_px([(482, 80), (508, 80), (508, 87.5), (482, 87.5)], [3.5] * 4, [4] * 4, [4, 1, 4, 1]) slab(mb, gr, SIDE, prof_plate(HW + 0.5, HW - 0.5, 0.4, 2), "M_NV_Grille", caps=(True, False), hint=(1, 0, 0)) # the rod in each C-groove, with pins across the groove for sg in (1, -1): rod = [P(sg * 52.6, 495.5, 161), P(sg * 52.6, 495.5, 339)] tube(mb, rod, 2.2 * S, 16, "M_NV_Chrome", cap_start=True, cap_end=True) for v in (164, 336): tube(mb, [P(sg * 50.5, 492.0, v), P(sg * 50.5, 499.5, v)], 1.5 * S, 12, "M_NV_Chrome", cap_start=True, cap_end=True) # the canister's clamp rings, touching the rear face; 0.2 px (1 mm) round the canister's glass for v in (211.0, 253.0): lathe(mb, [(14.8, v), (17.6, v), (17.6, v + 6), (14.8, v + 6)], P(sg * CAN_X, CAN_U, 0), (0, 0, -1), 32, "M_NV_Chrome", closed=True) # keypad on the lower housing's front (x -38..-12, v 420..447) kp = rpoly_px([(-38, 503 - 447), (-12, 503 - 447), (-12, 503 - 420), (-38, 503 - 420)], [2] * 4, [2] * 4, [4, 4, 4, 4], frame='front') fr = Frame(o=(0, Y(410), 0), a=(1, 0, 0), b=(0, 0, 1), n=(0, -1, 0)) slab(mb, kp, fr, prof_plate(1.0, -0.5, 0.5, 2), "M_NV_Keypad", caps=(True, False), hint=(0, -1, 0)) for i in range(3): for j in range(3): x0, z0 = -34 + i * 7.0, 503 - 443 + j * 7.0 bt = rpoly_px([(x0, z0), (x0 + 5, z0), (x0 + 5, z0 + 5), (x0, z0 + 5)], [1] * 4, [2] * 4, [1, 1, 1, 1], frame='front') slab(mb, bt, fr, prof_plate(1.9, 0.6, 0.4, 2), "M_NV_Shell", caps=(True, False), hint=(0, -1, 0)) # a light strip along the lower housing's front, low (the picture's cyan line under the rail) ls = rpoly_px([(-48, 503 - 468.6), (48, 503 - 468.6), (48, 503 - 465.4), (-48, 503 - 465.4)], [1.4] * 4, [2] * 4, [16, 1, 16, 1], frame='front') slab(mb, ls, Frame(o=(0, Y(410), 0), a=(1, 0, 0), b=(0, 0, 1), n=(0, -1, 0)), prof_plate(0.6, -1.0, 0.3, 2), "M_NV_Glow", caps=(True, False), hint=(0, -1, 0)) # handles: a U out of the head's front left corner, a rail round the lower housing's front left corner d = Vector((-0.9, -1, 1.1)).normalized() A, B = P(-30, 408.5, 79), P(-55.5, 432, 79) L = 44 * S top = fillet_path([A - d * 0.01, A + d * L, B + d * L, B - d * 0.01], 9 * S, 6) tube(mb, top, 4 * S, 16, "M_NV_Chrome", cap_start=True, cap_end=True) rail = [P(34, 409.5, 452), P(34, 384, 462), P(-74, 384, 468), P(-74, 446, 462), P(-55.5, 446, 452)] tube(mb, fillet_path(rail, 12 * S, 6), 4 * S, 16, "M_NV_Chrome", cap_start=True, cap_end=True) def port_frames(): """the six hose ports on the head's top: (base point, axis), round (x 0, u 495), r 45, tilted out 22 degrees""" out = [] for k in range(6): ph = math.radians(30 + 60 * k) cx, cu = 45 * math.cos(ph), 495 + 45 * math.sin(ph) o = Vector((math.cos(ph), math.sin(ph), 0)) axis = (Vector((0, 0, 1)) * math.cos(math.radians(22)) + o * math.sin(math.radians(22))).normalized() out.append((P(cx * 0.92, cu, 70), axis)) return out def catmull(pts, n): """a Catmull-Rom curve through pts (Vectors), n samples per span""" ext = [pts[0] * 2 - pts[1]] + list(pts) + [pts[-1] * 2 - pts[-2]] out = [] for i in range(1, len(ext) - 2): p0, p1, p2, p3 = ext[i - 1], ext[i], ext[i + 1], ext[i + 2] for k in range(n): t = k / n out.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t ** 3)) out.append(pts[-1]) return out def resample(path, step): """points every step metres along a polyline""" d = [0.0] for a, b in zip(path, path[1:]): d.append(d[-1] + (b - a).length) n = max(2, int(d[-1] / step) + 1) out, j = [], 0 for k in range(n): t = d[-1] * k / (n - 1) while j < len(d) - 2 and d[j + 1] < t: j += 1 f = (t - d[j]) / max(1e-12, d[j + 1] - d[j]) out.append(path[j].lerp(path[j + 1], f)) return out def relax_hoses(paths, radii, bvh, fixed_head, iters=320, margin=1.6 * S): """push the hoses apart from each other and out of the solids (bvh), keep them smooth and their length; the first fixed_head points of each (in the port) stay""" from mathutils import kdtree rest = [[(b - a).length for a, b in zip(p, p[1:])] for p in paths] rmax = max(radii) for it in range(iters): allp = [(h, i) for h, p in enumerate(paths) for i in range(len(p))] kd = kdtree.KDTree(len(allp)) for n, (h, i) in enumerate(allp): kd.insert(paths[h][i], n) kd.balance() moves = [[Vector() for _ in p] for p in paths] for n, (h, i) in enumerate(allp): p = paths[h][i] for (co, m, dist) in kd.find_range(p, radii[h] + rmax + margin): g, j = allp[m] if g == h: continue need = radii[h] + radii[g] + margin if dist < need: dvec = (p - co) if dist > 1e-9 else Vector((0, 0, 1)) moves[h][i] += dvec.normalized() * (need - dist) * 0.5 loc, nrm, idx, dist = bvh.find_nearest(p) if loc is not None: need = radii[h] + margin outside = (p - loc).dot(nrm) >= 0 if not outside: moves[h][i] += (loc + nrm * need) - p elif dist < need: moves[h][i] += (p - loc).normalized() * (need - dist) for h, p in enumerate(paths): for i in range(fixed_head, len(p)): # the far end is free to slide on the floor p[i] = p[i] + moves[h][i] if p[i].z < radii[h]: p[i].z = radii[h] for _ in range(2): # keep the lengths, smooth a little for i in range(len(p) - 1): a, b = p[i], p[i + 1] v = b - a L = v.length if L < 1e-12: continue corr = v * ((L - rest[h][i]) / L) * 0.5 if i >= fixed_head: p[i] = a + corr if i + 1 >= fixed_head: p[i + 1] = b - corr for i in range(fixed_head, len(p) - 1): p[i] = p[i].lerp((p[i - 1] + p[i + 1]) * 0.5, 0.15) return paths def build_hoses(mb, bvh): """the middle view's hose bundle (optional): three hoses out of each port, up and back over the head, down behind the arm and along the floor behind the pod; relaxed so that none touches another, the pod or the canisters""" import random rnd = random.Random(7) paths, radii = [], [] # where each hose comes down behind the pod: slots 12 px apart across, at four depths; in front of the arm's back # (u < 640) only outside it (|x| >= 36) slots = [(x, u) for u in (606.0, 622.0, 648.0, 664.0, 680.0) for x in range(-56, 57, 12) if u >= 640 or abs(x) >= 36] rnd.shuffle(slots) for k, (base, axis) in enumerate(port_frames()): a = axis ref = Vector((0, 0, 1)).cross(a) if ref.length < 1e-6: ref = Vector((1, 0, 0)) ref.normalize() ref2 = a.cross(ref) for j in range(3): ang = 2 * math.pi * j / 3 + k off = (ref * math.cos(ang) + ref2 * math.sin(ang)) * (5.6 * S) r = (2.6 + 0.5 * rnd.random()) * S x0 = base.x / S # the free slot nearest this port's side best = min(slots, key=lambda sl: abs(sl[0] - x0 * 1.2) + 0.15 * abs(sl[1] - 640)) slots.remove(best) xs, uf = best du = uf - 604.0 # how far behind the pod this hose falls lift = rnd.uniform(8, 26) # some rise higher over the head than others sway = rnd.uniform(-14, 14) # and swing out on their way down ctrl = [base + a * (12 * S) + off, base + a * (26 * S + lift * 0.4 * S) + off * 1.2, P(x0 * 1.15 + rnd.uniform(-6, 6), 530 + du * 0.5 + (k % 3) * 6, 20 - lift), P(xs + sway * 0.3, 604 + du + sway * 0.5, 62 + rnd.uniform(-18, 18)), P(xs - sway * 0.2, 628 + du + sway, 190 + rnd.uniform(-30, 30)), P(xs * 0.97, 636 + du * 1.1 - sway * 0.5, 360 + rnd.uniform(-30, 30)), P(xs, uf + 40, 503 - r / S), P(xs * 1.3 + (uf - 640) * 0.25, uf + 88 + rnd.uniform(0, 6), 503 - r / S)] head = [base + a * (h * S) + off for h in (12.0, 16.0, 20.0, 24.0, 28.0, 32.0, 35.0)] # out of the mouth up = head[-1] + a * (8 * S) + Vector((0, 0, 10 * S)) # clear of the other ports first paths.append(head[:-1] + resample(catmull([head[-1], up] + ctrl[2:], 12), 2.0 * S)) radii.append(r) relax_hoses(paths, radii, bvh, fixed_head=7) for p, r in zip(paths, radii): tube(mb, resample(p, 8.0 * S), r, 8, "M_NV_Hose", cap_start=True, cap_end=True) def build_canister(mb): o = P(CAN_X, CAN_U, 0) ax = (0, 0, -1) lathe(mb, [(10, 188), (13.6, 189.4), (15, 192), (15, 204.5), (16, 205.5), (16, 208), (14.6, 208.6)], o, ax, 32, "M_NV_Rubber", cap_start=True) lathe(mb, [(14.6, 208.6), (14.6, 262.4)], o, ax, 32, "M_NV_CanGlass") lathe(mb, [(14.6, 262.4), (16, 263), (16, 265.5), (15, 266.5), (15, 285), (13, 289), (6, 291.5), (5, 291.5), (5, 299)], o, ax, 32, "M_NV_Rubber", cap_end=True) # valve under it: a block on the axis with a lever pointing out vb = rpoly_px([(CAN_U - 6, 297), (CAN_U + 6, 297), (CAN_U + 6, 309), (CAN_U - 6, 309)], [2] * 4, [2] * 4, [2, 2, 2, 2]) slab(mb, vb, Frame(o=(0, 0, 0)), prof_full(CAN_X + 6, 1.5, 2, h_back=-(CAN_X - 6)), "M_NV_Rubber") lever = [P(CAN_X + 6.5, CAN_U, 303), P(CAN_X + 13, CAN_U, 303), P(CAN_X + 16, CAN_U + 13, 309)] tube(mb, fillet_path(lever, 3 * S, 4), 1.6 * S, 12, "M_NV_Brass", cap_start=True, cap_end=True) def add_decals(mb): """decal grids (nv_lib.DECALS), laid onto the surface under them 0.25 mm proud; their UVs come in nv_surfaces.py""" from mathutils.bvhtree import BVHTree bvh = BVHTree.FromPolygons([tuple(c) for c in mb.co], [list(f) for f in mb.faces]) for d in DECALS: o, a, b, n = decal_frame(d) nu, nv = d["grid"] mb.begin(hint=n) g = [] for j in range(nv + 1): row = [] for i in range(nu + 1): p = decal_point(d, i / nu, j / nv) if d["snap"]: hit = bvh.ray_cast(p + n * (3 * S), -n, 8 * S) if hit[0] is not None: p = hit[0] row.append(mb.v(p + n * 0.00025)) g.append(row) for j in range(nv): for i in range(nu): mb.f([g[j][i], g[j][i + 1], g[j + 1][i + 1], g[j + 1][i]], "M_NV_Decal") def ensure_mats(names): for n in names: if n not in bpy.data.materials: bpy.data.materials.new(n) 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(38)) 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) ensure_mats(ALL_MATS) sc = bpy.context.scene sc.unit_settings.system = 'METRIC' top = coll("NutritionVat") c_static, c_mov = coll("NV | static", top), coll("NV | movable", top) coll("NV | collision", top) mb = MB() build_shell(mb) build_tank(mb) build_arm(mb) build_details(mb) add_decals(mb) me, bad = mb.to_mesh("SM_NutritionVat_Body", MATS) body = obj("SM_NutritionVat_Body", me, c_static) fm = MB() build_fluid(fm) me_f, bad_f = fm.to_mesh("SM_NutritionVat_Fluid", ["M_NV_Fluid"]) fluid = obj("SM_NutritionVat_Fluid", me_f, c_mov, origin=P(0, TANK_U, 380)) cm = MB() build_canister(cm) me_c, bad_c = cm.to_mesh("SM_NutritionVat_Canister", MATS) can = obj("SM_NutritionVat_Canister", me_c, c_mov, origin=P(CAN_X, CAN_U, 299)) can.name = "NutritionVat_Canister_R" can_l = bpy.data.objects.new("NutritionVat_Canister_L", me_c) # the same cartridge, turned round, left side c_mov.objects.link(can_l) can_l.location = P(-CAN_X, CAN_U, 299) can_l.rotation_euler = (0, 0, math.pi) from mathutils.bvhtree import BVHTree cR = P(CAN_X, CAN_U, 0) cL = P(-CAN_X, CAN_U, 0) # obstacles from the finished meshes (their faces turned out; the builder's own faces have any winding) obst_co = [tuple(v.co) for v in me.vertices] obst_f = [list(pg.vertices) for pg in me.polygons] for sgn, o_ in ((1, cR), (-1, cL)): # the two canisters (the left one turned round its axis) off = len(obst_co) for v in me_c.vertices: d = (v.co + P(CAN_X, CAN_U, 299)) - cR # me_c is stored about the canister's origin obst_co.append(tuple(o_ + Vector((d.x * sgn, d.y * sgn, d.z)))) obst_f += [[i + off for i in pg.vertices] for pg in me_c.polygons] hm = MB() build_hoses(hm, BVHTree.FromPolygons(obst_co, obst_f)) me_h, bad_h = hm.to_mesh("SM_NutritionVat_Hoses", ["M_NV_Hose"]) c_opt = coll("NV | optional", top) hoses = obj("SM_NutritionVat_Hoses", me_h, c_opt) root = bpy.data.objects.new("NutritionVat_Root", None) root.empty_display_type = 'PLAIN_AXES' root.empty_display_size = 0.2 top.objects.link(root) for o in (body, fluid, can, can_l, hoses): o.parent = root rep = dict(faces_not_made=dict(body=bad, fluid=bad_f, canister=bad_c, hoses=bad_h), audit={o.name: audit(o) for o in (body, fluid, can, hoses)}) path = os.path.join(FOLDER, "NutritionVat.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()