Program helix.py služi za generisanje žičanog modela spiralne kupole na osnovu ukrštanja dve grupe spirala – levih i desnih – koje se prostiru duž površine polukupole. Ove spirale se modeluju matematički, a njihova ukrštanja formiraju mrežu tačaka (čvorišta) koja može da se koristi za dalje modeliranje, analizu ili vizualizaciju geometrijskih struktura.
Funkcionalnosti:
- Korisnik unosi:
- broj spirala po smeru (leva i desna),
- broj segmenata po spirali,
- prečnik baze kupole,
- visinu kupole,
- naziv izlaznog
.objfajla (fajl sa 3D tačkama i linijama).
- Program automatski generiše spirale u prostoru.
- Izračunava se gde se spirale međusobno seku (preseci se određuju uz malu toleranciju).
- Formira se mreža povezivanjem tih presečnih tačaka.
- Na kraju, rezultat se čuva u standardnom OBJ fajl formatu (kompatibilan sa mnogim 3D softverima poput Blender-a).
Upotreba:
Pokrenite program u komandnoj liniji:
python helix.py
Zatim pratite uputstva za unos parametara. Ako pritisnete Enter bez unosa, koristiće se podrazumevane vrednosti.
Instalacija potrebnih zavisnosti:
python3 -m pip install numpy scipy
Programski kod helix.py:
#helix.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 KUPOLE
# Instalacija potrebnih zavisnosti:python3 -m pip install numpy scipy
import numpy as np
from math import sin, cos, pi
from scipy.spatial import cKDTree
from collections import defaultdict, deque
def generate_spiral(n, segs, d, h, direction):
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
x = sin(theta + beta) * d/4 + sin(beta)*d/4
y = cos(theta + beta) * d/4 + cos(beta)*d/4
z = (alpha/pi) * h
spiral.append([x, y, z])
spirals.append(np.array(spiral))
return spirals
def find_intersections(spirals1, spirals2, tol=1e-4):
points1 = np.vstack(spirals1)
tree = cKDTree(points1)
intersections = []
for spiral in spirals2:
for p in spiral:
dist, idx = tree.query(p, distance_upper_bound=tol)
if dist < tol:
match = points1[idx]
intersections.append(tuple(np.round(match, 5)))
return list(dict.fromkeys(intersections))
def build_graph(intersections, spirals, tol=1e-4):
point_idx = {p: i + 1 for i, p in enumerate(intersections)}
edges = set()
for spiral in spirals:
prev = None
for p in spiral:
p_rounded = tuple(np.round(p, 5))
if p_rounded in point_idx:
if prev is not None:
edges.add((min(prev, point_idx[p_rounded]), max(prev, point_idx[p_rounded])))
prev = point_idx[p_rounded]
return point_idx, edges
def sort_face_vertices(face_indices, vertex_lookup):
points = np.array([vertex_lookup[i] for i in face_indices])
center = np.mean(points, axis=0)
v1 = points[0] - center
normal = np.cross(points[1] - points[0], points[2] - points[0])
normal /= np.linalg.norm(normal)
axis_x = v1 / np.linalg.norm(v1)
axis_y = np.cross(normal, axis_x)
angles = []
for p in points:
vec = p - center
x = np.dot(vec, axis_x)
y = np.dot(vec, axis_y)
angle = np.arctan2(y, x)
angles.append(angle)
sorted_indices = [i for _, i in sorted(zip(angles, face_indices))]
return sorted_indices
def face_normal(face, vertex_lookup):
p0 = np.array(vertex_lookup[face[0]])
p1 = np.array(vertex_lookup[face[1]])
p2 = np.array(vertex_lookup[face[2]])
normal = np.cross(p1 - p0, p2 - p0)
norm = np.linalg.norm(normal)
if norm == 0:
return np.array([0,0,0])
return normal / norm
def get_base_center(vertex_lookup):
base_points = [v for v in vertex_lookup.values() if abs(v[2]) < 1e-5]
if base_points:
return np.mean(base_points, axis=0)
return np.array([0,0,0])
def ensure_face_orientation(face, vertex_lookup, base_center):
normal = face_normal(face, vertex_lookup)
centroid = np.mean([vertex_lookup[v] for v in face], axis=0)
vec = centroid - base_center
if np.dot(normal, vec) < 0:
face = face[::-1]
return face
def build_rhombus_faces(edges, vertex_lookup, edge_tol=1e-3):
neighbors = defaultdict(set)
for a, b in edges:
neighbors[a].add(b)
neighbors[b].add(a)
def edge_length(u, v):
pu, pv = np.array(vertex_lookup[u]), np.array(vertex_lookup[v])
return np.linalg.norm(pu - pv)
result_faces = []
base_center = get_base_center(vertex_lookup)
for a in neighbors:
for b in neighbors[a]:
if b <= a:
continue
for c in neighbors[b]:
if c in (a,b):
continue
if c not in neighbors:
continue
for d in neighbors[c]:
if d in (a,b,c):
continue
if a in neighbors[d]:
lengths = [
edge_length(a,b),
edge_length(b,c),
edge_length(c,d),
edge_length(d,a)
]
if max(lengths) - min(lengths) < edge_tol:
sorted_face = sort_face_vertices([a,b,c,d], vertex_lookup)
oriented_face = ensure_face_orientation(sorted_face, vertex_lookup, base_center)
if oriented_face not in result_faces:
result_faces.append(oriented_face)
return result_faces
def face_to_triangles(face):
# face je lista 4 indeksa - podeli na dva trougla
return [
[face[0], face[1], face[2]],
[face[0], face[2], face[3]]
]
def build_triangle_adjacency(triangles):
edge_to_faces = defaultdict(list)
for i, tri in enumerate(triangles):
edges = [
tuple(sorted((tri[0], tri[1]))),
tuple(sorted((tri[1], tri[2]))),
tuple(sorted((tri[2], tri[0])))
]
for e in edges:
edge_to_faces[e].append(i)
adjacency = defaultdict(set)
for edge, f_list in edge_to_faces.items():
if len(f_list) == 2:
a, b = f_list
adjacency[a].add(b)
adjacency[b].add(a)
return adjacency
def flip_face(face):
return [face[0], face[2], face[1]]
def calc_normal(v1, v2, v3):
v1 = np.array(v1)
v2 = np.array(v2)
v3 = np.array(v3)
normal = np.cross(v2 - v1, v3 - v1)
norm = np.linalg.norm(normal)
if norm == 0:
return np.array([0,0,0])
return normal / norm
def consistent_orientation(vertices, triangles):
adjacency = build_triangle_adjacency(triangles)
visited = set()
queue = deque([0])
visited.add(0)
while queue:
curr = queue.popleft()
curr_face = triangles[curr]
curr_normal = calc_normal(vertices[curr_face[0]-1], vertices[curr_face[1]-1], vertices[curr_face[2]-1])
for neighbor in adjacency[curr]:
if neighbor in visited:
continue
neigh_face = triangles[neighbor]
shared = set(curr_face) & set(neigh_face)
if len(shared) != 2:
continue
def edge_order(face, edge):
for i in range(3):
e = (face[i], face[(i+1)%3])
if set(e) == set(edge):
return e
return None
shared_edge = tuple(shared)
curr_edge = edge_order(curr_face, shared_edge)
neigh_edge = edge_order(neigh_face, shared_edge)
if curr_edge is None or neigh_edge is None:
continue
# Ako su oba u istom smeru, okreni suseda
if curr_edge == neigh_edge:
triangles[neighbor] = flip_face(neigh_face)
visited.add(neighbor)
queue.append(neighbor)
return triangles
def export_obj(filename, intersections, edges, faces):
with open(filename, 'w') as f:
for p in intersections:
f.write(f"v {p[0]} {p[1]} {p[2]}\n")
for a,b in edges:
f.write(f"l {a} {b}\n")
for face in faces:
f.write("f " + " ".join(str(idx) for idx in face) + "\n")
print(f" Sačuvan OBJ fajl: {filename}")
def unos_broja(prompt, podrazumevano, tip=float):
while True:
unos = input(f"{prompt} [{podrazumevano}]: ").strip()
if unos == "":
return podrazumevano
try:
return tip(unos)
except ValueError:
print(" Neispravan unos, pokušaj ponovo.")
def main():
print(" GENERATOR SPIRALNE KUPOLE\n")
n = unos_broja("Unesi broj spirala po smeru", 24, int)
segs = unos_broja("Unesi broj segmenata po spirali", 24, int)
d = unos_broja("Unesi prečnik kupole", 4.0)
h = unos_broja("Unesi visinu kupole", 2.0)
filename = input("Unesi naziv izlaznog OBJ fajla [helix.obj]: ").strip()
if filename == "":
filename = "helix.obj"
if not filename.endswith(".obj"):
filename += ".obj"
tol = 1e-3
spirals_right = generate_spiral(n, segs, d, h, direction=1)
spirals_left = generate_spiral(n, segs, d, h, direction=-1)
intersections = find_intersections(spirals_right, spirals_left, tol=tol)
if not intersections:
print(" Nema presečnih tačaka!")
return
all_spirals = spirals_right + spirals_left
point_idx, edges = build_graph(intersections, all_spirals, tol=tol)
# Napravi reverzni lookup: index -> koordinata
vertex_lookup = {idx: pt for pt, idx in point_idx.items()}
faces = build_rhombus_faces(edges, vertex_lookup, edge_tol=tol)
if not faces:
print(" Nema pronađenih faces!")
return
# Razdvoji faces (četvorke) u trouglove za orijentaciju
triangles = []
for face in faces:
tris = face_to_triangles(face)
triangles.extend(tris)
# Ispravi globalnu orijentaciju trouglova
triangles = consistent_orientation(intersections, triangles)
# Ponovo spoji trouglove u četvorke za OBJ fajl
# (Ovde ćemo eksportovati trouglove jer je OBJ standard fleksibilan)
# Ali ako želiš, možeš grupisati trouglove u četvorke,
# ili eksportovati trouglove direktno.
# Eksportuj OBJ sa trouglovima i linijama
with open(filename, 'w') as f:
for p in intersections:
f.write(f"v {p[0]} {p[1]} {p[2]}\n")
for a,b in edges:
f.write(f"l {a} {b}\n")
for tri in triangles:
f.write("f " + " ".join(str(idx) for idx in tri) + "\n")
print(f" Sačuvan OBJ fajl sa pravilno orijentisanim trouglovima: {filename}")
if __name__ == "__main__":
main()
Program obj2ply.py

Opis programa obj2ply.py
Program obj2ply.py služi za konverziju jednostavnih žičanih 3D modela (OBJ format sa tačkama i linijama) u telo sastavljeno od cilindričnih šipki, koje se čuvaju u PLY formatu pogodnom za 3D štampu, vizualizaciju i obradu.
Čemu služi:
- Pretvara linije (
l) iz.objfajla u trodimenzionalne cilindre sa zadatom debljinom (promenljivaRADIUS). - Svaka linija između dve tačke postaje geometrijski oblik (cilindar) sa površinama.
- Izlaz je
.plyfajl sa kompletnom geometrijom šipkaste strukture.
Kako se koristi:
Pokretanje iz komandne linije:
python3 obj2ply.py ime_fajla.obj
Tokom pokretanja programa korisnik bira boju kojom će biti obojen ceo model. Boja se unosi izborom iz ponuđene liste, a zatim se sačuva u PLY fajlu kao RGB vrednosti za svaki vertex.
Program:
- Učita tačke i ivice iz
.objfajla. - Za svaku ivicu napravi cilindar između odgovarajućih tačaka.
- Sačuva rezultat u
.plyfajl sa istim imenom.
Napomena:
- Podržani su samo
.objfajlovi koji sadrže definisane tačke (v) i linije (l). - Debljina šipki i broj segmenata mogu se podesiti direktno u kodu (
RADIUS,CYLINDER_SEGMENTS).
Instalacija potrebnih zavisnosti:
python3 -m pip install numpy
Kod programa obj2ply.py:
#obj2ply.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: KONVERTOR IZ OBJ U PLY
# Instalacija potrebnih zavisnosti:python3 -m pip install numpy
import numpy as np
import sys
from pathlib import Path
RADIUS = 0.01
CYLINDER_SEGMENTS = 16
# Definiši dostupne boje kao RGB (0-255)
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 prikazi_meni_i_uzmi_boju():
print("Izaberite boju modela:")
for broj, (ime, rgb) in sorted(BOJE.items()):
print(f"{broj}. {ime} (RGB: {rgb})")
while True:
try:
izbor = int(input("Unesite broj boje: "))
if izbor in BOJE:
return BOJE[izbor][1]
else:
print("Nepoznat broj boje, pokušajte ponovo.")
except ValueError:
print("Molimo unesite ceo broj.")
def ucitaj_obj(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 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 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")
# Dodajemo RGB atribute
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 main():
if len(sys.argv) != 2:
print("Upotreba: python3 obj2ply.py ulazni_fajl.obj")
return
input_path = Path(sys.argv[1])
if not input_path.exists():
print(f"Fajl ne postoji: {input_path}")
return
boja = prikazi_meni_i_uzmi_boju()
output_path = input_path.with_suffix(".ply")
vertices, edges = ucitaj_obj(input_path)
print(f"Učitano {len(vertices)} tačaka i {len(edges)} ivica iz {input_path}")
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)
sacuvaj_ply(sve_verteksi, sve_face, output_path, boja)
print(f"Ply fajl sa šipkama sačuvan kao: {output_path}")
if __name__ == "__main__":
main()
Program obj2dxf.py

Opis programa obj2dxf.py
Program obj2dxf.py služi za konvertovanje jednostavnih 3D žičanih modela iz formata OBJ (sa tačkama i linijama) u 2D/3D DXF format, koji se često koristi u CAD softverima.
Čemu služi:
- Učitava koordinate tačaka i linija iz
.objfajla. - Kreira DXF fajl gde su linije modela rekonstruisane kao DXF entiteti.
- Omogućava dalju CAD obradu i prikaz u programima koji podržavaju DXF.
Kako se koristi:
Pokrenite program komandno sa:
python3 obj2dxf.py ime_fajla.obj
Program:
- Učita
.objfajl sa definisanim vrhovima i linijama. - Sačuva DXF fajl sa istim imenom i ekstenzijom
.dxf.
Napomena:
- Podržani su samo
.objfajlovi sa tačkama (v) i linijama (l). - Rezultat je DXF fajl u verziji R2010.
Instalacija potrebnih zavisnosti:
python3 -m pip install ezdxf
Programski kod obj2dxf.py
#obj2dxf.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: KONVERTOR IZ OBJ U DXF
# Instalacija potrebnih zavisnosti:python3 -m pip install ezdxf
import ezdxf
import sys
from pathlib import Path
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 obj_to_dxf(obj_file, dxf_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)
doc.saveas(dxf_file)
print(f" Sačuvan: {dxf_file}")
def main():
if len(sys.argv) != 2:
print("Upotreba: python3 obj2dxf.py ulazni_fajl.obj")
return
input_path = Path(sys.argv[1])
if not input_path.exists():
print(f" Fajl ne postoji: {input_path}")
return
output_path = input_path.with_suffix('.dxf')
obj_to_dxf(str(input_path), str(output_path))
if __name__ == "__main__":
main()
Program obj2stl.py

Opis programa obj2stl.py
Ovaj program služi za konverziju 3D modela iz OBJ formata u STL format. Učitava geometriju iz OBJ fajla, uključujući verteks (tačke) i face (površinske trouglove), a zatim generiše odgovarajući ASCII STL fajl sa izračunatim normalama za svaki trougao. Program podržava i objekte sa licima koja imaju više od tri verteksa tako što ih automatski trianguliše.
Kako se koristi
- Pokreni program iz komandne linije, navodeći putanju do OBJ fajla:
python3 obj2stl.py model.obj- Program će učitati OBJ fajl, konvertovati ga u STL format i sačuvati izlazni fajl sa istim imenom, ali sa
.stlekstenzijom u istom direktorijumu. - U slučaju problema (npr. nepostojeći fajl ili nepravilni podaci), program će prikazati odgovarajuću poruku.
Instalacija potrebnih zavisnosti:
python3 -m pip install numpy
Programski kod obj2stl.py:
#obj2stl.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: KONVERTOR IZ OBJ U STL
# Instalacija potrebnih zavisnosti:python3 -m pip install numpy
import sys
import os
def read_obj(filename):
vertices = []
faces = []
with open(filename, 'r') as f:
for line in f:
if line.startswith('v '):
parts = line.strip().split()
vertex = list(map(float, parts[1:4]))
vertices.append(vertex)
elif line.startswith('f '):
parts = line.strip().split()
# Faces mogu biti u formatu f v1 v2 v3 ili sa teksturama/normalama: f v1/vt1/vn1 ...
face = []
for part in parts[1:]:
idx = part.split('/')[0] # uzimamo samo indeks vrha
face.append(int(idx))
# Ako je lice sa više od 3 vrha (npr. četvorka), trijažemo na trouglove
if len(face) == 3:
faces.append(face)
elif len(face) > 3:
# triangulacija fan metodom
for i in range(1, len(face)-1):
faces.append([face[0], face[i], face[i+1]])
return vertices, faces
def write_stl(filename, vertices, faces):
with open(filename, 'w') as f:
f.write("solid converted\n")
for face in faces:
v1 = vertices[face[0]-1]
v2 = vertices[face[1]-1]
v3 = vertices[face[2]-1]
# Izračun normalnog vektora
normal = calc_normal(v1, v2, v3)
f.write(f" facet normal {normal[0]} {normal[1]} {normal[2]}\n")
f.write(" outer loop\n")
f.write(f" vertex {v1[0]} {v1[1]} {v1[2]}\n")
f.write(f" vertex {v2[0]} {v2[1]} {v2[2]}\n")
f.write(f" vertex {v3[0]} {v3[1]} {v3[2]}\n")
f.write(" endloop\n")
f.write(" endfacet\n")
f.write("endsolid converted\n")
def calc_normal(v1, v2, v3):
import numpy as np
v1 = np.array(v1)
v2 = np.array(v2)
v3 = np.array(v3)
normal = np.cross(v2 - v1, v3 - v1)
norm = np.linalg.norm(normal)
if norm == 0:
return (0.0, 0.0, 0.0)
return normal / norm
def main():
if len(sys.argv) < 2:
print("Upotreba: python3 obj2stl.py naziv_fajla.obj")
sys.exit(1)
input_file = sys.argv[1]
if not input_file.lower().endswith('.obj'):
print("Ulazni fajl mora biti .obj")
sys.exit(1)
vertices, faces = read_obj(input_file)
if not vertices or not faces:
print("Obj fajl nema vertekse ili faces.")
sys.exit(1)
output_file = os.path.splitext(input_file)[0] + '.stl'
write_stl(output_file, vertices, faces)
print(f" STL fajl sačuvan kao: {output_file}")
if __name__ == "__main__":
main()
Kombinacija STL i PLY

Program obj2stat.py

Opis programa obj2stat.py
Program obj2stat.py služi za analizu 3D žičanih modela kupola ili sličnih struktura definisanih u OBJ formatu (tačke i linije). Cilj je da se izračunaju statistike o dužinama ivica, osnovne geometrijske karakteristike kupole, kao i da se prepoznaju i analiziraju romboidni paneli (četvorka tačaka koje formiraju romboid).
Funkcionalnosti:
- Učitava vrhove i linije iz
.objfajla. - Grupira linije prema dužini i prikazuje koliko ima segmenata po tipu dužine.
- Izračunava:
- poluprečnik kupole,
- visinu,
- obim baze,
- površinu baze,
- približnu površinu kupole (zbir ivica).
- Detektuje romboidne elemente u mreži i analizira njihove dimenzije (dužine stranica, dijagonale, površine).
- Generiše tekstualni izveštaj sa svim statističkim podacima.
Kako se koristi:
Pokrenite program komandno:
python3 obj2stat.py ulazni_fajl.obj
Program automatski kreira izveštaj u tekstualnom fajlu sa dodatkom _izvestaj.txt u imenu, koji sadrži detaljne analize i statistike.
Priloženi programski kod za obj2stat.py
NIJE PROVEREN!
Nema garancije za tačnost rezultata:
Instalacija potrebnih zavisnosti:
python3 -m pip install numpy
Programski kod za obj2stat.py
#obj2stat.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: ANALIZA I STATISTIKA KUPOLE
# Instalacija potrebnih zavisnosti:python3 -m pip install numpy
import numpy as np
from collections import defaultdict
from math import pi
from pathlib import Path
import sys
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()
vertices.append([float(parts[1]), float(parts[2]), float(parts[3])])
elif line.startswith('l '):
parts = line.strip().split()
i1, i2 = int(parts[1]) - 1, int(parts[2]) - 1
lines.append((i1, i2))
return np.array(vertices), lines
def group_lines_by_length(vertices, lines, tolerance=1e-3):
length_groups = defaultdict(list)
for i1, i2 in lines:
p1 = vertices[i1]
p2 = vertices[i2]
length = np.linalg.norm(p1 - p2)
key = round(length / tolerance) * tolerance
length_groups[key].append((i1, i2))
return dict(sorted(length_groups.items()))
def print_line_statistics(length_groups, out):
print_and_write(out, "\n Statistika dužina linija:")
for i, (length, lines) in enumerate(length_groups.items(), start=1):
print_and_write(out, f"Tip {i}: dužina {length:.3f}, broj {len(lines)}")
def compute_dome_geometry(vertices, lines):
radii = np.linalg.norm(vertices[:, :2], axis=1)
radius = np.max(radii)
z_min = np.min(vertices[:, 2])
z_max = np.max(vertices[:, 2])
height = z_max - z_min
circumference = 2 * pi * radius
base_area = pi * radius**2
surface_approx = sum(np.linalg.norm(vertices[i1] - vertices[i2]) for i1, i2 in lines)
return radius, height, circumference, base_area, surface_approx
def find_rhomboids(vertices, lines, tolerance=1e-5):
adjacency = defaultdict(set)
for i1, i2 in lines:
adjacency[i1].add(i2)
adjacency[i2].add(i1)
rhomboids = []
visited = set()
for A in range(len(vertices)):
for B in adjacency[A]:
for C in adjacency[B]:
if C == A:
continue
for D in adjacency[C]:
if D == B or D == A:
continue
if D in adjacency[A]:
quad = tuple(sorted([A, B, C, D]))
if quad in visited:
continue
visited.add(quad)
rhomboids.append([A, B, C, D])
return rhomboids
def edge_length(vertices, i1, i2):
return np.linalg.norm(vertices[i1] - vertices[i2])
def rhomboid_area(vertices, quad):
A, B, C, D = quad
AC = vertices[C] - vertices[A]
BD = vertices[D] - vertices[B]
return 0.5 * np.linalg.norm(np.cross(AC, BD))
def analyze_rhomboids(vertices, rhomboids, out, tolerance=1e-3):
rhomb_types = {}
rhomb_counts = defaultdict(int)
def approx_equal_tuple(t1, t2):
return all(abs(x - y) < tolerance for x, y in zip(t1, t2))
for quad in rhomboids:
A, B, C, D = quad
sides = (
edge_length(vertices, A, B),
edge_length(vertices, B, C),
edge_length(vertices, C, D),
edge_length(vertices, D, A),
)
diagonals = (
edge_length(vertices, A, C),
edge_length(vertices, B, D)
)
sides_sorted = tuple(sorted(sides))
diagonals_sorted = tuple(sorted(diagonals))
key = (diagonals_sorted, sides_sorted)
found_key = None
for k in rhomb_types.keys():
if approx_equal_tuple(k[0], key[0]) and approx_equal_tuple(k[1], key[1]):
found_key = k
break
if found_key is None:
rhomb_types[key] = quad
rhomb_counts[key] = 1
else:
rhomb_counts[found_key] += 1
print_and_write(out, "\n Tipovi romboida u mreži:")
for i, (key, quad) in enumerate(rhomb_types.items(), start=1):
diagonals_sorted, sides_sorted = key
area = rhomboid_area(vertices, quad)
print_and_write(out, f"Tip {i}:")
print_and_write(out, f" Dijagonale: {diagonals_sorted[0]:.4f}, {diagonals_sorted[1]:.4f}")
print_and_write(out, f" Stranice (sortirano): {sides_sorted[0]:.4f}, {sides_sorted[1]:.4f}, {sides_sorted[2]:.4f}, {sides_sorted[3]:.4f}")
print_and_write(out, f" Površina: {area:.4f}")
print_and_write(out, f" Broj romboida ovog tipa: {rhomb_counts[key]}")
def print_and_write(out, text):
print(text)
out.write(text + "\n")
def main():
if len(sys.argv) != 2:
print("Upotreba: python3 analiza.py ulazni_fajl.obj")
return
input_path = Path(sys.argv[1])
if not input_path.exists():
print(f" Fajl ne postoji: {input_path}")
return
output_txt = input_path.with_stem(input_path.stem + "_izvestaj").with_suffix(".txt")
with open(output_txt, "w") as out:
vertices, lines = read_obj_vertices_and_lines(str(input_path))
length_groups = group_lines_by_length(vertices, lines, tolerance=1e-3)
print_line_statistics(length_groups, out)
radius, height, circumference, base_area, surface_approx = compute_dome_geometry(vertices, lines)
print_and_write(out, "\n Geometrijske karakteristike kupole:")
print_and_write(out, f"Poluprečnik: {radius:.3f}")
print_and_write(out, f"Visina: {height:.3f}")
print_and_write(out, f"Obim baze: {circumference:.3f}")
print_and_write(out, f"Površina baze: {base_area:.3f}")
print_and_write(out, f"Površina kupole ≈ {surface_approx:.3f} (približno, zbir ivica)")
rhomboids = find_rhomboids(vertices, lines)
analyze_rhomboids(vertices, rhomboids, out, tolerance=1e-3)
print(f"\n Izveštaj sačuvan u: {output_txt}")
if __name__ == "__main__":
main()
Linkovi:
Reference:
Matematički modeli i teorija
- „The Mathematics of Zome“ – Tom Davis
Izuzetno detaljan papir (PDF) koji prikazuje kako izračunati dužine šipki u Zome sistemu i kako se čvorišta međusobno povezuju
https://www.yumpu.com/en/document/view/25868597/zome-patterns-home-page-tom-davis - „Spinning Circles and Parametric Approach“ – SimplyDifferently.org
Web članak objašnjava dve metode generisanja helix Zome geometrije: korišćenjem uvijenih kružnica i parametrskog pristupa
https://www.simplydifferently.org/Helix_Zome - Zometool Manual 2.3 Detaljni PDF vodič sa uvodom u strukturu boja i simetrije Zome sistema (2-, 3‑ i 5‑fold modeli), te primene u geometriji i edukaciji: https://www.researchgate.net/publication/344594402_From_Zoom_Organization_to_Zome_Configuration_and_Dynamics_Integrating_the_doughnut_helix_and_pineapple_models_towards_global_strategic_coherence
Kontekst i primena
- https://www.gradnja.rs/ove-montazne-modularne-kupole-po-projektu-domacih-arhitekata-izvodice-se-u-sad
- Laetus in Praesens – o “Zonoedrima i Zome konstrukcijama”
Analiza spirala i helix formi unutar Zome struktura, spominje zlati rez i geometriju zamishljenog sustava: https://www.laetusinpraesens.org/pdfs/2020s/zoomzome_2020.pdf
