Arburg-Thermoplay-Wiring/app/main.py

375 lines
16 KiB
Python
Raw Permalink Normal View History

2026-03-12 13:00:04 +00:00
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 <path> element (no <text> 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'<path d="{" ".join(d)}" fill="{fill}" stroke="none"/>'
# ─── 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 <text> — 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'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {svg_w} {svg_h}" '
f'width="{svg_w}" height="{svg_h}" '
f'style="background:{bg}">')
L.append(f'<defs><style>'
f'line,polyline{{stroke:{stroke};fill:none;stroke-width:1.8;}}'
f'rect.heater{{fill:{bg};stroke:{stroke};stroke-width:1.8;}}'
f'circle.terminal{{fill:{bg};stroke:{stroke};stroke-width:1.8;}}'
f'circle.junc{{fill:{bg};stroke:{stroke};stroke-width:1.8;}}'
f'.cross{{stroke:{inactive_col};stroke-width:4;stroke-linecap:round;opacity:0.85;}}'
f'</style></defs>')
# 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'<line x1="{ox-2}" y1="0" x2="{ox-2}" y2="{svg_h}" '
f'stroke="{stroke}" stroke-width="1.5"/>')
# ── Heater ──
L.append(f'<circle cx="{hx}" cy="{TOP_Y}" r="{CR}" class="terminal"/>')
L.append(tp(str(t1), hx, TOP_Y, fs_num, 'middle', stroke))
L.append(f'<line x1="{hx}" y1="{TOP_Y+CR}" x2="{hx}" y2="{HR_TOP}"/>')
L.append(f'<rect x="{ox+HR_X}" y="{HR_TOP}" width="{HR_W}" height="{HR_H}" class="heater"/>')
if dividers > 0:
step = HR_H / (dividers + 1)
for d in range(1, dividers + 1):
dy = int(HR_TOP + step * d)
L.append(f'<line x1="{ox+HR_X+4}" y1="{dy}" x2="{ox+HR_X+HR_W-4}" y2="{dy}"/>')
L.append(f'<line x1="{hx}" y1="{HR_TOP+HR_H}" x2="{hx}" y2="{BOT_Y-CR}"/>')
L.append(f'<circle cx="{hx}" cy="{BOT_Y}" r="{CR}" class="terminal"/>')
L.append(tp(str(t2), hx, BOT_Y, fs_num, 'middle', stroke))
# ── Thermocouple ──
L.append(f'<circle cx="{tx}" cy="{TOP_Y}" r="{CR}" class="terminal"/>')
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'<line x1="{tx}" y1="{TOP_Y+CR}" x2="{tx}" y2="{chevron_top}"/>')
tip_x = tx + tip_dx
L.append(f'<polyline points="{tx},{chevron_top} {tip_x},{junc_y}"/>')
L.append(f'<circle cx="{tip_x}" cy="{junc_y}" r="{max(5,int(zone_w*0.027))}" class="junc"/>')
L.append(f'<polyline points="{tip_x},{junc_y} {tx},{chevron_bot}"/>')
L.append(f'<line x1="{tx}" y1="{chevron_bot}" x2="{tx}" y2="{BOT_Y-CR}"/>')
if show_pol:
L.append(tp('-', tx + CR // 2 + fs_pol // 2, int(svg_h * 0.724), fs_pol, 'start', stroke))
L.append(f'<circle cx="{tx}" cy="{BOT_Y}" r="{CR}" class="terminal"/>')
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'<line x1="{x0}" y1="{CROSS_TOP}" x2="{x1}" y2="{CROSS_BOT}" class="cross"/>')
L.append(f'<line x1="{x1}" y1="{CROSS_TOP}" x2="{x0}" y2="{CROSS_BOT}" class="cross"/>')
# ── 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('</svg>')
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'<div style="overflow-x:auto;background:#e0e0e0;padding:12px;border-radius:8px">'
f'{svg_str}</div>',
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"])