STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
drawing.py
Go to the documentation of this file.
1"""Engineering views of the impinging injector in its sleeve, as drawing primitives.
2
3Presentation of ``engine.core.injectors.layout`` only: nothing here derives geometry, it places
4what the layout computed. The frontend renders these primitives to SVG and to DXF from the same
5list, so the screen and the CAD file cannot disagree, and neither can disagree with Layer 1.
6
7Section views: x is radius (the axis at x = 0, the other half mirrored to x < 0), y is axial
8with the face datum at y = 0, the plug's back face at y = +t and the chamber toward -y. Face
9views: the face plane, origin on the axis.
10
11Primitive shapes::
12
13 {"t": "poly", "layer": L, "pts": [[x, y], ...], "closed": bool, "id": optional}
14 {"t": "circle", "layer": L, "c": [x, y], "r": r, "id": optional}
15 {"t": "dim", "layer": "DIM", "a": [x, y], "b": [x, y], "off": [dx, dy], "text": str,
16 "side": optional "right"}
17 {"t": "text", "layer": L, "at": [x, y], "text": str, "anchor": "start"|"middle"|"end"}
18
19``id`` ties the same physical hole across views ("O3" = LOX element 3).
20"""
21
22from __future__ import annotations
23
24import math
25from typing import Any, Dict, List, Optional, Tuple
26
27Pt = Tuple[float, float]
28
29_ELLIPSE_PTS = 28
30
31
32def _r(v: float) -> float:
33 return round(v, 7) # 0.1 um -- far below any machining tolerance
34
35
36def poly(layer: str, ps: List[Pt], closed: bool = False, id: Optional[str] = None) -> Dict[str, Any]:
37 out = {"t": "poly", "layer": layer, "pts": [[_r(x), _r(y)] for x, y in ps], "closed": closed}
38 if id:
39 out["id"] = id
40 return out
41
42
43def circle(layer: str, c: Pt, r: float, id: Optional[str] = None) -> Dict[str, Any]:
44 out = {"t": "circle", "layer": layer, "c": [_r(c[0]), _r(c[1])], "r": _r(r)}
45 if id:
46 out["id"] = id
47 return out
48
49
50def dim(a: Pt, b: Pt, off: Pt, text: str, side: Optional[str] = None) -> Dict[str, Any]:
51 """``side="right"`` puts a vertical dimension's text right of its line (default: left)."""
52 out = {"t": "dim", "layer": "DIM", "a": [_r(a[0]), _r(a[1])], "b": [_r(b[0]), _r(b[1])],
53 "off": [_r(off[0]), _r(off[1])], "text": text}
54 if side:
55 out["side"] = side
56 return out
57
58
59def text(layer: str, at: Pt, s: str, anchor: str = "start") -> Dict[str, Any]:
60 return {"t": "text", "layer": layer, "at": [_r(at[0]), _r(at[1])], "text": s, "anchor": anchor}
61
62
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)
66 ps = []
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)
72
73
74def _mm(v: float, d: int = 2) -> str:
75 return f"{v * 1000:.{d}f}"
76
77
78def _sin(th: float) -> float:
79 return math.sin(math.radians(th))
80
81
82def _cos(th: float) -> float:
83 return math.cos(math.radians(th))
84
85
86# =============================================================================================
87# Section
88# =============================================================================================
89
90def _zigzag(x: float, y0: float, y1: float, n: int = 4) -> List[Pt]:
91 """A break line across a part that continues past the view (x fixed, y0 -> y1)."""
92 amp = 0.12 * (y1 - y0) / n
93 pts = [(x, y0)]
94 for i in range(1, 2 * n):
95 pts.append((x + (amp if i % 2 else -amp), y0 + (y1 - y0) * i / (2 * n)))
96 pts.append((x, y1))
97 return pts
98
99
100def _plug_outline(lay: Dict[str, Any]) -> List[Pt]:
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"):
105 # The half-section the layout built (or read from the drawing): (r, z) -> (x = z, y = r).
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 # the drilled hole; threads cut beyond it
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"])
120 for ch in chans: # outward -> inward along the back
121 lo, hi = max(ch["r_lo"], r_port), min(ch["r_hi"], r_plate) # a channel off the plug is flagged, not drawn into the sleeve
122 if hi <= lo:
123 continue
124 zf = lambda rr, ch=ch: max(-t, ch["end"][1] + ch["floor_slope"] * (rr - ch["r_center"])) # noqa: E731
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)]
128 else:
129 back += [(-t, r_port)]
130 return face + back
131
132
133#: How each named inlet is cut at the hole's entry edge, as (kind, size / d). The fillets use the
134#: r/d the Cd table is keyed to (engine.core.discharge.INLET_GEOMETRY_RD); the chamfer is that
135#: table's "~45 deg x 0.1 d break". A countersink's size does not enter the Cd model; it is drawn
136#: 45 deg to the wall, 0.2 d a side (90 deg included, to about 1.4 d on a square entry).
137_INLET_CUT = {"chamfered": ("chamfer", 0.10), "conical": ("chamfer", 0.20)}
138
139
140def _inlet_shape(p: Dict[str, Any]) -> Optional[Tuple[str, float]]:
141 """(kind, size / d) for a passage's declared inlet; None for sharp or not set."""
142 from engine.core.discharge import INLET_GEOMETRY_RD
143 cd = p.get("cd") or {}
144 rd, name = cd.get("inlet_r_over_d"), cd.get("inlet_name")
145 if rd is not None:
146 return ("fillet", float(rd)) if rd > 0 else None
147 if name in _INLET_CUT:
148 return _INLET_CUT[name]
149 if name in INLET_GEOMETRY_RD and INLET_GEOMETRY_RD[name] > 0:
150 return ("fillet", INLET_GEOMETRY_RD[name])
151 return None
152
153
154def _unit(v: Pt) -> Pt:
155 n = math.hypot(v[0], v[1])
156 return (v[0] / n, v[1] / n) if n > 0 else (0.0, 0.0)
157
158
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."""
163 if not shape:
164 return [P]
165 kind, size = shape
166 cos_a = max(-1.0, min(1.0, w[0] * f[0] + w[1] * f[1]))
167 alpha = math.acos(cos_a) # the metal wedge's angle
168 if kind == "chamfer":
169 c = size * d
170 return [(P[0] + w[0] * c, P[1] + w[1] * c), (P[0] + f[0] * c, P[1] + f[1] * c)]
171 r = size * d
172 t = r / math.tan(0.5 * alpha) # tangent points' distance from the corner
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 # the short way round, past P
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)]
183
184
185def _passage_parts(lay: Dict[str, Any], k: str) -> List[Tuple[Pt, ...]]:
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
192 ux, uy = -_cos(th), sgn * _sin(th) # along the axis, into the plug (x = z, y = r)
193 nx, ny = sgn * _sin(th), _cos(th) # across it, in the section plane
194 ex, ey = p["exit"][1], p["exit"][0] # exit in (x, y)
195 t = float(inp["plate_thickness"])
196 ch = p.get("channel")
197
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
200 a, b = s0, s1
201 if start_x is not None:
202 a = (start_x - ox) / ux # x(s) = ox + s ux -> s where x = start_x
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)
206
207 start_x = None if f["contoured"] else 0.0 # contoured: the exit is square on the flank
208 if ch is not None:
209 end_x = None if ch["floor"] in ("coned", "spot", "drawing") else ch["end"][1]
210 L = ch["length"]
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))
225 return parts
226
227
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.
233
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."""
236 parts = _passage_parts(lay, k)
237 a1, a2, b2, b1 = parts[0]
238 d = float(lay["inputs"]["oxidizer" if k == "O" else "fuel"]["d_jet"])
239 shape = _inlet_shape(lay["passages"][k])
240 ca = _inlet_corner(a2, _unit((a1[0] - a2[0], a1[1] - a2[1])), _unit((a2[0] - b2[0], a2[1] - b2[1])), shape, d)
241 cb = _inlet_corner(b2, _unit((b1[0] - b2[0], b1[1] - b2[1])), _unit((b2[0] - a2[0], b2[1] - a2[1])), shape, d)
242 # The fill runs a hair past both openings so the floor and face lines do not show across them.
243 ax = _unit((0.5 * (a1[0] + b1[0] - a2[0] - b2[0]), 0.5 * (a1[1] + b1[1] - a2[1] - b2[1])))
244 e = 0.04 * d
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]
250 if len(parts) > 1: # counterbore: its bottom out to its wall, then up to the back
251 c1, c2, e2, e1 = parts[1]
252 wa += [c1, c2]
253 wb += [e1, e2]
254 return fills, [wa, wb]
255
256
257def _passage_polys(lay: Dict[str, Any], k: str) -> List[List[Pt]]:
258 """The open space of one passage in the section plane (see ``_passage_cut``)."""
259 return _passage_cut(lay, k)[0]
260
261
262def _inlet_note(p: Dict[str, Any], d: float) -> str:
263 """', rounded inlet r 0.19' for a hole label; empty for sharp or not set."""
264 shape = _inlet_shape(p)
265 if shape is None:
266 return ""
267 cd = p.get("cd") or {}
268 what = f"r/d {cd['inlet_r_over_d']:g}" if cd.get("inlet_r_over_d") is not None else cd.get("inlet_name")
269 kind, size = shape
270 return f", {what} inlet " + (f"r {_mm(size * d, 2)}" if kind == "fillet" else f"{_mm(size * d, 2)}×45°")
271
272
273def _rot(prims: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
274 """Model frame (x = z, y = r) -> drawing frame (X = r across, Y = -z up): the plug lies
275 across the view with the manifold side on top and the chamber below."""
276 R = lambda q: [_r(q[1]), _r(-q[0])] # noqa: E731
277 out = []
278 for p in prims:
279 p = dict(p)
280 if p["t"] == "poly":
281 p["pts"] = [R(q) for q in p["pts"]]
282 elif p["t"] == "circle":
283 p["c"] = R(p["c"])
284 elif p["t"] == "text":
285 p["at"] = R(p["at"])
286 out.append(p)
287 return out
288
289
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"])
298 n = int(f["n"])
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) # enough chamber to show the jets meet
304 x_aft = -max(hub_t, t) - 0.25 * t
305 outline = _plug_outline(lay)
306 chans = {k: P[k].get("channel") for k in ("O", "F")}
307
308 # ---- geometry, built in the model frame (x = z, y = r) and turned at the end ------------
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
314
315 def H(ps: List[Pt], half: int = half) -> List[Pt]:
316 return [(x, half * y) for x, y in ps]
317
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))
324 if ign:
325 # Tapped from the back: the thread's major diameter (NPT, 1 in 16 on diameter, large
326 # end at the back) over the length the metal gives, closed onto the drilled wall
327 # where the thread runs out. Red when the thread needs more than that (L2).
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():
334 if ch: # the channel's centre line, back face to floor
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
339 for k in ("O", "F"):
340 fills, walls = _passage_cut(lay, k)
341 for ps in fills:
342 geo.append(poly("HOLE_" + k, H(ps), closed=True, id=f"{k}{idx}"))
343 for ps in walls:
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
347 # The hole's axis, from where the jets meet back through the exit to where it
348 # enters its channel.
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)])))
353 # The sleeve and liner continue past the view: zig-zag break lines, not ends.
354 top = r_so if env["sleeve_declared"] else r_b + env["liner_thickness"]
355 if top > r_b:
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)]))
360 prims = _rot(geo)
361
362 # ---- annotation, in the drawing frame (X = r, Y = -z: back face at Y = t) ----------------
363 fs = 0.0021 # about the renderer's text height at this view's size
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")):
373 ch = chans[k]
374 if not ch:
375 continue
376 rf = ch.get("r_floor", ch["r_center"])
377 z_floor = t - ch["depth"] # the floor, drawing frame
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"))
382 if ign:
383 # The thread line runs the engagement the thread needs (L2) from the back; red when the
384 # metal at the port is shorter than that.
385 # Left of the centre line, level with the port: the one clear place in a half section.
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"))
390
391 grooves = (lay.get("profile") or {}).get("grooves") or []
392 if grooves:
393 g0 = grooves[0]
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"))
403 # Both hole labels stacked in the clear chamber space inside the inner exit, colour-coded:
404 # right of it the jets, the phenolic and the sleeve leave no room.
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"))):
407 p = P[k]
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}"
411 + _inlet_note(p, float(st["d_jet"])), "end"))
412 return prims
413
414
415# =============================================================================================
416# Face and back views
417# =============================================================================================
418
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")
422 n = int(f["n"])
423 r_b, r_s = env["r_bore"], env["r_sleeve_id"]
424 mx = -1.0 if back else 1.0 # seen from the back, clocking is mirrored
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))
429 if not back:
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))
433 g = f.get("groove")
434 if g:
435 for rr in (g["groove_edge_in"], g["groove_edge_out"]):
436 prims.append(circle("GROOVE", (0.0, 0.0), rr))
437 else:
438 prims.append(circle("HIDDEN", (0.0, 0.0), r_b))
439 for k in ("O", "F"):
440 ch = P[k].get("channel")
441 if ch:
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"]))
447 for k in ("O", "F"):
448 st = inp["oxidizer" if k == "O" else "fuel"]
449 th, d = float(st["impingement_angle"]), float(st["d_jet"])
450 p = P[k]
451 ch = p.get("channel")
452 if back:
453 r_c = p["r_back"]
454 if ch:
455 a, b = 0.5 * ch["footprint"], 0.5 * d # breakthrough on the channel floor
456 else:
457 w = p["entry_d"]
458 a, b = 0.5 * w / max(0.1, _cos(th)), 0.5 * w
459 else:
460 r_c = f["r_O"] if k == "O" else f["r_F"]
461 if f["contoured"]:
462 a, b = 0.5 * d * _cos(th), 0.5 * d # a round exit on a flank, seen along the axis
463 else:
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))
467 for i in range(n_k):
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"]))
473 if ign:
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))
480 if not back:
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"))
484 if f.get("groove"):
485 prims.append(text("NOTE", (0.0, f["groove"]["groove_edge_out"] + 0.0015), "groove", "middle"))
486 else:
487 for k, name in (("O", "LOX"), ("F", "fuel")):
488 ch = P[k].get("channel")
489 if ch:
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)]))
501 return prims
502
503
504# =============================================================================================
505# Port plate and revolve sketch
506# =============================================================================================
507
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)]
513 for k in ("O", "F"):
514 ch = P[k]["channel"]
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) # the first port at 12 o'clock, clockwise
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")
532 if ign:
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)]))
538 return prims
539
540
541def _revolve(lay: Dict[str, Any]) -> List[Dict[str, Any]]:
542 """The plate's half-section as a CAD revolve sketch: one closed profile, radius across (x),
543 axial up from the face (y = 0 at the face, the back face at y = t), and the axis. Nothing
544 else, so it revolves as it is."""
545 loop = lay["profile"]["loop"]
546 t = float(lay["profile"]["thickness"])
547 return [poly("PLATE", [(r, -z) for r, z in loop], closed=True),
548 poly("CENTER", [(0.0, -0.1 * t), (0.0, 1.1 * t)])]
549
550
551def injector_drawings(lay: Dict[str, Any]) -> Dict[str, Any]:
552 """All views of a layout."""
553 out = {
554 "face": _face(lay, back=False),
555 "back": _face(lay, back=True),
556 "section_doublet": _section(lay, True),
557 "section_between": _section(lay, False),
558 }
559 if lay.get("profile"):
560 out["revolve"] = _revolve(lay)
561 if lay.get("ports"):
562 out["ports"] = _ports(lay)
563 return out
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
List[Dict[str, Any]] _ports(Dict[str, Any] lay)
Definition drawing.py:508
Dict[str, Any] poly(str layer, List[Pt] ps, bool closed=False, Optional[str] id=None)
Definition drawing.py:36
List[Dict[str, Any]] _section(Dict[str, Any] lay, bool through_doublet)
Definition drawing.py:290
Dict[str, Any] ellipse(str layer, Pt c, float a, float b, float rot, Optional[str] id=None)
Definition drawing.py:63
Dict[str, Any] circle(str layer, Pt c, float r, Optional[str] id=None)
Definition drawing.py:43
str _inlet_note(Dict[str, Any] p, float d)
Definition drawing.py:262
Optional[Tuple[str, float]] _inlet_shape(Dict[str, Any] p)
Definition drawing.py:140
List[List[Pt]] _passage_polys(Dict[str, Any] lay, str k)
Definition drawing.py:257
List[Dict[str, Any]] _rot(List[Dict[str, Any]] prims)
Definition drawing.py:273
List[Dict[str, Any]] _face(Dict[str, Any] lay, bool back)
Definition drawing.py:419
Dict[str, Any] dim(Pt a, Pt b, Pt off, str text, Optional[str] side=None)
Definition drawing.py:50
Dict[str, Any] text(str layer, Pt at, str s, str anchor="start")
Definition drawing.py:59
List[Dict[str, Any]] _revolve(Dict[str, Any] lay)
Definition drawing.py:541
str _mm(float v, int d=2)
Definition drawing.py:74
List[Tuple[Pt,...]] _passage_parts(Dict[str, Any] lay, str k)
Definition drawing.py:185
Tuple[List[List[Pt]], List[List[Pt]]] _passage_cut(Dict[str, Any] lay, str k)
Definition drawing.py:228
List[Pt] _inlet_corner(Pt P, Pt w, Pt f, Optional[Tuple[str, float]] shape, float d, int n=12)
Definition drawing.py:159
Dict[str, Any] injector_drawings(Dict[str, Any] lay)
Definition drawing.py:551
List[Pt] _plug_outline(Dict[str, Any] lay)
Definition drawing.py:100
List[Pt] _zigzag(float x, float y0, float y1, int n=4)
Definition drawing.py:90