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"])