From eedafb186ddb1985a1d20f55cba1392a28239817 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 12 Mar 2026 13:00:04 +0000 Subject: [PATCH] Add app/main.py --- app/main.py | 375 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 app/main.py diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..516aa92 --- /dev/null +++ b/app/main.py @@ -0,0 +1,375 @@ +import streamlit as st +from matplotlib.textpath import TextPath +from matplotlib.font_manager import FontProperties +from matplotlib.path import Path as MplPath + +# ─── Text → SVG path helper ─────────────────────────────────────────────────── +def tp(text, cx, cy, font_size, anchor='middle', fill='#444444'): + """Convert a string to an SVG element (no tags).""" + if not text: + return '' + fp = FontProperties(family='DejaVu Sans') + mtp = TextPath((0, 0), text, size=font_size, prop=fp) + bb = mtp.get_extents() + tw = bb.x1 - bb.x0 + + if anchor == 'middle': + ox = cx - tw / 2 - bb.x0 + elif anchor == 'end': + ox = cx - tw - bb.x0 + else: + ox = cx - bb.x0 + + # Vertical: centre of glyph bounding box → cy + text_mid_y = (bb.y0 + bb.y1) / 2 + oy = cy + text_mid_y # SVG y = oy − matplotlib y + + verts = mtp.vertices + codes = mtp.codes + d = [] + i = 0 + n = len(codes) + while i < n: + code = codes[i] + vx, vy = verts[i] + sx = vx + ox; sy = oy - vy + if code == MplPath.MOVETO: + d.append(f'M {sx:.2f},{sy:.2f}') + i += 1 + elif code == MplPath.LINETO: + d.append(f'L {sx:.2f},{sy:.2f}') + i += 1 + elif code == MplPath.CURVE3: + vx2, vy2 = verts[i+1] + d.append(f'Q {sx:.2f},{sy:.2f} {vx2+ox:.2f},{oy-vy2:.2f}') + i += 2 + elif code == MplPath.CURVE4: + vx2, vy2 = verts[i+1]; vx3, vy3 = verts[i+2] + d.append(f'C {sx:.2f},{sy:.2f} {vx2+ox:.2f},{oy-vy2:.2f} {vx3+ox:.2f},{oy-vy3:.2f}') + i += 3 + elif code == MplPath.CLOSEPOLY: + d.append('Z') + i += 1 + else: + i += 1 + return f'' + + +# ─── Language packages ──────────────────────────────────────────────────────── +LANG = { + "SL": { + "app_title": "🔌 Generator sheme con ARBURG", + "app_sub": "Konfigurirajte grelne cone in izvozite SVG.", + "general": "⚙️ Splošne nastavitve", + "diag_title": "Naziv diagrama", + "num_zones": "Število con", + "zone_width": "Širina cone (px)", + "svg_height": "Višina diagrama (px)", + "symbol": "📐 Nastavitve simbolov", + "show_pol": "Prikaži oznake +/−", + "show_zlbl": "Prikaži oznake con", + "dividers": "Notranje črte grelca", + "style": "🎨 Slog", + "stroke_col": "Barva linij", + "bg_col": "Barva ozadja", + "inact_col": "Barva križa (neaktivno)", + "font_sz": "Velikost pisave terminalov", + "num_scheme": "Shema številčenja terminalov", + "seq_label": "Zaporedno (1-2, 3-4 … grelec; 13-14 … TC)", + "custom_label": "Po meri (ročni vnos)", + "per_zone": "Konfiguracija po conah", + "zone_hdr": "Cona", + "active_lbl": "Aktivna", + "wattage_lbl": "Moč", + "wattage_def": "350 W", + "zone_type_lbl": "Tip cone", + "type_nozzle": "VSTOPNA SOBA", # without Š for laser compat + "type_block": "GRELNI BLOK", + "type_heater": "GRELEC", + "warn_nozzle": "⚠️ VSTOPNA ŠOBA je že dodeljena coni {z}. Samo ena je dovoljena.", + "warn_block": "⚠️ GRELNI BLOK je že dodeljen coni {z}. Samo eden je dovoljen.", + "h_plus": "G+", + "h_minus": "G−", + "tc_plus": "TC+", + "tc_minus": "TC−", + "preview": "Predogled", + "download": "⬇️ Prenesi SVG", + "tip": "Nasvet: odprite SVG v Inkscape ali brskalniku za urejanje.", + "type_nozzle_svg": "VSTOPNA SOBA", + "type_block_svg": "GRELNI BLOK", + "type_heater_svg": "GRELEC", + }, + "EN": { + "app_title": "🔌 ARBURG Zone Diagram Generator", + "app_sub": "Configure heating zones and export as SVG.", + "general": "⚙️ General Settings", + "diag_title": "Diagram title", + "num_zones": "Number of zones", + "zone_width": "Zone width (px)", + "svg_height": "Diagram height (px)", + "symbol": "📐 Symbol Settings", + "show_pol": "Show +/− polarity labels", + "show_zlbl": "Show zone labels", + "dividers": "Heater internal dividers", + "style": "🎨 Style", + "stroke_col": "Line / stroke color", + "bg_col": "Background color", + "inact_col": "Inactive cross color", + "font_sz": "Terminal number font size", + "num_scheme": "Terminal numbering scheme", + "seq_label": "Sequential pairs (1-2, 3-4 … heater; 13-14 … TC)", + "custom_label": "Custom (enter manually)", + "per_zone": "Per-zone configuration", + "zone_hdr": "Zone", + "active_lbl": "Active", + "wattage_lbl": "Wattage", + "wattage_def": "350 W", + "zone_type_lbl": "Zone type", + "type_nozzle": "NOZZLE", + "type_block": "HEATING BLOCK", + "type_heater": "HEATER", + "warn_nozzle": "⚠️ NOZZLE already assigned to zone {z}. Only one allowed.", + "warn_block": "⚠️ HEATING BLOCK already assigned to zone {z}. Only one allowed.", + "h_plus": "H+", + "h_minus": "H−", + "tc_plus": "TC+", + "tc_minus": "TC−", + "preview": "Preview", + "download": "⬇️ Download SVG", + "tip": "Tip: open the SVG in Inkscape or a browser for further editing.", + "type_nozzle_svg": "NOZZLE", + "type_block_svg": "HEATING BLOCK", + "type_heater_svg": "HEATER", + }, +} + +# ─── Page setup ─────────────────────────────────────────────────────────────── +st.set_page_config(page_title="ARBURG Zone Diagram Generator", layout="wide") + +with st.sidebar: + lang_choice = st.selectbox("🌐 Jezik / Language", ["SL", "EN"], index=0) + +T = LANG[lang_choice] + +st.title(T["app_title"]) +st.markdown(T["app_sub"]) + +# ─── Sidebar config ─────────────────────────────────────────────────────────── +with st.sidebar: + st.header(T["general"]) + title_text = st.text_input(T["diag_title"], value="ARBURG") + num_zones = st.slider(T["num_zones"], 1, 12, 6) + zone_width = st.slider(T["zone_width"], 180, 320, 256) + svg_height = st.slider(T["svg_height"], 400, 800, 580) + + st.markdown("---") + st.header(T["symbol"]) + show_polarity = st.checkbox(T["show_pol"], value=True) + show_zone_lbl = st.checkbox(T["show_zlbl"], value=True) + heater_dividers = st.slider(T["dividers"], 0, 5, 3) + + st.markdown("---") + st.header(T["style"]) + stroke_color = st.color_picker(T["stroke_col"], value="#444444") + bg_color = st.color_picker(T["bg_col"], value="#ffffff") + inactive_color = st.color_picker(T["inact_col"], value="#cc0000") + font_size_num = st.slider(T["font_sz"], 14, 36, 24) + +# ─── Terminal numbering scheme ──────────────────────────────────────────────── +st.subheader(T["num_scheme"]) +num_scheme = st.radio("", [T["seq_label"], T["custom_label"]], + horizontal=True, label_visibility="collapsed") + +# ─── Per-zone config ────────────────────────────────────────────────────────── +st.subheader(T["per_zone"]) + +ZONE_TYPES = [T["type_nozzle"], T["type_block"], T["type_heater"]] + +zone_configs = [] +nozzle_assigned_to = None +block_assigned_to = None +heater_counter = 0 + +cols_per_row = min(num_zones, 6) +rows_needed = (num_zones + cols_per_row - 1) // cols_per_row +col_sets = [st.columns(cols_per_row) for _ in range(rows_needed)] + +for z in range(num_zones): + row = z // cols_per_row + col_i = z % cols_per_row + with col_sets[row][col_i]: + st.markdown(f"**{T['zone_hdr']} {z+1}**") + + zone_type = st.selectbox(T["zone_type_lbl"], ZONE_TYPES, + index=2, key=f"ztype{z}") + + if zone_type == T["type_nozzle"]: + if nozzle_assigned_to is not None: + st.warning(T["warn_nozzle"].format(z=nozzle_assigned_to)) + else: + nozzle_assigned_to = z + 1 + elif zone_type == T["type_block"]: + if block_assigned_to is not None: + st.warning(T["warn_block"].format(z=block_assigned_to)) + else: + block_assigned_to = z + 1 + + if zone_type == T["type_nozzle"]: + svg_label = T["type_nozzle_svg"] + elif zone_type == T["type_block"]: + svg_label = T["type_block_svg"] + else: + heater_counter += 1 + svg_label = f"{T['type_heater_svg']} {heater_counter}" + + active = st.checkbox(T["active_lbl"], value=True, key=f"act{z}") + wattage = "" + if active: + wattage = st.text_input(T["wattage_lbl"], value=T["wattage_def"], key=f"wat{z}") + + if num_scheme == T["custom_label"]: + h_top = st.number_input(T["h_plus"], value=z*2+1, key=f"ht{z}", step=1) + h_bot = st.number_input(T["h_minus"], value=z*2+2, key=f"hb{z}", step=1) + t_top = st.number_input(T["tc_plus"], value=z*2+1+num_zones*2, key=f"tt{z}", step=1) + t_bot = st.number_input(T["tc_minus"],value=z*2+2+num_zones*2, key=f"tb{z}", step=1) + zone_configs.append((int(h_top), int(h_bot), int(t_top), int(t_bot), + svg_label, active, wattage)) + else: + zone_configs.append(( + z*2+1, z*2+2, + z*2+1+num_zones*2, z*2+2+num_zones*2, + svg_label, active, wattage + )) + + +# ─── SVG generation (no — all paths) ────────────────────────────────── +def generate_svg(zones, title, zone_w, svg_h, + stroke, bg, inactive_col, font_num, + show_pol, show_zlbl, dividers): + + svg_w = zone_w * len(zones) + 4 + + H_CX = zone_w * 30 // 100 + T_CX = zone_w * 70 // 100 + TOP_Y = int(svg_h * 0.145) + BOT_Y = int(svg_h * 0.852) + CR = int(zone_w * 0.125) + + HR_TOP = int(svg_h * 0.232) + HR_H = int(svg_h * 0.483) + HR_W = int(zone_w * 0.226) + HR_X = H_CX - HR_W // 2 + + chevron_top = int(svg_h * 0.396) + chevron_bot = int(svg_h * 0.672) + junc_y = (chevron_top + chevron_bot) // 2 + tip_dx = int(zone_w * 0.109) + + CROSS_PAD_X = int(zone_w * 0.06) + CROSS_TOP = int(svg_h * 0.10) + CROSS_BOT = int(svg_h * 0.90) + + fs_num = font_num + fs_title = int(svg_h * 0.076) + fs_lbl = int(svg_h * 0.029) + fs_pol = int(svg_h * 0.033) + + L = [] + L.append(f'') + L.append(f'') + + # Title — path + L.append(tp(title, svg_w // 2, int(svg_h * 0.048), fs_title, 'middle', stroke)) + + for i, (t1, t2, t3, t4, zlabel, active, wattage) in enumerate(zones): + ox = i * zone_w + 2 + hx = ox + H_CX + tx = ox + T_CX + mid = ox + (H_CX + T_CX) // 2 + + # Separator + if i > 0: + L.append(f'') + + # ── Heater ── + L.append(f'') + L.append(tp(str(t1), hx, TOP_Y, fs_num, 'middle', stroke)) + L.append(f'') + L.append(f'') + if dividers > 0: + step = HR_H / (dividers + 1) + for d in range(1, dividers + 1): + dy = int(HR_TOP + step * d) + L.append(f'') + L.append(f'') + L.append(f'') + L.append(tp(str(t2), hx, BOT_Y, fs_num, 'middle', stroke)) + + # ── Thermocouple ── + L.append(f'') + L.append(tp(str(t3), tx, TOP_Y, fs_num, 'middle', stroke)) + if show_pol: + L.append(tp('+', tx + CR // 2 + fs_pol // 2, int(svg_h * 0.270), fs_pol, 'start', stroke)) + L.append(f'') + tip_x = tx + tip_dx + L.append(f'') + L.append(f'') + L.append(f'') + L.append(f'') + if show_pol: + L.append(tp('-', tx + CR // 2 + fs_pol // 2, int(svg_h * 0.724), fs_pol, 'start', stroke)) + L.append(f'') + L.append(tp(str(t4), tx, BOT_Y, fs_num, 'middle', stroke)) + + # ── Inactive cross ── + if not active: + x0 = ox + CROSS_PAD_X + x1 = ox + zone_w - CROSS_PAD_X + L.append(f'') + L.append(f'') + + # ── Zone label + wattage (only when active) ── + if show_zlbl and active: + L.append(tp(zlabel, mid, int(svg_h * 0.950), fs_lbl, 'middle', stroke)) + if active and wattage.strip(): + L.append(tp(wattage, mid, int(svg_h * 0.985), fs_lbl, 'middle', stroke)) + + L.append('') + return '\n'.join(L) + + +# ─── Preview & download ─────────────────────────────────────────────────────── +svg_str = generate_svg( + zone_configs, title_text, zone_width, svg_height, + stroke_color, bg_color, inactive_color, font_size_num, + show_polarity, show_zone_lbl, heater_dividers +) + +st.markdown("---") +st.subheader(T["preview"]) +st.components.v1.html( + f'
' + f'{svg_str}
', + height=svg_height + 40, + scrolling=True +) + +st.download_button( + label=T["download"], + data=svg_str.encode("utf-8"), + file_name=f"{title_text.replace(' ','_')}_zones.svg", + mime="image/svg+xml", + use_container_width=True, +) + +st.caption(T["tip"]) \ No newline at end of file