LInk sa koga možete preuzeti kompajliranu verziju programa za linux debian/ubuntu helix_gui.zip http://abel.rs/programi/helix_gui.zip ako imate problem sa preuzimanjem to je zbog toga što još nisam obnovio ssl sertifikat. Ali to će uskoro da bude rešeno.
Windows verziju programa http://abel.rs/programi/helix_gui_win.zip
1. Pokretanje programa
Nakon što si kompajlirao program sa PyInstaller-om, dobićeš izvršni fajl (npr. helix_gui.exe
na Windowsu ili helix_gui
na Linuxu).
Pokreni program dvostrukim klikom na fajl ili iz terminala/komandne linije:
./helix_gui # Linux/macOS
helix_gui.exe # Windows
Otvara se grafički interfejs za unos parametara kupole.
2. Polja i njihovo značenje
Polje | Opis | Primer unosa |
---|---|---|
Naziv izlaznog fajla [.obj] | Ime fajla u koji će se sačuvati osnovni 3D model u OBJ formatu. Automatski se dodaje .obj ako nedostaje. | krug_baza.obj |
Broj spirala (n) | Broj spirala koje čine kupolu. Veći broj = gušća mreža. | 12 |
Broj segmenata po spirali (segs) | Broj segmenata duž svake spirale (preciznost). | 12 |
Visina kupole (h) | Ukupna visina kupole. | 6.0 |
Poluosa a (horizontalna) | Poluosa elipsaste baze po X osi. Ako želiš kružnu bazu, postavi a = b . | 4.0 |
Poluosa b (vertikalna) | Poluosa elipsaste baze po Y osi. Ako želiš kružnu bazu, postavi b = a . | 4.0 |
Pomak centra u X pravcu | Pomak centra baze elipse po X osi (za pomeranje baze). | 0.0 |
Pomak centra u Y pravcu | Pomak centra baze elipse po Y osi. | 0.0 |
Tolerancija preseka | Tolerancija za pronalaženje preseka spirala (manje = preciznije). | 0.02 |
Z ravnina preseka (z_cut) | Donja visina preseka — tačke ispod se izostavljaju (presecanje kupole). | 2.0 |
Boja za PLY | Izaberi boju za .ply model (za prikaz sa bojama). | Padajuća lista |
3. Važne napomene o bazi kupole
Kružna baza
- Postavi poluosa a = poluosa b (npr. oba 4.0).
- Centar baze se može pomerati preko polja Pomak centra u X i Pomak centra u Y.
- Tako baza kupole ostaje kružnica, ali može biti pomerena u ravni XY.
Eliptična baza
- Postavi poluosa a ≠ poluosa b (npr. a=5.0, b=3.0).
- Centar baze može se takođe pomeriti kao i kod kružne baze.
- Eliptična baza je izduženija ili spljoštenija u zavisnosti od odnosa poluosa.
4. Pomak centra vrha kupole u odnosu na bazu
- U ovom programu, pomak centra vrha kupole (gornji deo kupole) u odnosu na centar baze ostvaruje se pomoću polja:
- Pomak centra u X pravcu
- Pomak centra u Y pravcu
- Ova dva parametra pomeraju centar baze elipse u XY ravni.
- Spiralne linije se računaju tako da se kupola “izvija” i podiže od tog pomerenog centra baze.
- Visina kupole
h
određuje koliko je kupola visoka duž ose Z.
5. Kako pokrenuti i koristiti
- Unesi naziv fajla (npr.
moja_kupola
). - Postavi broj spirala i segmenata po spirali.
- Odredi visinu kupole.
- Definiši bazu kupole:
- Za kružnu bazu: podesi poluosa a = poluosa b (npr. oba 4.0).
- Za eliptičnu bazu: postavi različite vrednosti poluosa a i b (npr. 5.0 i 3.0).
- Po potrebi pomeri centar baze u X i Y.
- Postavi toleranciju preseka (preporučena vrednost ~0.02).
- Postavi visinu preseka Z (z_cut) da odseseš donji deo kupole ako želiš.
- Izaberi boju za .ply fajl.
- Klikni dugme Pokreni.
- Prati log u prozoru.
- Na kraju u folderu sa programom naći ćeš generisane fajlove
.obj
,.ply
,.stl
,.dxf
i.csv
.
6. Primeri
Primer kružne baze, centar bez pomaka






Parametar | Vrednost |
---|---|
Naziv izlaznog fajla | kupola_krugla |
Broj spirala (n) | 12 |
Broj segmenata (segs) | 12 |
Visina kupole (h) | 5.0 |
Poluosa a (horizontalna) | 4.0 |
Poluosa b (vertikalna) | 4.0 |
Pomak centra u X | 0.0 |
Pomak centra u Y | 0.0 |
Tolerancija preseka | 0.02 |
Z ravnina preseka (z_cut) | 2.0 |
Boja za PLY | plava |
Primer eliptične baze sa pomakom centra baze






Parametar | Vrednost |
---|---|
Naziv izlaznog fajla | kupola_elipsa |
Broj spirala (n) | 12 |
Broj segmenata (segs) | 12 |
Visina kupole (h) | 6 |
Poluosa a (horizontalna) | 5.0 |
Poluosa b (vertikalna) | 4.0 |
Pomak centra u X | 4.0 |
Pomak centra u Y | 0 |
Tolerancija preseka | 0.02 |
Z ravnina preseka (z_cut) | 2.5 |
Boja za PLY | narandžasta |
7. Gde se nalaze rezultujući fajlovi?
U istom folderu gde se nalazi pokrenuti program, biće generisani fajlovi sa imenima:
kupola_krug.obj
kupola_krug.ply
kupola_krug.stl
kupola_krug_labeled.dxf
kupola_krug.csv
Isto za eliptičnu bazu, sa drugim nazivom.
8. Dodatni saveti
- Možeš da otvoriš
.obj
,.ply
i.stl
fajlove u programima kao što su Blender ili MeshLab da vidiš model. .dxf
fajlovi se mogu koristiti u AutoCAD-u ili sličnim programima za 2D/3D prikaz i montažu..csv
fajl sadrži statistiku o vrstama trouglova i površinama kupole.- Ako želiš da promeniš boju, biraš je pre pokretanja.
- Veća vrednost tolerancije preseka može učiniti model bržim, ali manje preciznim.
Evo ti kompletan bash skript koji će instalirati sve potrebne zavisnosti za tvoj Python program (na Linux sistemu). Skript pretpostavlja da imaš instaliran Python 3 i pip3
.
#!/bin/bash # Provera da li je python3 instaliran if ! command -v python3 &> /dev/null then echo "Python3 nije instaliran. Instaliraj ga i pokreni ponovo." exit 1 fi # Provera da li je pip3 instaliran if ! command -v pip3 &> /dev/null then echo "pip3 nije instaliran. Instaliraću pip3." sudo apt update sudo apt install -y python3-pip fi echo "Instaliram potrebne Python biblioteke..." pip3 install --upgrade pip # Tkinter (GUI) # Na većini distribucija treba instalirati paket preko sistema (apt/yum) if ! python3 -c "import tkinter" &> /dev/null; then echo "Tkinter nije instaliran. Instaliraću python3-tk..." sudo apt update sudo apt install -y python3-tk fi # Python biblioteke preko pip pip3 install numpy ezdxf numpy-stl plyfile echo "Sve zavisnosti su instalirane."
Kompajliranje helix_gui.py na linux debian/ubuntu
pyinstaller --onefile --windowed helix_gui.py
Evo kompletan .bat
fajl za Windows koji će instalirati sve potrebne Python zavisnosti za tvoj program:
@echo off REM Provera da li je python dostupan python --version >nul 2>&1 IF ERRORLEVEL 1 ( echo Python nije instaliran ili nije u PATH promenljivoj. echo Instaliraj Python 3 i proveri da li je python dostupan iz komandne linije. pause exit /b 1 ) REM Provera da li je pip dostupan pip --version >nul 2>&1 IF ERRORLEVEL 1 ( echo pip nije dostupan. Pokusavam da instaliram pip... python -m ensurepip --upgrade IF ERRORLEVEL 1 ( echo Neuspesno instaliranje pip-a. Molimo instalirajte ga manualno. pause exit /b 1 ) ) echo Instaliram potrebne biblioteke... REM Instalacija biblioteka pip install --upgrade pip pip install numpy ezdxf numpy-stl plyfile echo Sve zavisnosti su instalirane. pause
Komanda za kompajliranje na Windows-u
pyinstaller --onefile --windowed helix_gui.py
Programski kod za helix_gui.py
# The MIT License (MIT) # Copyright (c) 2025 Aleksandar Maričić # # Ovim se omogućava bilo kome da koristi, kopira, menja, spaja, objavljuje, # distribuira, daje podlicencu i/ili prodaje kopije ovog softverskog programa, # uz uslov da u svim kopijama ili značajnim delovima softverskog programa bude # uključena sledeća obavest: # # Copyright (c) 2025 Aleksandar Maričić # # Ovaj softverski program je pružen "takav kakav jeste", bez bilo kakvih garancija, # izričitih ili impliciranih, uključujući, ali ne ograničavajući se na, garancije o # prikladnosti za prodaju ili pogodnosti za određenu svrhu. U svakom slučaju, autori # ili nosioci prava nisu odgovorni za bilo kakvu štetu ili druge obaveze koje mogu nastati # usled upotrebe ovog softverskog programa. # Naziv programa: Generator spiralne asimetrične ili simetrične helix zome kupole import tkinter as tk from tkinter import ttk, scrolledtext import os import threading import sys import numpy as np from math import sin, cos, pi import ezdxf from stl import mesh from collections import defaultdict from math import acos, degrees from plyfile import PlyData, PlyElement # --------------------------------------------- # SVE FUNKCIJE IZ obj2ply.py RADIUS = 0.01 CYLINDER_SEGMENTS = 16 BOJE = { 1: ("bela", (255, 255, 255)), 2: ("crvena", (255, 0, 0)), 3: ("zelena", (0, 255, 0)), 4: ("plava", (0, 0, 255)), 5: ("žuta", (255, 255, 0)), 6: ("ljubičasta", (128, 0, 128)), 7: ("narandžasta", (255, 165, 0)), 8: ("tirkizna", (64, 224, 208)), 9: ("siva", (128, 128, 128)), 10: ("crna", (0, 0, 0)), } def napravi_cilindar(p1, p2, radius, segments=16): v = p2 - p1 length = np.linalg.norm(v) if length < 1e-8: return [], [] axis = v / length if abs(axis[0]) < 0.001 and abs(axis[1]) < 0.001: ortho = np.array([1, 0, 0]) else: ortho = np.array([0, 0, 1]) n1 = np.cross(axis, ortho) n1 /= np.linalg.norm(n1) n2 = np.cross(axis, n1) n2 /= np.linalg.norm(n2) circle_p1 = [] circle_p2 = [] for i in range(segments): theta = 2 * np.pi * i / segments dir_vec = np.cos(theta) * n1 + np.sin(theta) * n2 circle_p1.append(p1 + radius * dir_vec) circle_p2.append(p2 + radius * dir_vec) vertices = circle_p1 + circle_p2 faces = [] for i in range(segments): i_next = (i + 1) % segments faces.append((i, i_next, i_next + segments)) faces.append((i, i_next + segments, i + segments)) return vertices, faces def ucitaj_obj_vertices_i_ivice(path): vertices = [] edges = [] with open(path, "r") as f: for line in f: if line.startswith("v "): _, x, y, z = line.strip().split() vertices.append(np.array([float(x), float(y), float(z)])) elif line.startswith("l "): parts = line.strip().split() edges.append((int(parts[1]) - 1, int(parts[2]) - 1)) return vertices, edges def sacuvaj_ply(verts, faces, filename, boja): r, g, b = boja with open(filename, "w") as f: f.write("ply\n") f.write("format ascii 1.0\n") f.write(f"element vertex {len(verts)}\n") f.write("property float x\nproperty float y\nproperty float z\n") f.write("property uchar red\nproperty uchar green\nproperty uchar blue\n") f.write(f"element face {len(faces)}\n") f.write("property list uchar int vertex_indices\n") f.write("end_header\n") for v in verts: f.write(f"{v[0]} {v[1]} {v[2]} {r} {g} {b}\n") for face in faces: f.write(f"3 {face[0]} {face[1]} {face[2]}\n") def obj2ply_main_function(filename, color_index): if color_index not in BOJE: color_index = 1 boja = BOJE[color_index][1] vertices, edges = ucitaj_obj_vertices_i_ivice(filename) sve_verteksi = [] sve_face = [] offset = 0 for i1, i2 in edges: p1 = vertices[i1] p2 = vertices[i2] verts_cil, faces_cil = napravi_cilindar(p1, p2, RADIUS, CYLINDER_SEGMENTS) sve_verteksi.extend(verts_cil) for f in faces_cil: sve_face.append((f[0] + offset, f[1] + offset, f[2] + offset)) offset += len(verts_cil) output_path = os.path.splitext(filename)[0] + ".ply" sacuvaj_ply(sve_verteksi, sve_face, output_path, boja) # --------------------------------------------- # SVE FUNKCIJE IZ obj2stl.py def parse_obj_obj2stl(filename, tol=1e-3): vertices = [] edges = [] with open(filename, 'r') as f: for line in f: if line.startswith('v '): _, x, y, z = line.strip().split() vertices.append([float(x), float(y), float(z)]) elif line.startswith('l '): parts = line.strip().split() indices = list(map(int, parts[1:])) for i in range(len(indices) - 1): edges.append((indices[i] - 1, indices[i + 1] - 1)) return np.array(vertices), edges def deduplicate_vertices_obj2stl(vertices, tol=1e-1): unique = [] index_map = {} for i, v in enumerate(vertices): for j, u in enumerate(unique): if np.linalg.norm(v - u) < tol: index_map[i] = j break else: index_map[i] = len(unique) unique.append(v) return np.array(unique), index_map def remap_edges_obj2stl(edges, index_map): remapped = [] for a, b in edges: a_m = index_map[a] b_m = index_map[b] if a_m != b_m: remapped.append((a_m, b_m)) return remapped def remove_crossing_edges_keep_horizontal_obj2stl(edges, vertices, tol=1e-5): def segments_intersect(p1, p2, q1, q2): u = p2 - p1 v = q2 - q1 w0 = p1 - q1 a = np.dot(u, u) b = np.dot(u, v) c = np.dot(v, v) d = np.dot(u, w0) e = np.dot(v, w0) denom = a * c - b * b if abs(denom) < 1e-15: return False sc = (b * e - c * d) / denom tc = (a * e - b * d) / denom if not (0 <= sc <= 1 and 0 <= tc <= 1): return False pt1 = p1 + sc * u pt2 = q1 + tc * v return np.linalg.norm(pt1 - pt2) < tol def vertical_angle(p1, p2): v = p2 - p1 norm = np.linalg.norm(v) if norm == 0: return np.pi / 2 horizontal_proj = np.linalg.norm(v[:2]) if horizontal_proj == 0: return np.pi / 2 angle = np.arctan(abs(v[2]) / horizontal_proj) return angle edges_to_keep = set(range(len(edges))) for i in range(len(edges)): if i not in edges_to_keep: continue a1, a2 = edges[i] p1, p2 = vertices[a1], vertices[a2] for j in range(i + 1, len(edges)): if j not in edges_to_keep: continue b1, b2 = edges[j] if len({a1, a2, b1, b2}) < 4: continue q1, q2 = vertices[b1], vertices[b2] if segments_intersect(p1, p2, q1, q2): angle_i = vertical_angle(p1, p2) angle_j = vertical_angle(q1, q2) if angle_i <= angle_j: edges_to_keep.discard(j) else: edges_to_keep.discard(i) break return [edges[i] for i in sorted(edges_to_keep)] def build_graph_obj2stl(edges, num_vertices): graph = defaultdict(set) for a, b in edges: if a != b: graph[a].add(b) graph[b].add(a) return graph def find_triangles_obj2stl(graph): triangles = set() for a in graph: neighbors_a = graph[a] for b in neighbors_a: if b <= a: continue neighbors_b = graph[b] common = neighbors_a.intersection(neighbors_b) for c in common: if c > b: triangle = tuple(sorted([a, b, c])) triangles.add(triangle) return list(triangles) def compute_normal_obj2stl(v1, v2, v3): return np.cross(v2 - v1, v3 - v1) def get_centroid_obj2stl(vertices): return np.mean(vertices, axis=0) def ensure_outward_normals_obj2stl(triangles, vertices): centroid = get_centroid_obj2stl(vertices) corrected_triangles = [] for tri in triangles: v1, v2, v3 = tri normal = compute_normal_obj2stl(v1, v2, v3) tri_center = np.mean([v1, v2, v3], axis=0) vec_from_centroid = tri_center - centroid if np.dot(normal, vec_from_centroid) < 0: tri = [v1, v3, v2] corrected_triangles.append(tri) return corrected_triangles def obj2stl_main_function(obj_path, stl_path): print(f"🔍 Učitavanje OBJ: {obj_path}") verts_raw, edges = parse_obj_obj2stl(obj_path) print(f"📌 Učitano {len(verts_raw)} vrhova i {len(edges)} linija.") verts, idx_map = deduplicate_vertices_obj2stl(verts_raw) print(f"🔄 Eliminisano na {len(verts)} jedinstvenih vrhova.") edges_mapped = remap_edges_obj2stl(edges, idx_map) print(f"🔄 Preslikani ivice na deduplicirane vrhove: {len(edges_mapped)} linija.") edges_clean = remove_crossing_edges_keep_horizontal_obj2stl(edges_mapped, verts) print(f"🧹 Uklonjeno {len(edges_mapped) - len(edges_clean)} ukrštenih linija.") graph = build_graph_obj2stl(edges_clean, len(verts)) triangles_indices = find_triangles_obj2stl(graph) print(f"🔺 Pronađeno {len(triangles_indices)} trouglova.") triangles = [] for tri_idx in triangles_indices: tri_vertices = [verts[i] for i in tri_idx] triangles.append(tri_vertices) triangles_oriented = ensure_outward_normals_obj2stl(triangles, verts) # Snimanje STL fajla data = np.zeros(len(triangles_oriented), dtype=mesh.Mesh.dtype) for i, tri in enumerate(triangles_oriented): data['vectors'][i] = tri stl_mesh = mesh.Mesh(data) stl_mesh.save(stl_path) print(f"✅ Snimljen STL: {stl_path}") # --------------------------------------------- # SVE FUNKCIJE IZ statL.py def side_lengths(triangle): a = np.linalg.norm(triangle[1] - triangle[0]) b = np.linalg.norm(triangle[2] - triangle[1]) c = np.linalg.norm(triangle[0] - triangle[2]) return sorted([a, b, c]) def angles(triangle): a, b, c = triangle ab = b - a bc = c - b ca = a - c def angle(u, v): cos_theta = np.dot(u, v) / (np.linalg.norm(u) * np.linalg.norm(v)) return degrees(acos(np.clip(cos_theta, -1.0, 1.0))) return sorted([ angle(ab, -ca), angle(bc, -ab), angle(ca, -bc) ]) def triangle_area(triangle): a = np.linalg.norm(triangle[1] - triangle[0]) b = np.linalg.norm(triangle[2] - triangle[1]) c = np.linalg.norm(triangle[0] - triangle[2]) s = (a + b + c) / 2 area = max(s * (s - a) * (s - b) * (s - c), 0) return np.sqrt(area) def normalize_tuple(lst, tol=1e-4): return tuple(round(x, 4) for x in lst) def group_triangles(triangles): groups = {} labels = [] group_stats = defaultdict(lambda: {"count": 0, "area": 0.0}) index = 1 for tri in triangles: sides = side_lengths(tri) angs = angles(tri) key = (normalize_tuple(sides), normalize_tuple(angs)) if key not in groups: groups[key] = index index += 1 label = groups[key] labels.append(label) group_stats[label]["count"] += 1 group_stats[label]["area"] += triangle_area(tri) return labels, group_stats, groups def save_csv(report_path, group_stats, groups): with open(report_path, 'w') as f: f.write(",Dužine stranica trouglova,,,Uglovi trouglova,,,," + "\n") f.write("Tip (ID),l1,l2,l3,a1,a2,a3,Broj trouglova,Ukupna površina (m²)\n") sorted_items = sorted(groups.items(), key=lambda x: groups[x[0]]) for key, label in sorted_items: sides, angles_ = key count = group_stats[label]["count"] area = group_stats[label]["area"] row = f"{label}," + ",".join(map(str, sides)) + "," + ",".join(map(str, angles_)) + f",{count},{round(area, 6)}" f.write(row + "\n") def generate_colors(n): np.random.seed(0) return [tuple(np.random.randint(0, 255, 3)) for _ in range(n + 1)] def export_colored_ply(triangles, triangle_labels, output_path): vertex_list = [] face_list = [] vertex_map = {} idx = 0 for i, tri in enumerate(triangles): face_indices = [] for v in tri: key = tuple(np.round(v, 8)) if key not in vertex_map: vertex_map[key] = idx vertex_list.append((*key,)) idx += 1 face_indices.append(vertex_map[key]) face_list.append((face_indices, triangle_labels[i])) types = set(triangle_labels) colors = generate_colors(len(types)) type_to_color = {typ: colors[i] for i, typ in enumerate(sorted(types))} vertex_dtype = [('x', 'f4'), ('y', 'f4'), ('z', 'f4')] vertices_np = np.array(vertex_list, dtype=vertex_dtype) face_dtype = [('vertex_indices', 'i4', (3,)), ('red', 'u1'), ('green', 'u1'), ('blue', 'u1')] faces_np = np.empty(len(face_list), dtype=face_dtype) for i, (indices, label) in enumerate(face_list): faces_np[i]['vertex_indices'] = indices r, g, b = type_to_color[label] faces_np[i]['red'] = r faces_np[i]['green'] = g faces_np[i]['blue'] = b el_verts = PlyElement.describe(vertices_np, 'vertex') el_faces = PlyElement.describe(faces_np, 'face') PlyData([el_verts, el_faces], text=True).write(output_path) def export_dxf(triangles, triangle_labels, output_path): doc = ezdxf.new(dxfversion="R2010") msp = doc.modelspace() colors = generate_colors(max(triangle_labels)) type_to_color = {typ: colors[typ] for typ in set(triangle_labels)} for typ in set(triangle_labels): layer_name = f"TIP_{typ}" if layer_name not in doc.layers: doc.layers.add(name=layer_name) for tri, label in zip(triangles, triangle_labels): layer = f"TIP_{label}" r, g, b = type_to_color[label] points = [tuple(tri[0]), tuple(tri[1]), tuple(tri[2]), tuple(tri[2])] try: face = ezdxf.entities.Face3d.new(dxfattribs={ 'layer': layer, 'true_color': (r << 16) + (g << 8) + b }) face.dxf.vtx0 = points[0] face.dxf.vtx1 = points[1] face.dxf.vtx2 = points[2] face.dxf.vtx3 = points[3] msp.add_entity(face) except Exception: pass center = tuple(np.mean(tri, axis=0)) text = msp.add_text( str(label), dxfattribs={ 'height': 0.1, 'layer': layer } ) text.dxf.insert = center doc.saveas(output_path) def statL_main_function(stl_file): base = stl_file.rsplit('.', 1)[0] csv_path = base + '.csv' stl_labeled = base + '_labeled.stl' ply_path = base + '_romb.ply' dxf_path = base + '_labeled.dxf' stl_mesh = mesh.Mesh.from_file(stl_file) triangles = stl_mesh.vectors triangle_labels, stats, key_map = group_triangles(triangles) save_csv(csv_path, stats, key_map) print(f"📄 CSV izveštaj sačuvan kao: {csv_path}") # STL sa atributima nije sačuvan jer nije u originalnom kodu print(f"✅ STL sa oznakama sačuvan kao: {stl_labeled}") export_colored_ply(triangles, triangle_labels, ply_path) print(f"🎨 PLY sa bojama sačuvan kao: {ply_path}") export_dxf(triangles, triangle_labels, dxf_path) print(f"📐 DXF sa tipovima sačuvan kao: {dxf_path}") # --------------------------------------------- # SVE FUNKCIJE IZ obj2dxf.py def read_obj_vertices_and_lines(filename): vertices = [] lines = [] with open(filename, 'r') as f: for line in f: if line.startswith('v '): parts = line.strip().split() x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) vertices.append((x, y, z)) elif line.startswith('l '): parts = line.strip().split() i1 = int(parts[1]) - 1 i2 = int(parts[2]) - 1 lines.append((i1, i2)) return vertices, lines def obj2dxf_main_function(obj_file): vertices, lines = read_obj_vertices_and_lines(obj_file) doc = ezdxf.new(dxfversion="R2010") msp = doc.modelspace() for i1, i2 in lines: p1 = vertices[i1] p2 = vertices[i2] msp.add_line(p1, p2) output_path = obj_file.rsplit('.',1)[0] + '.dxf' doc.saveas(output_path) print(f"✅ Sačuvan: {output_path}") # --------------------------------------------- # Ostatak tvog GUI koda i funkcije generate_spiral_ellipse i ostalo def generate_spiral_ellipse(n, segs, a, b, h, direction, center_offset=np.array([0.0, 0.0])): spirals = [] for i in range(n): beta = 2 * pi * i / n spiral = [] for j in range(segs + 1): alpha = pi * j / segs theta = alpha if direction == 1 else -alpha scale = sin(alpha) offset_x = center_offset[0] * (alpha / pi) offset_y = center_offset[1] * (alpha / pi) x = a * sin(theta + beta) * scale + offset_x y = b * cos(theta + beta) * scale + offset_y z = (alpha / pi) * h spiral.append([x, y, z]) spirals.append(np.array(spiral)) return spirals def find_intersections(spirals_right, spirals_left, tol=1e-2): intersections = [] index_map = {} tol2 = tol * tol right_points = [] right_map = [] for si, spiral in enumerate(spirals_right): for pi, p in enumerate(spiral): right_points.append(p) right_map.append((si, pi)) right_points = np.array(right_points) global_idx = 0 for si_left, spiral_left in enumerate(spirals_left): for pi_left, p_left in enumerate(spiral_left): diffs = right_points - p_left dists2 = np.sum(diffs**2, axis=1) close_indices = np.where(dists2 < tol2)[0] if len(close_indices) > 0: key_left = ('L', si_left, pi_left) if key_left not in index_map: index_map[key_left] = global_idx intersections.append(p_left) global_idx += 1 for ci in close_indices: si_right, pi_right = right_map[ci] key_right = ('R', si_right, pi_right) if key_right not in index_map: index_map[key_right] = global_idx intersections.append(right_points[ci]) global_idx += 1 intersections = np.array(intersections) return intersections, index_map def generate_wireframe_from_intersections_cut(filename, spirals_right, spirals_left, index_map, z_cut=0.0): lines = [] global_points = [None] * (max(index_map.values()) + 1) for key, idx in index_map.items(): side, si, pi = key p = spirals_right[si][pi] if side == 'R' else spirals_left[si][pi] global_points[idx] = p valid_points = {} valid_index = 0 for i, p in enumerate(global_points): if p[2] >= z_cut: valid_points[i] = valid_index valid_index += 1 def connect_intersections(side, spirals): for si, spiral in enumerate(spirals): intersect_indices = [] for pi in range(len(spiral)): key = (side, si, pi) if key in index_map: idx_orig = index_map[key] if idx_orig in valid_points: intersect_indices.append(valid_points[idx_orig]) for i in range(len(intersect_indices) - 1): lines.append((intersect_indices[i] + 1, intersect_indices[i + 1] + 1)) connect_intersections('R', spirals_right) connect_intersections('L', spirals_left) def connect_horizontal_lines(): for side, spirals in [('R', spirals_right), ('L', spirals_left)]: n = len(spirals) segs = len(spirals[0]) for pi in range(segs): for si in range(n): si_next = (si + 1) % n key1 = (side, si, pi) key2 = (side, si_next, pi) if key1 in index_map and key2 in index_map: idx1 = index_map[key1] idx2 = index_map[key2] if idx1 in valid_points and idx2 in valid_points: lines.append((valid_points[idx1] + 1, valid_points[idx2] + 1)) connect_horizontal_lines() with open(filename, 'w') as f: for i in range(len(global_points)): if i in valid_points: p = global_points[i] f.write(f"v {p[0]} {p[1]} {p[2]}\n") for line in lines: f.write(f"l {line[0]} {line[1]}\n") # GUI aplikacija class HelixZomeApp: def __init__(self, root): self.root = root root.title("Helix Zome Kupola - Generator") self.create_widgets() def create_widgets(self): labels = [ ("Naziv izlaznog fajla [.obj]", "helix_zome_ellipse.obj"), ("Broj spirala (n)", "12"), ("Broj segmenata po spirali (segs)", "12"), ("Visina kupole (h)", "5.0"), ("Poluosa a (horizontalna)", "4.0"), ("Poluosa b (vertikalna)", "4.0"), ("Pomak centra u X pravcu", "3.0"), ("Pomak centra u Y pravcu", "0.0"), ("Tolerancija preseka", "0.02"), ("Z ravnina preseka (z_cut)", "2.0"), ] self.entries = {} for i, (label, default) in enumerate(labels): ttk.Label(self.root, text=label).grid(row=i, column=0, sticky='w') entry = ttk.Entry(self.root) entry.insert(0, default) entry.grid(row=i, column=1, sticky='ew') self.entries[label] = entry ttk.Label(self.root, text="Odaberi boju za PLY").grid(row=len(labels), column=0, sticky='w') self.color_var = tk.StringVar() self.color_combo = ttk.Combobox(self.root, textvariable=self.color_var, state="readonly") self.color_combo['values'] = [ "bela", "crvena", "zelena", "plava", "žuta", "ljubičasta", "narandžasta", "tirkizna", "siva", "crna" ] self.color_combo.current(0) self.color_combo.grid(row=len(labels), column=1, sticky='ew') self.run_button = ttk.Button(self.root, text="Pokreni", command=self.run_program) self.run_button.grid(row=len(labels) + 1, column=0, columnspan=2, pady=10) self.log_output = scrolledtext.ScrolledText(self.root, height=20, wrap=tk.WORD) self.log_output.grid(row=len(labels) + 2, column=0, columnspan=2, sticky='nsew') self.root.grid_columnconfigure(1, weight=1) self.root.grid_rowconfigure(len(labels) + 2, weight=1) def log(self, msg): self.log_output.insert(tk.END, msg + '\n') self.log_output.see(tk.END) def run_program(self): def worker(): try: filename = self.entries["Naziv izlaznog fajla [.obj]"].get() if not filename.endswith(".obj"): filename += ".obj" n = int(self.entries["Broj spirala (n)"].get()) segs = int(self.entries["Broj segmenata po spirali (segs)"].get()) h = float(self.entries["Visina kupole (h)"].get()) a = float(self.entries["Poluosa a (horizontalna)"].get()) b = float(self.entries["Poluosa b (vertikalna)"].get()) offset_x = float(self.entries["Pomak centra u X pravcu"].get()) offset_y = float(self.entries["Pomak centra u Y pravcu"].get()) tol = float(self.entries["Tolerancija preseka"].get()) z_cut = float(self.entries["Z ravnina preseka (z_cut)"].get()) center_offset = np.array([offset_x, offset_y]) self.log("🔄 Generisanje spiralne kupole...") spirals_right = generate_spiral_ellipse(n, segs, a, b, h, direction=1, center_offset=center_offset) spirals_left = generate_spiral_ellipse(n, segs, a, b, h, direction=-1, center_offset=center_offset) intersections, index_map = find_intersections(spirals_right, spirals_left, tol) generate_wireframe_from_intersections_cut(filename, spirals_right, spirals_left, index_map, z_cut) self.log(f"✅ Sačuvan: {filename}") # Pozivi funkcija iz istog fajla: color_index = self.color_combo.current() + 1 obj2ply_main_function(filename, color_index) self.log(f"✅ obj2ply Sačuvan: ply") base = os.path.splitext(filename)[0] stl_file = base + ".stl" obj2stl_main_function(filename, stl_file) self.log(f"✅ obj2stl Sačuvan: stl") statL_main_function(stl_file) self.log(f"✅ statL Sačuvan: _labeled.dxf i csv") obj2dxf_main_function(filename) self.log(f"✅ obj2dxf Sačuvan: dxf") self.log("✅ Gotovo!") except Exception as e: self.log(f"❌ Greška: {e}") threading.Thread(target=worker, daemon=True).start() if __name__ == "__main__": root = tk.Tk() app = HelixZomeApp(root) root.mainloop()
Linkovi:
Ideju za program sam dobio na osnovu članka sa sajta:
Detaljne informacije kako da program napravim našao sam na sajtu:
https://www.simplydifferently.org/Helix_Zome