Uputstvo za korišćenje kompajliranog programa helix_gui

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

PoljeOpisPrimer 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 pravcuPomak centra baze elipse po X osi (za pomeranje baze).0.0
Pomak centra u Y pravcuPomak centra baze elipse po Y osi.0.0
Tolerancija presekaTolerancija 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 PLYIzaberi 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

  1. Unesi naziv fajla (npr. moja_kupola).
  2. Postavi broj spirala i segmenata po spirali.
  3. Odredi visinu kupole.
  4. 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).
  5. Po potrebi pomeri centar baze u X i Y.
  6. Postavi toleranciju preseka (preporučena vrednost ~0.02).
  7. Postavi visinu preseka Z (z_cut) da odseseš donji deo kupole ako želiš.
  8. Izaberi boju za .ply fajl.
  9. Klikni dugme Pokreni.
  10. Prati log u prozoru.
  11. 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

ParametarVrednost
Naziv izlaznog fajlakupola_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 X0.0
Pomak centra u Y0.0
Tolerancija preseka0.02
Z ravnina preseka (z_cut)2.0
Boja za PLYplava

Primer eliptične baze sa pomakom centra baze

ParametarVrednost
Naziv izlaznog fajlakupola_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 X4.0
Pomak centra u Y0
Tolerancija preseka0.02
Z ravnina preseka (z_cut)2.5
Boja za PLYnarandž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:

https://www.gradnja.rs/ove-montazne-modularne-kupole-po-projektu-domacih-arhitekata-izvodice-se-u-sad

Detaljne informacije kako da program napravim našao sam na sajtu:

https://www.simplydifferently.org/Helix_Zome

By Abel

Leave a Reply

Your email address will not be published. Required fields are marked *