63def ellipse(layer: str, c: Pt, a: float, b: float, rot: float, id: Optional[str] =
None) -> Dict[str, Any]:
64 """Closed polyline: semi-axis ``a`` along angle ``rot`` (radians), ``b`` across it."""
65 ca, sa = math.cos(rot), math.sin(rot)
67 for i
in range(_ELLIPSE_PTS):
68 tt = 2 * math.pi * i / _ELLIPSE_PTS
69 x, y = a * math.cos(tt), b * math.sin(tt)
70 ps.append((c[0] + x * ca - y * sa, c[1] + x * sa + y * ca))
71 return poly(layer, ps, closed=
True, id=id)
101 """The plug's half-section outline in (x = z, y = r), from the port wall around to it again:
102 along the face (groove included) out to the sleeve bore, down the rim, back along the back
103 face with each channel notched in, up the hub, and the port wall."""
104 if lay.get(
"profile"):
106 return [(z, r)
for r, z
in lay[
"profile"][
"loop"]]
107 f, inp, env = lay[
"face"], lay[
"inputs"], lay[
"envelope"]
108 t = float(inp[
"plate_thickness"])
109 ign = lay.get(
"igniter")
or {}
110 r_port = 0.5 * ign[
"tap_drill"]
if ign
else 0.0
111 hub_t = (ign.get(
"hub_thickness")
or t)
if ign
else t
112 hub_r = 0.5 * (ign.get(
"hub_diameter")
or 0.0)
if ign
else 0.0
113 r_plate = env[
"r_sleeve_id"]
114 face = [(z, r)
for r, z
in f[
"profile"]
if r > r_port]
115 face.insert(0, (0.0, r_port))
116 face[-1] = (0.0, r_plate)
117 back: List[Pt] = [(-t, r_plate)]
118 chans = sorted((p[
"channel"]
for p
in lay[
"passages"].values()
if p.get(
"channel")),
119 key=
lambda c: -c[
"r_center"])
121 lo, hi =
max(ch[
"r_lo"], r_port),
min(ch[
"r_hi"], r_plate)
124 zf =
lambda rr, ch=ch:
max(-t, ch[
"end"][1] + ch[
"floor_slope"] * (rr - ch[
"r_center"]))
125 back += [(-t, hi), (zf(hi), hi), (zf(lo), lo), (-t, lo)]
126 if hub_t > t
and hub_r > r_port:
127 back += [(-t, hub_r), (-hub_t, hub_r), (-hub_t, r_port)]
129 back += [(-t, r_port)]
159def _inlet_corner(P: Pt, w: Pt, f: Pt, shape: Optional[Tuple[str, float]], d: float, n: int = 12) -> List[Pt]:
160 """The entry edge at corner P, from the hole wall (unit ray ``w``, toward the exit) round to
161 the floor (unit ray ``f``, away from the hole). The metal is the wedge between the two rays;
162 a chamfer or fillet takes its corner off. Sharp: just P."""
166 cos_a =
max(-1.0,
min(1.0, w[0] * f[0] + w[1] * f[1]))
167 alpha = math.acos(cos_a)
168 if kind ==
"chamfer":
170 return [(P[0] + w[0] * c, P[1] + w[1] * c), (P[0] + f[0] * c, P[1] + f[1] * c)]
172 t = r / math.tan(0.5 * alpha)
173 h = r / math.sin(0.5 * alpha)
174 b =
_unit((w[0] + f[0], w[1] + f[1]))
175 C = (P[0] + b[0] * h, P[1] + b[1] * h)
176 T1 = (P[0] + w[0] * t, P[1] + w[1] * t)
177 T2 = (P[0] + f[0] * t, P[1] + f[1] * t)
178 a1 = math.atan2(T1[1] - C[1], T1[0] - C[0])
179 a2 = math.atan2(T2[1] - C[1], T2[0] - C[0])
180 sweep = (a2 - a1 + math.pi) % (2 * math.pi) - math.pi
181 return [(C[0] + r * math.cos(a1 + sweep * i / n), C[1] + r * math.sin(a1 + sweep * i / n))
182 for i
in range(n + 1)]
186 f, inp = lay[
"face"], lay[
"inputs"]
187 p = lay[
"passages"][k]
188 st = inp[
"oxidizer" if k ==
"O" else "fuel"]
189 th, d = float(st[
"impingement_angle"]), float(st[
"d_jet"])
190 inner = (k ==
"O") == bool(f[
"ox_is_inner"])
191 sgn = -1.0
if inner
else 1.0
194 ex, ey = p[
"exit"][1], p[
"exit"][0]
195 t = float(inp[
"plate_thickness"])
196 ch = p.get(
"channel")
198 def edge(off: float, s0: float, s1: float, start_x: Optional[float], end_x: Optional[float]):
199 ox, oy = ex + off * nx, ey + off * ny
201 if start_x
is not None:
202 a = (start_x - ox) / ux
203 if end_x
is not None:
204 b = (end_x - ox) / ux
205 return (ox + a * ux, oy + a * uy), (ox + b * ux, oy + b * uy)
207 start_x =
None if f[
"contoured"]
else 0.0
209 end_x =
None if ch[
"floor"]
in (
"coned",
"spot",
"drawing")
else ch[
"end"][1]
211 (a1, a2), (b1, b2) = (edge(+0.5 * d, 0.0, L, start_x, end_x),
212 edge(-0.5 * d, 0.0, L, start_x, end_x))
213 return [(a1, a2, b2, b1)]
214 land, bore = p[
"land"], p[
"bore"]
215 (a1, a2), (b1, b2) = (edge(+0.5 * d, 0.0, land, start_x,
None),
216 edge(-0.5 * d, 0.0, land, start_x,
None))
217 if p[
"bore_len"] <= 0:
218 (a1, a2), (b1, b2) = (edge(+0.5 * d, 0.0, 0.0, start_x, -t),
219 edge(-0.5 * d, 0.0, 0.0, start_x, -t))
220 parts: List[Tuple[Pt, ...]] = [(a1, a2, b2, b1)]
221 if p[
"bore_len"] > 0:
222 c1, c2 = edge(+0.5 * bore, land, 0.0,
None, -t)
223 e1, e2 = edge(-0.5 * bore, land, 0.0,
None, -t)
224 parts.append((c1, c2, e2, e1))
228def _passage_cut(lay: Dict[str, Any], k: str) -> Tuple[List[List[Pt]], List[List[Pt]]]:
229 """The cut of one passage by the section plane (which contains its axis): a strip of width d
230 from the exit to where it ends, each end the shape it really has -- square to the axis on a
231 contoured flank or a coned floor; cut obliquely by a flat face, a flat channel floor, or the
232 back face -- with the declared inlet (chamfer, countersink, radius) cut into its entry edge.
234 Returns (fills, walls): the open space as closed polygons, and the hole's walls as open
235 polylines. The ends where the hole opens (onto the face, into a channel) are not edges."""
237 a1, a2, b2, b1 = parts[0]
238 d = float(lay[
"inputs"][
"oxidizer" if k ==
"O" else "fuel"][
"d_jet"])
243 ax =
_unit((0.5 * (a1[0] + b1[0] - a2[0] - b2[0]), 0.5 * (a1[1] + b1[1] - a2[1] - b2[1])))
245 out_a, out_b = (a1[0] + ax[0] * e, a1[1] + ax[1] * e), (b1[0] + ax[0] * e, b1[1] + ax[1] * e)
246 in_a = (ca[-1][0] - ax[0] * e, ca[-1][1] - ax[1] * e)
247 in_b = (cb[-1][0] - ax[0] * e, cb[-1][1] - ax[1] * e)
248 fills = [[out_a, a1, *ca, in_a, in_b, *reversed(cb), b1, out_b]] + [list(q)
for q
in parts[1:]]
249 wa, wb = [a1, *ca], [b1, *cb]
251 c1, c2, e2, e1 = parts[1]
254 return fills, [wa, wb]
290def _section(lay: Dict[str, Any], through_doublet: bool) -> List[Dict[str, Any]]:
291 """Half section of the plug in its sleeve, true scale. Radius runs across the view (axis at
292 the left edge, the other half mirrored to the left), the back face is on top and the chamber
293 below. Each channel carries its width and depth, and each passage's axis is drawn through to
294 the channel floor with the channel's centre line, so the two meet where the hole enters."""
295 f, P, inp, env = lay[
"face"], lay[
"passages"], lay[
"inputs"], lay[
"envelope"]
296 ign = lay.get(
"igniter")
297 t = float(inp[
"plate_thickness"])
299 r_b, r_s, r_so = env[
"r_bore"], env[
"r_sleeve_id"], env[
"r_sleeve_od"]
300 hub_t = ((ign
or {}).get(
"hub_thickness")
or t)
301 r_port = 0.5 * ign[
"thread_od"]
if ign
else 0.0
302 r_port_major, r_tap = r_port, (0.5 * ign[
"tap_drill"]
if ign
else 0.0)
303 x_ch =
max(2.5 *
max(f[
"z_imp"], 0.0) + 0.004, 0.015)
304 x_aft = -
max(hub_t, t) - 0.25 * t
306 chans = {k: P[k].get(
"channel")
for k
in (
"O",
"F")}
309 geo: List[Dict[str, Any]] = []
310 phi_top = 0.0
if through_doublet
else math.pi / n
311 for half, phi
in ((1, phi_top), (-1, phi_top + math.pi)):
312 kk = phi * n / (2 * math.pi)
313 has_doublet = abs(kk - round(kk)) < 1e-6
315 def H(ps: List[Pt], half: int = half) -> List[Pt]:
316 return [(x, half * y)
for x, y
in ps]
318 geo.append(
poly(
"PLATE", H(outline), closed=
True))
319 if env[
"sleeve_declared"]:
320 geo.append(
poly(
"SLEEVE", H([(x_aft, r_s), (x_ch, r_s), (x_ch, r_so), (x_aft, r_so)]), closed=
True))
321 if env[
"liner_thickness"] > 0:
322 geo.append(
poly(
"LINER", H([(0.0, r_b), (x_ch, r_b), (x_ch, r_b + env[
"liner_thickness"]),
323 (0.0, r_b + env[
"liner_thickness"])]), closed=
True))
328 z_back = -
max(hub_t, t)
329 run =
min(ign[
"l2"], -z_back)
330 r_end = r_port_major - run / 32.0
331 geo.append(
poly(
"THREAD" if ign[
"l2"] <= -z_back + 1e-9
else "BAD",
332 H([(z_back, r_port_major), (z_back + run, r_end), (z_back + run, r_tap)])))
333 for ch
in chans.values():
335 rf = ch.get(
"r_floor", ch[
"r_center"])
336 geo.append(
poly(
"CENTER", H([(-t, rf), (-t + ch[
"depth"], rf)])))
337 if has_doublet
and not f[
"degenerate"]:
338 idx = int(round(kk)) % n
342 geo.append(
poly(
"HOLE_" + k, H(ps), closed=
True, id=f
"{k}{idx}"))
344 geo.append(
poly(
"PASSAGE_" + k, H(ps), id=f
"{k}{idx}"))
345 ex = (P[k][
"exit"][1], P[k][
"exit"][0])
346 end = chans[k][
"end"]
if chans[k]
else None
349 axis = [(f[
"z_imp"], f[
"r_imp"]), ex] + ([(end[1], end[0])]
if end
else [])
350 geo.append(
poly(
"JET_" + k, H(axis), id=f
"{k}{idx}"))
351 geo.append(
circle(
"IMPINGE", (f[
"z_imp"], half * f[
"r_imp"]),
max(0.00035, 0.012 * f[
"l_imp"])))
352 geo.append(
poly(
"CHAMBER", H([(0.0, r_b), (x_ch, r_b)])))
354 top = r_so
if env[
"sleeve_declared"]
else r_b + env[
"liner_thickness"]
356 geo.append(
poly(
"BREAK", H(
_zigzag(x_ch, r_b, top))))
357 if env[
"sleeve_declared"]:
358 geo.append(
poly(
"BREAK", H(
_zigzag(x_aft, r_s, r_so))))
359 geo.append(
poly(
"CENTER", [(x_aft - 0.003, 0.0), (x_ch, 0.0)]))
364 top_y =
max(hub_t, t)
365 prims.append(
text(
"NOTE", (0.5 * r_b, -0.85 * x_ch),
"chamber",
"middle"))
366 if env[
"liner_thickness"] > 0:
367 prims.append(
text(
"NOTE", (r_b + 0.5 * env[
"liner_thickness"], -0.6 * x_ch),
"phenolic",
"middle"))
368 if env[
"sleeve_declared"]:
369 prims.append(
text(
"NOTE", (r_so + 0.0015, -0.6 * x_ch),
"sleeve",
"start"))
370 land_out =
max([c[
"r_hi"]
for c
in chans.values()
if c]
or [r_port])
371 prims.append(
text(
"NOTE", (0.5 * (land_out + r_s), 0.5 * t - 0.4 * fs),
"injector",
"middle"))
372 for k, name
in ((
"O",
"LOX"), (
"F",
"fuel")):
376 rf = ch.get(
"r_floor", ch[
"r_center"])
377 z_floor = t - ch[
"depth"]
378 prims.append(
dim((ch[
"r_lo"], t), (ch[
"r_hi"], t), (0.0, 0.0015),
_mm(ch[
"width"])))
379 prims.append(
dim((rf, t), (rf, z_floor), (ch[
"r_hi"] - rf + 0.0012, 0.0),
_mm(ch[
"depth"]), side=
"right"))
380 prims.append(
text(
"PASSAGE_" + k, (0.5 * (ch[
"r_lo"] + ch[
"r_hi"]), t + 0.0015 + 1.6 * fs),
381 f
"{name} channel",
"middle"))
386 prims.append(
text(
"NOTE", (-0.0015, 0.5 * top_y + 0.6 * fs),
387 f
"{ign['thread']} port, L2 {_mm(ign['l2'])}",
"end"))
388 if ign[
"l2"] > top_y:
389 prims.append(
text(
"BAD", (-0.0015, 0.5 * top_y - 0.9 * fs), f
"needs {_mm(ign['l2'])}, has {_mm(top_y)}",
"end"))
391 grooves = (lay.get(
"profile")
or {}).get(
"grooves")
or []
394 prims.append(
text(
"NOTE", (0.5 * (g0[
"r_inner"] + g0[
"r_outer"]), t + 0.0015 + 1.6 * fs),
"seal",
"middle"))
395 ref = r_so
if env[
"sleeve_declared"]
else r_s
396 prims.append(
dim((r_s, 0.0), (r_s, t), (ref - r_s + 0.003, 0.0), f
"plate {_mm(t)}", side=
"right"))
397 if hub_t > t
and ign
and ign.get(
"hub_diameter"):
398 prims.append(
dim((0.5 * ign[
"hub_diameter"], 0.0), (0.5 * ign[
"hub_diameter"], hub_t),
399 (0.003, 0.0), f
"centre {_mm(hub_t)}", side=
"right"))
400 if through_doublet
and not f[
"degenerate"]:
401 prims.append(
text(
"NOTE", (f[
"r_imp"], -f[
"z_imp"] - 1.6 * fs),
402 f
"meet {_mm(f['z_imp'], 1)} in front, ⌀{_mm(2 * f['r_imp'], 1)}",
"middle"))
405 x_lab =
min(P[
"O"][
"exit"][0], P[
"F"][
"exit"][0]) - 0.002
406 for row, (k, name)
in enumerate(((
"O",
"LOX"), (
"F",
"fuel"))):
408 st = inp[
"oxidizer" if k ==
"O" else "fuel"]
409 prims.append(
text(
"PASSAGE_" + k, (x_lab, -(1.4 + 1.4 * row) * fs),
410 f
"{name} {st['impingement_angle']:.0f}° ⌀{_mm(st['d_jet'], 3)} L/d {p['land_ld']:.1f}"
419def _face(lay: Dict[str, Any], back: bool) -> List[Dict[str, Any]]:
420 f, P, inp, env = lay[
"face"], lay[
"passages"], lay[
"inputs"], lay[
"envelope"]
421 ign = lay.get(
"igniter")
423 r_b, r_s = env[
"r_bore"], env[
"r_sleeve_id"]
424 mx = -1.0
if back
else 1.0
425 prims: List[Dict[str, Any]] = []
426 if env[
"sleeve_declared"]:
427 prims.append(
circle(
"SLEEVE", (0.0, 0.0), env[
"r_sleeve_od"]))
428 prims.append(
circle(
"PLATE", (0.0, 0.0), r_s))
430 if env[
"liner_thickness"] > 0:
431 prims.append(
circle(
"LINER", (0.0, 0.0), r_b + env[
"liner_thickness"]))
432 prims.append(
circle(
"CHAMBER", (0.0, 0.0), r_b))
435 for rr
in (g[
"groove_edge_in"], g[
"groove_edge_out"]):
436 prims.append(
circle(
"GROOVE", (0.0, 0.0), rr))
438 prims.append(
circle(
"HIDDEN", (0.0, 0.0), r_b))
440 ch = P[k].get(
"channel")
442 prims.append(
circle(
"CHANNEL_" + k, (0.0, 0.0), ch[
"r_lo"]))
443 prims.append(
circle(
"CHANNEL_" + k, (0.0, 0.0), ch[
"r_hi"]))
444 for g
in (lay.get(
"profile")
or {}).get(
"grooves")
or []:
445 prims.append(
circle(
"SEAL", (0.0, 0.0), g[
"r_inner"]))
446 prims.append(
circle(
"SEAL", (0.0, 0.0), g[
"r_outer"]))
448 st = inp[
"oxidizer" if k ==
"O" else "fuel"]
449 th, d = float(st[
"impingement_angle"]), float(st[
"d_jet"])
451 ch = p.get(
"channel")
455 a, b = 0.5 * ch[
"footprint"], 0.5 * d
458 a, b = 0.5 * w /
max(0.1,
_cos(th)), 0.5 * w
460 r_c = f[
"r_O"]
if k ==
"O" else f[
"r_F"]
462 a, b = 0.5 * d *
_cos(th), 0.5 * d
464 a, b = 0.5 * d /
max(0.1,
_cos(th)), 0.5 * d
465 prims.append(
circle(
"PITCH_" + k, (0.0, 0.0), r_c))
466 n_k = n
if k ==
"O" else int(f.get(
"n_F", n))
468 ang = 2 * math.pi * i / n_k + math.pi / 2
469 c = (mx * r_c * math.cos(ang), r_c * math.sin(ang))
470 prims.append(
ellipse(
"PASSAGE_" + k, c, a, b, math.atan2(c[1], c[0]), id=f
"{k}{i}"))
471 if not back
and not f[
"degenerate"]:
472 prims.append(
circle(
"IMPINGE_RING", (0.0, 0.0), f[
"r_imp"]))
474 prims.append(
circle(
"IGNITER", (0.0, 0.0), 0.5 * ign[
"thread_od"]))
475 if back
and ign.get(
"hub_diameter"):
476 prims.append(
circle(
"IGNITER", (0.0, 0.0), 0.5 * ign[
"hub_diameter"]))
477 keep = (lay.get(
"centre_keepout")
or {}).get(
"dia")
or 0.0
478 if not back
and keep > 0:
479 prims.append(
circle(
"KEEPOUT", (0.0, 0.0), 0.5 * keep))
481 prims.append(
text(
"NOTE", (0.0, -(r_b - 0.0035)),
"bore",
"middle"))
482 if env[
"liner_thickness"] > 0:
483 prims.append(
text(
"NOTE", (0.0, -(r_b + 0.5 * env[
"liner_thickness"]) - 0.001),
"liner end",
"middle"))
485 prims.append(
text(
"NOTE", (0.0, f[
"groove"][
"groove_edge_out"] + 0.0015),
"groove",
"middle"))
487 for k, name
in ((
"O",
"LOX"), (
"F",
"fuel")):
488 ch = P[k].get(
"channel")
490 prims.append(
text(
"PASSAGE_" + k, (0.0, ch[
"r_hi"] + 0.0015), f
"{name} channel",
"middle"))
491 if back
and lay[
"back"].get(
"lands"):
492 L = lay[
"back"][
"lands"]
493 inner =
"O" if f[
"ox_is_inner"]
else "F"
494 outer =
"F" if inner ==
"O" else "O"
495 ci, co = P[inner][
"channel"], P[outer][
"channel"]
496 prims.append(
text(
"NOTE", (0.0, -0.5 * (ci[
"r_hi"] + co[
"r_lo"])), f
"land {_mm(L['between'])}",
"middle"))
497 prims.append(
text(
"NOTE", (0.0, -0.5 * (co[
"r_hi"] + r_s)), f
"land {_mm(L['outer'])}",
"middle"))
498 R = env[
"r_sleeve_od"]
if env[
"sleeve_declared"]
else r_s
499 prims.append(
poly(
"CENTER", [(-R * 1.04, 0.0), (R * 1.04, 0.0)]))
500 prims.append(
poly(
"CENTER", [(0.0, -R * 1.04), (0.0, R * 1.04)]))
508def _ports(lay: Dict[str, Any]) -> List[Dict[str, Any]]:
509 """The cover plate from above: its feed ports over the channels (hidden), the igniter."""
510 ports, env, P = lay[
"ports"], lay[
"envelope"], lay[
"passages"]
511 r_s = env[
"r_sleeve_id"]
512 prims: List[Dict[str, Any]] = [
circle(
"PLATE", (0.0, 0.0), r_s)]
515 prims.append(
circle(
"HIDDEN", (0.0, 0.0), ch[
"r_lo"]))
516 prims.append(
circle(
"HIDDEN", (0.0, 0.0), ch[
"r_hi"]))
517 for g
in (lay.get(
"profile")
or {}).get(
"grooves")
or []:
518 prims.append(
circle(
"SEAL", (0.0, 0.0), g[
"r_inner"]))
519 prims.append(
circle(
"SEAL", (0.0, 0.0), g[
"r_outer"]))
520 for k, name
in ((
"O",
"LOX"), (
"F",
"fuel")):
521 ring = ports[
"rings"][k]
522 for i, a
in enumerate(ring[
"angles_deg"]):
523 ang = math.radians(90.0 - a)
524 c = (ring[
"r"] * math.cos(ang), ring[
"r"] * math.sin(ang))
525 prims.append(
circle(
"PASSAGE_" + k, c, 0.5 * ports[
"bore"], id=f
"P{k}{i}"))
526 prims.append(
circle(
"THREAD", c, 0.5 * ports[
"thread_od"]))
527 a = math.radians(90.0 - ring[
"angles_deg"][0])
528 prims.append(
text(
"PASSAGE_" + k, (ring[
"r"] * math.cos(a) + 0.5 * ports[
"thread_od"] + 0.0015,
529 ring[
"r"] * math.sin(a)),
530 f
"{name} {ports['per_ring']}× {ports['thread']} on ⌀{_mm(2 * ring['r'], 1)}",
"start"))
531 ign = lay.get(
"igniter")
533 prims.append(
circle(
"IGNITER", (0.0, 0.0), 0.5 * ign[
"tap_drill"]))
534 prims.append(
circle(
"THREAD", (0.0, 0.0), 0.5 * ign[
"thread_od"]))
535 prims.append(
text(
"NOTE", (0.0, -0.5 * ign[
"thread_od"] - 0.004), f
"igniter {ign['thread']}",
"middle"))
536 prims.append(
poly(
"CENTER", [(-r_s * 1.04, 0.0), (r_s * 1.04, 0.0)]))
537 prims.append(
poly(
"CENTER", [(0.0, -r_s * 1.04), (0.0, r_s * 1.04)]))