Загрузка данных


import tkinter as tk
import math

# задаётся куб
VERTICES = [
    (-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1),
    (-1,-1, 1),(1,-1, 1),(1,1, 1),(-1,1, 1),
]

FACES = [
    ([4,5,6,7], 1, ( 0, 0, 1)),
    ([1,0,3,2], 2, ( 0, 0,-1)),
    ([0,1,5,4], 3, ( 0,-1, 0)),
    ([3,7,6,2], 4, ( 0, 1, 0)),
    ([0,4,7,3], 5, (-1, 0, 0)),
    ([5,1,2,6], 6, ( 1, 0, 0)),
]

FACE_BG = {
    1:"#F0F0F0", 2:"#1a1a2e",
    3:"#e63946", 4:"#1565C0",
    5:"#2e7d32", 6:"#E65100",
}
FACE_FG = {
    1:"#1a1a2e", 2:"#F0F0F0",
    3:"#F0F0F0", 4:"#F0F0F0",
    5:"#F0F0F0", 6:"#1a1a2e",
}

# поворот
def rot_x(v,a):
    x,y,z=v; c,s=math.cos(a),math.sin(a)
    return (x, y*c-z*s, y*s+z*c)

def rot_y(v,a):
    x,y,z=v; c,s=math.cos(a),math.sin(a)
    return (x*c+z*s, y, -x*s+z*c)

def rot_z(v,a):
    x,y,z=v; c,s=math.cos(a),math.sin(a)
    return (x*c-y*s, x*s+y*c, z)

def dot3(a,b): return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]

def normalize3(v):
    L=math.sqrt(dot3(v,v))
    return (v[0]/L,v[1]/L,v[2]/L) if L>1e-9 else v

def cross2d(ax,ay,bx,by): return ax*by - ay*bx

# проекция 3д в 2д
def project(v, scale, ox, oy, fov=5.0):
    x,y,z=v
    d=fov+z
    if d<0.01: d=0.01
    return (ox + x*scale*fov/d,
            oy - y*scale*fov/d)

def shade_color(hex6, bright):
    r=int(hex6[1:3],16); g=int(hex6[3:5],16); b=int(hex6[5:7],16)
    r=min(255,int(r*bright)); g=min(255,int(g*bright)); b=min(255,int(b*bright))
    return f"#{r:02x}{g:02x}{b:02x}"

def face_area_2d(pts2):
    n=len(pts2); s=0.0
    for i in range(n):
        x0,y0=pts2[i]; x1,y1=pts2[(i+1)%n]
        s+=(x0*y1 - x1*y0)
    return s/2.0

LIGHT = normalize3((0.5, 0.8, 1.0))

class DiceRenderer:
    def __init__(self, canvas):
        self.canvas = canvas

    def render(self, ax, ay, az, scale):
        c=self.canvas
        c.delete("all")
        W=c.winfo_width()  or 560
        H=c.winfo_height() or 560
        ox,oy=W/2, H/2

        c.create_rectangle(0,0,W,H, fill="#1a1a2e", outline="")

        verts=[]
        for v in VERTICES:
            v=rot_x(v,ax); v=rot_y(v,ay); v=rot_z(v,az)
            verts.append(v)

        face_data=[]
        for vi, fnum, n0 in FACES:
            pts3=[verts[j] for j in vi]
            zc=sum(p[2] for p in pts3)/4
            n=rot_x(n0,ax); n=rot_y(n,ay); n=rot_z(n,az)
            face_data.append((zc, fnum, pts3, n))

        face_data.sort(key=lambda x: x[0])

        for zc, fnum, pts3, normal in face_data:
            pts2=[project(p,scale,ox,oy) for p in pts3]

            area=face_area_2d(pts2)
            if area < 0:
                continue

            if abs(area) < 1.0:
                continue

            diff=max(0.0, dot3(normal, LIGHT))
            bright=0.38 + 0.62*diff

            bg=shade_color(FACE_BG[fnum], bright)
            spec=diff**6
            ec=shade_color(FACE_BG[fnum], min(1.0, bright+spec*0.35))

            flat=[coord for p in pts2 for coord in p]
            c.create_polygon(flat, fill=bg, outline=ec,
                             width=max(1,int(scale*0.035)))

            cx=sum(p[0] for p in pts2)/4
            cy=sum(p[1] for p in pts2)/4

            # размер символа от размера площади проекции
            sym_size=max(6, int(math.sqrt(abs(area))*0.38))
            self._label(c, fnum, cx, cy, pts2, sym_size, area, FACE_FG[fnum])

    def _label(self, c, fnum, cx, cy, pts2, sym, area, fg):
        inset=math.sqrt(abs(area))*0.18

        if fnum == 1:
            c.create_text(cx, cy, text="1", fill=fg,
                          font=("Georgia", sym, "bold"))

        elif fnum == 2:
            c.create_text(cx, cy, text="2", fill=fg,
                          font=("Georgia", sym, "bold"))

        elif fnum == 3:
            r=max(2, int(sym*0.22))
            sp=inset*0.72
            for dx,dy in [(-sp,-sp),(sp,-sp),(0,0),(-sp,sp),(sp,sp)]:
                x0,y0=cx+dx-r, cy+dy-r
                c.create_oval(x0,y0,x0+2*r,y0+2*r, fill=fg, outline="")

        elif fnum == 4:
            side=inset*1.5
            h3=side*math.sqrt(3)/2
            pts_t=[cx, cy-h3*0.62,
                   cx+side/2, cy+h3*0.42,
                   cx-side/2, cy+h3*0.42]
            c.create_polygon(pts_t, fill=fg, outline="")

        elif fnum == 5:
            pts_s=[]
            for i in range(10):
                a=-math.pi/2 + i*math.pi/5
                r2=(sym*0.52) if i%2==0 else (sym*0.22)
                pts_s.extend([cx+math.cos(a)*r2, cy+math.sin(a)*r2])
            c.create_polygon(pts_s, fill=fg, outline="")

        elif fnum == 6:
            r=max(2, int(sym*0.20))
            sp=inset*0.68
            cols=["#e63946","#f4a261","#2a9d8f",
                  "#457b9d","#a8dadc","#f8edeb"]
            pos6=[(-sp,-sp),(sp,-sp),
                  (-sp,  0),(sp,  0),
                  (-sp, sp),(sp, sp)]
            for (dx,dy),col in zip(pos6,cols):
                x0,y0=cx+dx-r, cy+dy-r
                c.create_oval(x0,y0,x0+2*r,y0+2*r, fill=col, outline="")

class DiceApp:
    def __init__(self, root):
        self.root=root
        self.root.title("3D Dice Viewer")
        self.root.configure(bg="#0f0f1a")
        self.root.resizable(True,True)

        self.ax=tk.DoubleVar(value=0.4)
        self.ay=tk.DoubleVar(value=0.6)
        self.az=tk.DoubleVar(value=0.0)
        self.scale=tk.DoubleVar(value=160.0)

        self._drag=None
        self._anim_id=None
        self._auto_spin=False
        self._fullscreen=False

        self._build_ui()
        self._bind()
        self.draw()

        try:
            self.root.state("zoomed")
        except Exception:
            try: self.root.attributes("-zoomed",True)
            except Exception: self.root.geometry("920x680")

    def _build_ui(self):
        tb=tk.Frame(self.root, bg="#12122a", height=38)
        tb.pack(side="top", fill="x")
        tb.pack_propagate(False)

        def tbtn(text, cmd, accent=False):
            b=tk.Button(tb, text=text, command=cmd, relief="flat",
                        bg="#2563eb" if accent else "#1e1e3a",
                        fg="white", font=("Arial",9), cursor="hand2",
                        padx=10, pady=6,
                        activebackground="#3b3b5c", activeforeground="white")
            b.pack(side="left", padx=2, pady=4)
            return b

        tbtn("⛶  Полный экран", self.toggle_fullscreen)
        tk.Frame(tb,bg="#333355",width=1).pack(side="left",fill="y",pady=6,padx=4)
        tbtn("↺  Сбросить вид", self.reset_view)
        self._spin_btn=tbtn("▶  Авто-вращение", self.toggle_spin, accent=True)

        main=tk.Frame(self.root, bg="#0f0f1a")
        main.pack(fill="both", expand=True)

        self.canvas=tk.Canvas(main, bg="#1a1a2e",
                              highlightthickness=0, cursor="hand2")
        self.canvas.pack(side="left", fill="both", expand=True,
                         padx=(8,4), pady=(4,8))

        self.renderer=DiceRenderer(self.canvas)

        rp=tk.Frame(main, bg="#12122a", width=230)
        rp.pack(side="right", fill="y", padx=(0,8), pady=(4,8))
        rp.pack_propagate(False)

        tk.Label(rp, text="ТОЧКА ЗРЕНИЯ", bg="#12122a", fg="#8888aa",
                 font=("Arial",8,"bold")).pack(pady=(14,6))

        self._sl(rp,"Ось X",self.ax,-math.pi,math.pi)
        self._sl(rp,"Ось Y",self.ay,-math.pi,math.pi)
        self._sl(rp,"Ось Z", self.az,-math.pi,math.pi)

        tk.Frame(rp,bg="#222244",height=1).pack(fill="x",padx=10,pady=8)
        tk.Label(rp,text="МАСШТАБ",bg="#12122a",fg="#8888aa",
                 font=("Arial",8,"bold")).pack(pady=(0,4))
        self._sl(rp,"Размер",self.scale,60,400,integer=True)

        tk.Frame(rp,bg="#222244",height=1).pack(fill="x",padx=10,pady=8)
        tk.Label(rp,text="ГРАНИ",bg="#12122a",fg="#8888aa",
                 font=("Arial",8,"bold")).pack(pady=(0,4))

        for fnum,(bg,lbl) in {
            1:("#F0F0F0","1 — цифра (белая)"),
            2:("#1a1a2e","2 — цифра (чёрная)"),
            3:("#e63946","3 — пять точек"),
            4:("#1565C0","4 — треугольник"),
            5:("#2e7d32","5 — звезда"),
            6:("#E65100","6 — цвет. маркеры"),
        }.items():
            row=tk.Frame(rp,bg="#12122a")
            row.pack(fill="x",padx=12,pady=1)
            dot=tk.Canvas(row,width=16,height=16,bg="#12122a",highlightthickness=0)
            dot.pack(side="left")
            dot.create_rectangle(1,1,15,15,fill=bg,outline="#555577")
            tk.Label(row,text=lbl,bg="#12122a",fg="#ccccee",
                     font=("Arial",9)).pack(side="left",padx=6)

    def _sl(self, parent, label, var, lo, hi, integer=False):
        frm=tk.Frame(parent,bg="#12122a")
        frm.pack(fill="x",padx=10,pady=3)
        tk.Label(frm,text=label,bg="#12122a",fg="#aaaacc",
                 font=("Arial",9),width=16,anchor="w").pack(side="left")
        res=200 if not integer else int(hi-lo)
        tk.Scale(frm,variable=var,from_=lo,to=hi,
                 orient="horizontal",bg="#12122a",fg="#aaaacc",
                 troughcolor="#222244",highlightthickness=0,
                 showvalue=False,length=110,resolution=(hi-lo)/res,
                 command=lambda _: self.draw()).pack(side="left")

    def _bind(self):
        c=self.canvas
        c.bind("<ButtonPress-1>",  self._ds)
        c.bind("<B1-Motion>",      self._dm)
        c.bind("<ButtonRelease-1>",self._de)
        c.bind("<MouseWheel>",     self._wheel)
        c.bind("<Button-4>",       self._wheel)
        c.bind("<Button-5>",       self._wheel)
        self.root.bind("<F11>",    lambda e: self.toggle_fullscreen())
        self.root.bind("<Escape>", lambda e: self._xfs())
        self.root.bind("<Left>",   lambda e: self._nudge("y",-0.06))
        self.root.bind("<Right>",  lambda e: self._nudge("y", 0.06))
        self.root.bind("<Up>",     lambda e: self._nudge("x",-0.06))
        self.root.bind("<Down>",   lambda e: self._nudge("x", 0.06))
        self.root.bind("q",        lambda e: self._nudge("z",-0.06))
        self.root.bind("e",        lambda e: self._nudge("z", 0.06))
        self.root.bind("+",        lambda e: self._ns(12))
        self.root.bind("=",        lambda e: self._ns(12))
        self.root.bind("-",        lambda e: self._ns(-12))
        self.root.bind("<Configure>", lambda e: self.draw())

    def _nudge(self,axis,d):
        v={"x":self.ax,"y":self.ay,"z":self.az}[axis]
        v.set(v.get()+d)
        self.draw()

    def _ns(self,d):
        self.scale.set(max(60,min(400,self.scale.get()+d)))
        self.draw()

    def _ds(self,e): self._drag=(e.x,e.y,self.ax.get(),self.ay.get())
    def _dm(self,e):
        if not self._drag: return
        sx,sy,ax0,ay0=self._drag
        self.ax.set(ax0+(e.y-sy)*0.008)
        self.ay.set(ay0+(e.x-sx)*0.008)
        self.draw()
    def _de(self,e): self._drag=None

    def _wheel(self,e):
        d=10*(1 if e.num==4 else -1 if e.num==5 else e.delta/120)
        self.scale.set(max(60,min(400,self.scale.get()+d)))
        self.draw()

    def toggle_fullscreen(self):
        self._fullscreen=not self._fullscreen
        self.root.attributes("-fullscreen",self._fullscreen)

    def _xfs(self):
        if self._fullscreen:
            self._fullscreen=False
            self.root.attributes("-fullscreen",False)

    def toggle_spin(self):
        self._auto_spin=not self._auto_spin
        if self._auto_spin:
            self._spin_btn.configure(text="⏹  Стоп",bg="#e63946")
            self._anim()
        else:
            self._spin_btn.configure(text="▶  Авто-вращение",bg="#2563eb")
            if self._anim_id:
                self.root.after_cancel(self._anim_id)
                self._anim_id=None

    def _anim(self):
        if not self._auto_spin: return
        self.ay.set(self.ay.get()+0.018)
        self.ax.set(self.ax.get()+0.005)
        self.draw()
        self._anim_id=self.root.after(16,self._anim)

    def reset_view(self):
        self.ax.set(0.4); self.ay.set(0.6); self.az.set(0.0)
        self.scale.set(160); self.draw()

    def draw(self,_=None):
        self.renderer.render(self.ax.get(),self.ay.get(),
                              self.az.get(),self.scale.get())

if __name__=="__main__":
    root=tk.Tk()
    DiceApp(root)
    root.mainloop()