Seuraa alla olevaa videota nähdäksesi, miten asennat sivustomme verkkosovelluksena aloitusnäytöllesi.
Huom: This feature may not be available in some browsers.
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import os
import shutil
import subprocess
import tempfile
# OpenSCADin oletuspolku Windowsissa
OPENSCAD_EXE = r"C:\Program Files\OpenSCAD\openscad.exe"
class NameplateApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Nimilapun generointi")
self.geometry("900x600")
self.font_options = [
"Arial",
"Arial Black",
"Calibri",
"Cambria",
"Candara",
"Comic Sans MS",
"Consolas",
"Constantia",
"Corbel",
"Courier New",
"Georgia",
"Segoe UI",
"Tahoma",
"Times New Roman",
"Trebuchet MS",
"Verdana",
]
self.create_widgets()
# ---------- GUI ----------
def create_widgets(self):
settings_frame = ttk.LabelFrame(self, text="Asetukset")
settings_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
bottom_frame = ttk.Frame(self)
bottom_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=5)
gen_frame = ttk.LabelFrame(bottom_frame, text="Nimilapun generointi")
gen_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
preview_frame = ttk.LabelFrame(bottom_frame, text="Esikatselu")
preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# --- ASETUKSET ---
# Rivi 0: leveys, korkeus, paksuus
ttk.Label(settings_frame, text="Leveys (mm):").grid(row=0, column=0, sticky="e", padx=5, pady=2)
self.width_var = tk.StringVar(value="80")
ttk.Entry(settings_frame, textvariable=self.width_var, width=8).grid(row=0, column=1, sticky="w")
ttk.Label(settings_frame, text="Korkeus (mm):").grid(row=0, column=2, sticky="e", padx=5, pady=2)
self.height_var = tk.StringVar(value="30")
ttk.Entry(settings_frame, textvariable=self.height_var, width=8).grid(row=0, column=3, sticky="w")
ttk.Label(settings_frame, text="Paksuus (mm):").grid(row=0, column=4, sticky="e", padx=5, pady=2)
self.thickness_var = tk.StringVar(value="2")
ttk.Entry(settings_frame, textvariable=self.thickness_var, width=8).grid(row=0, column=5, sticky="w")
# Rivi 1: korotus + lapun kulmapyöristys
ttk.Label(settings_frame, text="Tekstin ja kehyksen korkeus (mm):").grid(
row=1, column=0, sticky="e", padx=5, pady=2
)
self.relief_height_var = tk.StringVar(value="0.8")
ttk.Entry(settings_frame, textvariable=self.relief_height_var, width=8).grid(
row=1, column=1, sticky="w"
)
ttk.Label(settings_frame, text="Lapun kulmapyöristys (mm):").grid(
row=1, column=2, sticky="e", padx=5, pady=2
)
self.corner_radius_var = tk.StringVar(value="2.0")
ttk.Entry(settings_frame, textvariable=self.corner_radius_var, width=8).grid(
row=1, column=3, sticky="w"
)
# Rivi 2: kehys päälle/pois ja leveys
self.frame_enabled_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
settings_frame,
text="Reunakehys päällä",
variable=self.frame_enabled_var,
).grid(row=2, column=0, sticky="w", padx=5, pady=2)
ttk.Label(settings_frame, text="Reunakehyksen leveys (mm):").grid(
row=2, column=2, sticky="e", padx=5, pady=2
)
self.frame_width_var = tk.StringVar(value="1.0")
ttk.Entry(settings_frame, textvariable=self.frame_width_var, width=8).grid(
row=2, column=3, sticky="w"
)
# Rivi 3: fontti, koko, riviväli, lihavointi
ttk.Label(settings_frame, text="Fontti:").grid(row=3, column=0, sticky="e", padx=5, pady=2)
self.font_name_var = tk.StringVar(value=self.font_options[0])
ttk.Combobox(
settings_frame,
textvariable=self.font_name_var,
values=self.font_options,
width=18,
).grid(row=3, column=1, columnspan=2, sticky="w")
ttk.Label(settings_frame, text="Fontin koko (mm):").grid(
row=3, column=3, sticky="e", padx=5, pady=2
)
self.font_size_var = tk.StringVar(value="10")
ttk.Entry(settings_frame, textvariable=self.font_size_var, width=8).grid(
row=3, column=4, sticky="w"
)
ttk.Label(settings_frame, text="Riviväli lisä (mm):").grid(
row=4, column=0, sticky="e", padx=5, pady=2
)
self.line_spacing_var = tk.StringVar(value="2.0") # oletus 2 mm
ttk.Entry(settings_frame, textvariable=self.line_spacing_var, width=8).grid(
row=4, column=1, sticky="w"
)
self.font_bold_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
settings_frame,
text="Lihavointi (bold)",
variable=self.font_bold_var,
).grid(row=4, column=5, sticky="w", padx=5, pady=2)
# Rivi 5: asetusten lataus
ttk.Button(
settings_frame,
text="Lataa asetukset...",
command=self.load_settings,
).grid(row=5, column=0, columnspan=2, sticky="w", padx=5, pady=5)
# --- GENEROINTI-OSA ---
lines_frame = ttk.Frame(gen_frame)
lines_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
ttk.Label(lines_frame, text="Rivien määrä:").grid(row=0, column=0, sticky="w")
self.line_count_var = tk.IntVar(value=1)
for val in (1, 2, 3):
ttk.Radiobutton(
lines_frame,
text=str(val),
variable=self.line_count_var,
value=val,
command=self.update_line_states,
).grid(row=0, column=val)
text_frame = ttk.Frame(gen_frame)
text_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
self.text_vars = [tk.StringVar(), tk.StringVar(), tk.StringVar()]
for i in range(3):
ttk.Label(text_frame, text=f"Rivi {i+1}:").grid(
row=i, column=0, sticky="e", padx=5, pady=2
)
ttk.Entry(text_frame, textvariable=self.text_vars[i], width=40).grid(
row=i, column=1, sticky="w"
)
button_frame = ttk.Frame(gen_frame)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=10)
ttk.Button(button_frame, text="Esikatselu", command=self.preview).pack(
side=tk.LEFT, padx=5
)
ttk.Button(
button_frame,
text="Tallenna STL + asetukset",
command=self.save_all,
).pack(side=tk.LEFT, padx=5)
self.preview_canvas = tk.Canvas(preview_frame, bg="white")
self.preview_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.update_line_states()
self.draw_preview_placeholder()
def update_line_states(self):
pass
# ---------- PARAMETRIEN LUKU ----------
def get_float(self, var, name, min_value=None):
try:
value = float(var.get().replace(",", "."))
except ValueError:
raise ValueError(f"{name} ei ole kelvollinen numero.")
if min_value is not None and value < min_value:
raise ValueError(f"{name} pitää olla vähintään {min_value}.")
return value
def collect_params(self):
try:
width = self.get_float(self.width_var, "Leveys", 1)
height = self.get_float(self.height_var, "Korkeus", 1)
thickness = self.get_float(self.thickness_var, "Paksuus", 0.1)
relief_h = self.get_float(
self.relief_height_var, "Tekstin ja kehyksen korkeus", 0.01
)
corner_r = self.get_float(
self.corner_radius_var, "Lapun kulmapyöristys", 0.0
)
frame_w = self.get_float(
self.frame_width_var, "Reunakehyksen leveys", 0.1
)
font_size = self.get_float(self.font_size_var, "Fontin koko", 1.0)
line_spacing_extra = self.get_float(self.line_spacing_var, "Riviväli lisä", 0.0)
except ValueError as e:
messagebox.showerror("Virhe asetuksissa", str(e))
return None
frame_enabled = self.frame_enabled_var.get()
font_name = self.font_name_var.get()
font_bold = self.font_bold_var.get()
line_count = self.line_count_var.get()
lines = [v.get().strip() for v in self.text_vars]
return {
"width": width,
"height": height,
"thickness": thickness,
"relief_height": relief_h,
"corner_radius": corner_r,
"frame_width": frame_w,
"frame_enabled": frame_enabled,
"font_name": font_name,
"font_size": font_size,
"font_bold": font_bold,
"line_spacing_extra": line_spacing_extra,
"line_count": line_count,
"lines": lines,
}
# ---------- ESIKATSELU-APU ----------
def draw_rounded_rect(self, c, x0, y0, x1, y1, r, outline="black", fill=None):
r = max(0, min(r, (x1 - x0) / 2, (y1 - y0) / 2))
if r == 0:
c.create_rectangle(x0, y0, x1, y1, outline=outline, fill=fill)
return
c.create_rectangle(x0 + r, y0, x1 - r, y1, outline=outline, fill=fill)
c.create_rectangle(x0, y0 + r, x1, y1 - r, outline=outline, fill=fill)
c.create_arc(x0, y0, x0 + 2 * r, y0 + 2 * r, start=90, extent=90,
outline=outline, fill=fill, style="pieslice")
c.create_arc(x1 - 2 * r, y0, x1, y0 + 2 * r, start=0, extent=90,
outline=outline, fill=fill, style="pieslice")
c.create_arc(x0, y1 - 2 * r, x0 + 2 * r, y1, start=180, extent=90,
outline=outline, fill=fill, style="pieslice")
c.create_arc(x1 - 2 * r, y1 - 2 * r, x1, y1, start=270, extent=90,
outline=outline, fill=fill, style="pieslice")
def draw_preview_placeholder(self):
self.preview_canvas.delete("all")
w = self.preview_canvas.winfo_width() or 400
h = self.preview_canvas.winfo_height() or 300
self.preview_canvas.create_text(
w / 2, h / 2, text="Esikatselu ilmestyy tähän", fill="gray"
)
# ---------- ESIKATSELU ----------
def preview(self):
p = self.collect_params()
if not p:
return
c = self.preview_canvas
c.delete("all")
canvas_w = c.winfo_width() or 400
canvas_h = c.winfo_height() or 300
W = p["width"]
H = p["height"]
if W <= 0 or H <= 0:
return
scale = min(canvas_w / (W * 1.2), canvas_h / (H * 1.2))
badge_w = W * scale
badge_h = H * scale
cx = canvas_w / 2
cy = canvas_h / 2
x0 = cx - badge_w / 2
x1 = cx + badge_w / 2
y0 = cy - badge_h / 2
y1 = cy + badge_h / 2
def mm_to_px(x_mm, y_mm):
x_px = cx + x_mm * scale
y_px = cy - y_mm * scale
return x_px, y_px
# tausta – ilman mustaa reunaviivaa
corner_r_px = p["corner_radius"] * scale
self.draw_rounded_rect(c, x0, y0, x1, y1, corner_r_px,
outline="", fill="#f0f0ff")
# --- Kehys: rengas reunassa ---
if p["frame_enabled"]:
fw_px = p["frame_width"] * scale
# ulkoreuna = musta “levy”
self.draw_rounded_rect(c, x0, y0, x1, y1, corner_r_px,
outline="", fill="black")
# sisäreuna: pienempi pyöristetty suorakaide, täytetään taustan värillä
inner_x0 = x0 + fw_px
inner_y0 = y0 + fw_px
inner_x1 = x1 - fw_px
inner_y1 = y1 - fw_px
inner_r_px = max(corner_r_px - fw_px, 0)
self.draw_rounded_rect(c, inner_x0, inner_y0, inner_x1, inner_y1,
inner_r_px, outline="", fill="#f0f0ff")
# --- Teksti: rivivälin mukaan keskitetty ---
lines = p["lines"][:p["line_count"]]
lines = [t for t in lines if t]
if not lines:
return
n = len(lines)
spacing_mm = p["font_size"] + p["line_spacing_extra"]
ys_mm = [((n - 1) / 2.0 - i) * spacing_mm for i in range(n)]
font_px = int(p["font_size"] * scale)
if font_px < 6:
font_px = 6
style = ("bold",) if p["font_bold"] else ()
center_x, _ = mm_to_px(0, 0)
for text, y_mm in zip(lines, ys_mm):
_, y_px = mm_to_px(0, y_mm)
c.create_text(
center_x,
y_px,
text=text,
font=(p["font_name"], font_px, *style),
fill="black",
)
# ---------- TALLENNUS ----------
def save_all(self):
p = self.collect_params()
if not p:
return
active_lines = p["lines"][:p["line_count"]]
active_lines = [t for t in active_lines if t]
base = "_".join(active_lines) if active_lines else "nimilappu"
base = base.replace(" ", "_")
base = "".join(ch for ch in base if ch.isalnum() or ch in "_-") or "nimilappu"
stl_path = filedialog.asksaveasfilename(
title="Tallenna STL",
defaultextension=".stl",
initialfile=base + ".stl",
filetypes=[("STL-tiedostot", "*.stl"), ("Kaikki tiedostot", "*.*")],
)
if not stl_path:
return
scad_code = self.build_scad(p)
if not os.path.isfile(OPENSCAD_EXE) and shutil.which(OPENSCAD_EXE) is None:
messagebox.showerror(
"OpenSCAD puuttuu",
"OpenSCAD-ohjelmaa ei löytynyt polusta:\n"
f"{OPENSCAD_EXE}\n\n"
"Varmista, että OpenSCAD on asennettu tähän kansioon\n"
"tai muuta OPENSCAD_EXE-polku koodissa.",
)
return
try:
# TÄMÄ RIVI OLI VÄÄRIN ENNEN (liikaa sulkuja)
with tempfile.TemporaryDirectory() as tmpdir:
scad_file = os.path.join(tmpdir, "temp.scad")
with open(scad_file, "w", encoding="utf-8") as f:
f.write(scad_code)
subprocess.run(
[OPENSCAD_EXE, "-o", stl_path, scad_file],
check=True,
)
except subprocess.CalledProcessError as e:
messagebox.showerror("OpenSCAD-virhe", f"OpenSCADin ajo epäonnistui:\n{e}")
return
except Exception as e:
messagebox.showerror("Virhe", f"Tallennus epäonnistui:\n{e}")
return
settings_path = os.path.splitext(stl_path)[0] + ".txt"
try:
with open(settings_path, "w", encoding="utf-8") as f:
json.dump(p, f, ensure_ascii=False, indent=2)
except Exception as e:
messagebox.showerror(
"Asetusten tallennus epäonnistui",
f"Asetusten tallennus epäonnistui:\n{e}",
)
return
messagebox.showinfo(
"Valmis",
f"Nimilappu ja asetukset tallennettu.\n\n"
f"STL: {stl_path}\nAsetukset: {settings_path}",
)
# ---------- SCAD ----------
def scad_escape(self, text: str) -> str:
return text.replace("\\", "\\\\").replace("\"", "\\\"")
def build_scad(self, p):
width = p["width"]
height = p["height"]
thickness = p["thickness"]
relief_h = p["relief_height"]
corner_r = p["corner_radius"]
frame_w = p["frame_width"]
frame_enabled = p["frame_enabled"]
font_name = p["font_name"]
font_bold = p["font_bold"]
font_size = p["font_size"]
line_spacing_extra = p["line_spacing_extra"]
line_count = p["line_count"]
lines = [t for t in p["lines"][:line_count] if t]
scad_font = f"{font_name}:style=Bold" if font_bold else font_name
s = []
s.append("$fn = 64;")
s.append(f"W = {width};")
s.append(f"H = {height};")
s.append(f"T = {thickness};")
s.append(f"relief_h = {relief_h};")
s.append(f"corner_r = {corner_r};")
s.append(f"frame_w = {frame_w};")
s.append(f"font_size_mm = {font_size};")
s.append(f"extra_spacing_mm = {line_spacing_extra};")
s.append("line_spacing_mm = font_size_mm + extra_spacing_mm;")
s.append(f"frame_on = {str(frame_enabled).lower()};")
s.append(f"font_name = \"{self.scad_escape(scad_font)}\";")
s.append("")
s.append("module rounded_rect(w, h, r) {")
s.append(" rr = min(r, min(w, h)/2);")
s.append(" if (rr <= 0) {")
s.append(" square([w, h], center=true);")
s.append(" } else {")
s.append(" hull() {")
s.append(" for (x = [-w/2 + rr, w/2 - rr])")
s.append(" for (y = [-h/2 + rr, h/2 - rr])")
s.append(" translate([x, y, 0]) circle(r=rr);")
s.append(" }")
s.append(" }")
s.append("}")
s.append("")
s.append("module plate_base() {")
s.append(" linear_extrude(height=T)")
s.append(" rounded_rect(W, H, corner_r);")
s.append("}")
s.append("")
# Kehys: rengas
s.append("module plate_frame() {")
s.append(" if (frame_on) {")
s.append(" fw = frame_w;")
s.append(" outer_w = W;")
s.append(" outer_h = H;")
s.append(" inner_w = max(W - 2*fw, 0);")
s.append(" inner_h = max(H - 2*fw, 0);")
s.append(" inner_r = max(corner_r - fw, 0);")
s.append(" translate([0,0,T])")
s.append(" linear_extrude(height=relief_h)")
s.append(" difference() {")
s.append(" rounded_rect(outer_w, outer_h, corner_r);")
s.append(" rounded_rect(inner_w, inner_h, inner_r);")
s.append(" };")
s.append(" }")
s.append("}")
s.append("")
# Teksti
s.append("module plate_text() {")
if lines:
n = len(lines)
for idx, txt in enumerate(lines):
esc = self.scad_escape(txt)
y_expr = f"(({n - 1})/2 - {idx}) * line_spacing_mm"
s.append(f" translate([0, {y_expr}, T])")
s.append(" linear_extrude(height = relief_h)")
s.append(
f" text(\"{esc}\", size = font_size_mm, font = font_name, "
"halign=\"center\", valign=\"center\");"
)
s.append("}")
s.append("")
s.append("module nameplate() {")
s.append(" union() {")
s.append(" plate_base();")
s.append(" plate_frame();")
s.append(" plate_text();")
s.append(" }")
s.append("}")
s.append("")
s.append("nameplate();")
return "\n".join(s)
# ---------- ASETUSTEN LATAUS ----------
def load_settings(self):
path = filedialog.askopenfilename(
title="Lataa asetukset",
filetypes=[("Tekstitiedostot", "*.txt"), ("Kaikki tiedostot", "*.*")],
)
if not path:
return
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
messagebox.showerror("Virhe", f"Asetusten lataus epäonnistui:\n{e}")
return
def set_if_exists(key, var):
if key in data:
var.set(str(data[key]))
set_if_exists("width", self.width_var)
set_if_exists("height", self.height_var)
set_if_exists("thickness", self.thickness_var)
set_if_exists("relief_height", self.relief_height_var)
set_if_exists("corner_radius", self.corner_radius_var)
set_if_exists("frame_width", self.frame_width_var)
set_if_exists("font_size", self.font_size_var)
set_if_exists("line_spacing_extra", self.line_spacing_var)
if "frame_enabled" in data:
self.frame_enabled_var.set(bool(data["frame_enabled"]))
if "font_name" in data:
self.font_name_var.set(data["font_name"])
if "font_bold" in data:
self.font_bold_var.set(bool(data["font_bold"]))
if "line_count" in data:
try:
self.line_count_var.set(int(data["line_count"]))
except ValueError:
pass
if "lines" in data and isinstance(data["lines"], list):
for i in range(min(3, len(data["lines"]))):
self.text_vars[i].set(str(data["lines"][i]))
self.update_line_states()
self.preview()
if __name__ == "__main__":
app = NameplateApp()
app.mainloop()
Mää oon hommannut noita käytettyjä toimistokoneita. Ainakin kun Windows 10 päivitettiin 11 niin työsuorituksen tekijä kehui koneen nopeutta ja ominaisuuksia.Uhkana näkisin, jos kuitenkin vaatii ~saman minkä muutkin 3d-skannerit tietokoneen raudan puolesta
ollakseen hyödyllisiä/käytettäviä. Ei o noi millekkään perusläppäreille/pöytäkoneille.
No joo pöytäkoneella voi sentään kriittiset osat useimmiten päivittää riittäviksi, mutta perus toimistokäyttöönMää oon hommannut noita käytettyjä toimistokoneita. Ainakin kun Windows 10 päivitettiin 11 niin työsuorituksen tekijä kehui koneen nopeutta ja ominaisuuksia.
No juu, en tiedä tietokoneista mitään.No joo pöytäkoneella voi sentään kriittiset osat useimmiten päivittää riittäviksi, mutta perus toimistokäyttöön
ei varmasti ole niihin koneisiin upotettu rautaa(pl. ehkä prossu), joilla 3d-skannauksia mielekkäästi käsiteltäisiin.
Vaatii siis koneelta ihan reilusti enemmän, kuin vaikkapa pikkupoikien(/aikuisten) piupaupyssypelit, eli joku
vuoden/pari vanha "pelikone", niin lähtökohdat olisi äkkiseltään helposti paremmat vs. käytetty toimistokone,
mutta ne nyt jo maksaakin jotain nykyisellään, koska tekoäly nostanut hintoja(muistin ja näytönohjaimien).
^oon muute kirjoittanu nää sen perusteella, mitä tiedän skannailun autojen(/auton osien) kanssa olevan, eli
jos vaikka skannailee sytkäreitä tai muuta pientä niin luulis niiden olevan täysin tehtävissä, jos speksit osuu
edes crealityn vaatimiin(, jotka ovat mielestäni niin miedot, että ei niillä ihmeitä käyttöön asti skannailla).
Tollaset speksit tarvitaan ja tollaset on pöytäkoneessa...Näytä liitetiedosto 164367Performance requirements
wiki.creality.com
Tollasta konetta ei taida mistään saada ostettua vaan sen täytyy joku tehdä?Ei ikävä kyllä riitä edes alimmalle tasolle. Prossu on skylake, eli gen6 vaikka gen8 vähintään vaaditaan,
ja näyttiksen muistiakin puuttuu pari gigaa + suositus rammillekin ois 32 tai 64gb. Tallennustilanakin
ois syytä olla joku suoraan pcie väylässä sijaitseva nvme, eikä mikään sata pohjainen ssd.
Kun ei oikein mikään osu, niin voi olla, ettei tuota edes kärsivällisyydellä paikkaisi, eli uudempaa rautaa
tuollainen skanneri tuntuisi huutelevan.
muok: noiden vähimmäisspeksien koneen kasannee ihan satasilla tori.fi:stä, jos käytetyt osat kelpaa,
eikä nyt välttämättä ihan päivän päälle kiire ole.
Saa, vaikka heti gigantista, ja ihan ovat hyllykoneita, ettei sinänsä mitään erikoisia.Tollasta konetta ei taida mistään saada ostettua vaan sen täytyy joku tehdä?
Mikä tohon nopeuteen vaikuttaa?Saa, vaikka heti gigantista, ja ihan ovat hyllykoneita, ettei sinänsä mitään erikoisia.
1399e vaihtoehto, tässä riittää varmasti potkua: https://www.gigantti.fi/product/tie...26l-i7-13650hx161-tb5070-pelitietokone/913585
1049e vaihtoehto, tässä ei paljasteta paljonko näytönohjaimella on muistia, mutta veikkaisin sen vähimmäisen 6gb löytyvän, ellei enemmän: https://www.gigantti.fi/product/tie...t500-i5-210h16512rtx5060-pelitietokone/998870
muok: noistakin molemmat jää vielä määrässä suositellusta keskusmuistista, mutta emo näytti halvemmassakin
tukevan päivitystä vaikka sinne 64 asti, jos tarve vaatii.
RAM. Monille skannereille taitaa 32GB olla minimi, ja suositus 64GB. On vaan tällä hetkellä ihan julmetun kallista. Taitaa 64GB DDR5 maksaa vähintään 800 euroa tällä hetkellä.Mikä tohon nopeuteen vaikuttaa?
RAM:in koko vai muistin koko?
Mitään en tietokoneista tiedä....
Rasia eläkepäivien eväille? Hieno kyll!Tein eläköityvälle varastomiehelle läksiäislahjan...Näytä liitetiedosto 164295
Luo tili tai kirjaudu sisään kommentoidaksesi
Sinun täytyy olla jäsen voidaksesi jättää kommentin.
Luo käyttäjätili
Liity Konekansalaiseksi. Se on helppoa ja ilmaista! Rekisteröityneenä et näe mainoksia, voit käyttää hakua, näet alueita, joita nyt ovat piilossa...jne.