7 changed files with 3516 additions and 0 deletions
@ -0,0 +1,37 @@ |
|||||||
|
``` |
||||||
|
____ __ __ |
||||||
|
/\ _`\ /\ \__ __ /\ \__ |
||||||
|
\ \ \L\ \ __ _ __ ____ _____ __ ___\ \ ,_\/\_\ __ __ __ \ \ ,_\ ___ _ __ |
||||||
|
\ \ ,__/'__`\/\`'__\/',__\/\ '__`\ /'__`\ /'___\ \ \/\/\ \/\ \/\ \ /'__`\ \ \ \/ / __`\/\`'__\ |
||||||
|
\ \ \/\ __/\ \ \//\__, `\ \ \L\ \/\ __//\ \__/\ \ \_\ \ \ \ \_/ |/\ \L\.\_\ \ \_/\ \L\ \ \ \/ |
||||||
|
\ \_\ \____\\ \_\\/\____/\ \ ,__/\ \____\ \____\\ \__\\ \_\ \___/ \ \__/.\_\\ \__\ \____/\ \_\ |
||||||
|
\/_/\/____/ \/_/ \/___/ \ \ \/ \/____/\/____/ \/__/ \/_/\/__/ \/__/\/_/ \/__/\/___/ \/_/ |
||||||
|
\ \_\ |
||||||
|
\/_/ |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
 |
||||||
|
|
||||||
|
# Perspectivator |
||||||
|
|
||||||
|
**Perspectivator** is a tool designed to help you create stunning hero images for your software projects. With Perspectivator, you can easily compose multiple screenshots (or any image) onto a stylish, reflective surface, producing eye-catching visuals perfect for landing pages, presentations, or promotional materials. |
||||||
|
|
||||||
|
The app allows you to: |
||||||
|
- Use as many images as you want. |
||||||
|
- Arrange and transform them in perspective to simulate depth and realism. |
||||||
|
- Automatically generate realistic reflections and subtle effects. |
||||||
|
- Export high-quality hero images ready for use in your projects. |
||||||
|
|
||||||
|
Whether you're a developer, designer, or marketer, Perspectivator streamlines the process of producing professional hero images that showcase your software at its best. |
||||||
|
|
||||||
|
# Installation/Run |
||||||
|
```bash |
||||||
|
# Clone the git repo |
||||||
|
git clone https://github.com/ceccopierangiolieugenio/pyTermTk.git |
||||||
|
# Install the required packages |
||||||
|
pip install numpy pillow |
||||||
|
# Run Perspectivator |
||||||
|
python apps/perspectivator/perspectivator.py <List of Images (any format) or a configuration file (.json)> |
||||||
|
# Enjoy!!! |
||||||
|
``` |
||||||
@ -0,0 +1,992 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import sys,os |
||||||
|
import math |
||||||
|
import argparse |
||||||
|
import json |
||||||
|
|
||||||
|
from dataclasses import dataclass |
||||||
|
from typing import Optional,Tuple,List,Dict,Any |
||||||
|
|
||||||
|
import numpy as np,array |
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageOps |
||||||
|
|
||||||
|
sys.path.append(os.path.join(sys.path[0],'../..')) |
||||||
|
|
||||||
|
import TermTk as ttk |
||||||
|
|
||||||
|
@dataclass |
||||||
|
class RenderData: |
||||||
|
# fogNear: float |
||||||
|
# fogFar: float |
||||||
|
# bgColor: Tuple[int,int,int,int] |
||||||
|
resolution: Tuple[int,int] |
||||||
|
# outFile: str |
||||||
|
# offY: int |
||||||
|
# show: bool = False |
||||||
|
# mirror: Tuple[int,int] |
||||||
|
|
||||||
|
class _ThreadingData: |
||||||
|
__slots__ = ('timer') |
||||||
|
timer: ttk.TTkTimer |
||||||
|
def __init__(self): |
||||||
|
self.timer = ttk.TTkTimer() |
||||||
|
|
||||||
|
def find_coeffs(pa, pb): |
||||||
|
matrix = [] |
||||||
|
for p1, p2 in zip(pa, pb): |
||||||
|
matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]]) |
||||||
|
matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]]) |
||||||
|
|
||||||
|
A = np.matrix(matrix, dtype=float) |
||||||
|
B = np.array(pb).reshape(8) |
||||||
|
|
||||||
|
res = np.dot(np.linalg.inv(A.T * A) * A.T, B) |
||||||
|
return np.array(res).reshape(8) |
||||||
|
|
||||||
|
def project_point(camera_position, look_at, point_3d): |
||||||
|
""" |
||||||
|
Project a 3D point into a normalized projection matrix. |
||||||
|
|
||||||
|
Args: |
||||||
|
camera_position: The 3D position of the camera (x, y, z). |
||||||
|
look_at: The 3D position the camera is looking at (x, y, z). |
||||||
|
point_3d: The 3D point to project (x, y, z). |
||||||
|
|
||||||
|
Returns: |
||||||
|
The 2D coordinates of the projected point in normalized space. |
||||||
|
""" |
||||||
|
# Step 1: Calculate the forward, right, and up vectors |
||||||
|
def normalize(v): |
||||||
|
return v / np.linalg.norm(v) |
||||||
|
|
||||||
|
forward = normalize(np.array(look_at) - np.array(camera_position)) |
||||||
|
right = normalize(np.cross(forward, [0, 1, 0])) |
||||||
|
if np.all(np.isnan(right)): |
||||||
|
right = [0,1,0] |
||||||
|
up = np.cross(right, forward) |
||||||
|
|
||||||
|
# Step 2: Create the view matrix |
||||||
|
view_matrix = np.array([ |
||||||
|
[ right[0] , right[1] , right[2] , -np.dot( right , camera_position)], |
||||||
|
[ up[0] , up[1] , up[2] , -np.dot( up , camera_position)], |
||||||
|
[-forward[0] , -forward[1] , -forward[2] , np.dot(forward , camera_position)], |
||||||
|
[ 0 , 0 , 0 , 1] |
||||||
|
]) |
||||||
|
|
||||||
|
# Step 3: Create the projection matrix |
||||||
|
near = 1.0 # Near plane normalized to 1 |
||||||
|
width = 1.0 # Width of the near plane |
||||||
|
height = 1.0 # Height of the near plane |
||||||
|
aspect_ratio = width / height |
||||||
|
|
||||||
|
projection_matrix = np.array([ |
||||||
|
[1 / aspect_ratio, 0, 0, 0], |
||||||
|
[ 0, 1, 0, 0], |
||||||
|
[ 0, 0, -1, -2 * near], |
||||||
|
[ 0, 0, -1, 0] |
||||||
|
]) |
||||||
|
|
||||||
|
# Step 4: Transform the 3D point into clip space |
||||||
|
point_3d_homogeneous = np.array([point_3d[0], point_3d[1], point_3d[2], 1]) |
||||||
|
view_space_point = view_matrix @ point_3d_homogeneous |
||||||
|
clip_space_point = projection_matrix @ view_space_point |
||||||
|
|
||||||
|
# Step 5: Perform perspective divide to get normalized device coordinates (NDC) |
||||||
|
if clip_space_point[3] == 0: |
||||||
|
raise ValueError("Invalid projection: w component is zero.") |
||||||
|
ndc_x = clip_space_point[0] / clip_space_point[3] |
||||||
|
ndc_y = clip_space_point[1] / clip_space_point[3] |
||||||
|
|
||||||
|
return ndc_x, ndc_y |
||||||
|
|
||||||
|
class _Toolbox(): |
||||||
|
__slots__ = ('widget', 'updated') |
||||||
|
updated:ttk.pyTTkSignal |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
self.updated = ttk.pyTTkSignal() |
||||||
|
self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.toolbox.tui.json")) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot(str) |
||||||
|
def _presetChanged(preset): |
||||||
|
w,h = preset.split('x') |
||||||
|
self.widget.getWidgetByName("SB_HRes").setValue(int(w)) |
||||||
|
self.widget.getWidgetByName("SB_VRes").setValue(int(h)) |
||||||
|
|
||||||
|
pres = self.widget.getWidgetByName("CB_ResPresets") |
||||||
|
pres.addItems([ |
||||||
|
'320x200', '320x240', '400x300', '512x384', |
||||||
|
'640x400', '640x480', '800x600', '1024x768', |
||||||
|
'1152x864', '1280x720', '1280x800', '1280x960', |
||||||
|
'1280x1024', '1360x768', '1366x768', '1400x1050', |
||||||
|
'1440x900', '1600x900', '1600x1200', '1680x1050', |
||||||
|
'1920x1080', '1920x1200', '2048x1152', '2048x1536', |
||||||
|
'2560x1080', '2560x1440', '2560x1600', '2880x1800', |
||||||
|
'3200x1800', '3440x1440', '3840x2160', '4096x2160', |
||||||
|
'5120x2880', '7680x4320']) |
||||||
|
pres.setCurrentIndex(6) |
||||||
|
pres.currentTextChanged.connect(_presetChanged) |
||||||
|
|
||||||
|
aa = self.widget.getWidgetByName("CB_AA") |
||||||
|
aa.addItems(['0X','2X','3X','4X']) |
||||||
|
aa.setCurrentIndex(0) |
||||||
|
|
||||||
|
self.widget.getWidgetByName("SB_HRes").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_VRes").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("CB_Bg").stateChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("BG_Color").colorSelected.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_Fog_Near").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_Fog_Far").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_OffY").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_FadingFrom").valueChanged.connect(self._triggerUpdated) |
||||||
|
self.widget.getWidgetByName("SB_FadingTo").valueChanged.connect(self._triggerUpdated) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _triggerUpdated(self): |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def mirror(self) -> Tuple[float,float]: |
||||||
|
return ( |
||||||
|
self.widget.getWidgetByName("SB_FadingFrom").value() , |
||||||
|
self.widget.getWidgetByName("SB_FadingTo").value() ) |
||||||
|
|
||||||
|
def bg(self) -> Tuple[int,int,int,int]: |
||||||
|
if self.widget.getWidgetByName("CB_Bg").isChecked(): |
||||||
|
btnColor = self.widget.getWidgetByName("BG_Color").color() |
||||||
|
return (*btnColor.fgToRGB(),255) |
||||||
|
else: |
||||||
|
return (0,0,0,0) |
||||||
|
|
||||||
|
def fog(self) -> tuple[float,float]: |
||||||
|
return ( |
||||||
|
self.widget.getWidgetByName("SB_Fog_Near").value() , |
||||||
|
self.widget.getWidgetByName("SB_Fog_Far").value() ) |
||||||
|
|
||||||
|
def resolution(self) -> Tuple[int,int]: |
||||||
|
return( |
||||||
|
self.widget.getWidgetByName("SB_HRes").value(), |
||||||
|
self.widget.getWidgetByName("SB_VRes").value()) |
||||||
|
|
||||||
|
def offset_y(self) -> float: |
||||||
|
return self.widget.getWidgetByName("SB_OffY").value() |
||||||
|
|
||||||
|
def serialize(self) -> Dict: |
||||||
|
return { |
||||||
|
'bg':self.bg(), |
||||||
|
'fog':self.fog(), |
||||||
|
'resolution':self.resolution(), |
||||||
|
'offset_y': self.offset_y(), |
||||||
|
'mirror': self.mirror(), |
||||||
|
} |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def deserialize(data:Dict) -> '_Toolbox': |
||||||
|
ret = _Toolbox() |
||||||
|
ret.widget.getWidgetByName("SB_HRes").setValue(data['resolution'][0]) |
||||||
|
ret.widget.getWidgetByName("SB_VRes").setValue(data['resolution'][1]) |
||||||
|
if data['bg']==[0,0,0,0]: |
||||||
|
ret.widget.getWidgetByName("CB_Bg").setChecked(False) |
||||||
|
else: |
||||||
|
ret.widget.getWidgetByName("CB_Bg").setChecked(True) |
||||||
|
r = data['bg'][0] |
||||||
|
g = data['bg'][1] |
||||||
|
b = data['bg'][2] |
||||||
|
color = ttk.TTkColor.fg(f"#{r<<16|g<<8|b:06x}") |
||||||
|
ret.widget.getWidgetByName("BG_Color").setColor(color) |
||||||
|
ret.widget.getWidgetByName("SB_Fog_Near").setValue(data['fog'][0]) |
||||||
|
ret.widget.getWidgetByName("SB_Fog_Far").setValue(data['fog'][1]) |
||||||
|
ret.widget.getWidgetByName("SB_OffY").setValue(data['offset_y']) |
||||||
|
ret.widget.getWidgetByName("SB_OffY").setValue(data['offset_y']) |
||||||
|
ret.widget.getWidgetByName("SB_FadingFrom").setValue(data['mirror'][0]) |
||||||
|
ret.widget.getWidgetByName("SB_FadingTo").setValue(data['mirror'][1]) |
||||||
|
return ret |
||||||
|
|
||||||
|
class _Movable(): |
||||||
|
__slots__ = ('_x','_y','_z','data', 'selected', 'name','widget', 'updated') |
||||||
|
_x:int |
||||||
|
_y:int |
||||||
|
_z:int |
||||||
|
data:Dict[str,Any] |
||||||
|
selected:ttk.pyTTkSignal |
||||||
|
updated:ttk.pyTTkSignal |
||||||
|
name:ttk.TTkLabel |
||||||
|
widget:ttk.TTkWidget |
||||||
|
|
||||||
|
def __init__(self, x:int=0,y:int=0,z:int=0, name:str=""): |
||||||
|
self.selected = ttk.pyTTkSignal(_Movable) |
||||||
|
self.updated = ttk.pyTTkSignal() |
||||||
|
self.data = {} |
||||||
|
self._x = x |
||||||
|
self._y = y |
||||||
|
self._z = z |
||||||
|
self.name = ttk.TTkLabel(text=ttk.TTkString(name),maxHeight=1) |
||||||
|
self.widget = ttk.TTkWidget() |
||||||
|
|
||||||
|
def serialize(self) -> Dict: |
||||||
|
return { |
||||||
|
'x':self._x, |
||||||
|
'y':self._y, |
||||||
|
'z':self._z, |
||||||
|
'name': self.name.text().toAscii() |
||||||
|
} |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def deserialize(data:Dict) -> '_Movable': |
||||||
|
return _Movable(**data) |
||||||
|
|
||||||
|
def x(self) -> int: |
||||||
|
return self._x |
||||||
|
def setX(self, value:int): |
||||||
|
self._x = value |
||||||
|
self._updateBox() |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def y(self) -> int: |
||||||
|
return self._y |
||||||
|
def setY(self, value:int): |
||||||
|
self._y = value |
||||||
|
self._updateBox() |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def z(self) -> int: |
||||||
|
return self._z |
||||||
|
def setZ(self, value:int): |
||||||
|
self._z = value |
||||||
|
self._updateBox() |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def getBox(self) -> Tuple[int,int,int,int]: |
||||||
|
return (self.x-1,self._y,4,1) |
||||||
|
|
||||||
|
def _updateBox(self): |
||||||
|
pass |
||||||
|
|
||||||
|
class _Image(_Movable): |
||||||
|
__slots__ = ('_size','_tilt','fileName', 'box', 'image') |
||||||
|
_size:int |
||||||
|
_tilt:float |
||||||
|
fileName:str |
||||||
|
image:Image |
||||||
|
box:Tuple[int,int,int,int] |
||||||
|
def __init__(self,size:int,tilt:float,fileName:str, **kwargs): |
||||||
|
self._size = size |
||||||
|
self._tilt = tilt |
||||||
|
self.fileName = fileName |
||||||
|
super().__init__(**kwargs) |
||||||
|
self._loadStuff() |
||||||
|
|
||||||
|
def _loadStuff(self): |
||||||
|
self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.image.tui.json")) |
||||||
|
if not os.path.isfile(self.fileName): |
||||||
|
raise ValueError(f"{self.fileName} is not a file") |
||||||
|
try: |
||||||
|
self.image = Image.open(self.fileName).convert("RGBA") |
||||||
|
except FileNotFoundError: |
||||||
|
raise ValueError(f"Failed to open {self.fileName}") |
||||||
|
self._updateBox() |
||||||
|
self.widget.getWidgetByName("SB_X").valueChanged.connect(self.setX) |
||||||
|
self.widget.getWidgetByName("SB_Y").valueChanged.connect(self.setY) |
||||||
|
self.widget.getWidgetByName("SB_Z").valueChanged.connect(self.setZ) |
||||||
|
self.widget.getWidgetByName("SB_Tilt").valueChanged.connect(self.setTilt) |
||||||
|
self.widget.getWidgetByName("SB_Size").valueChanged.connect(self.setSize) |
||||||
|
self.updated.connect(self._updated) |
||||||
|
self._updated() |
||||||
|
|
||||||
|
def serialize(self) -> Dict: |
||||||
|
ret = super().serialize() |
||||||
|
return ret | { |
||||||
|
'size': self._size, |
||||||
|
'tilt': self._tilt, |
||||||
|
'fileName': self.fileName |
||||||
|
} |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def deserialize(data:Dict) -> '_Image': |
||||||
|
return _Image(**data) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _updated(self): |
||||||
|
self.widget.getWidgetByName("SB_X").setValue(self._x) |
||||||
|
self.widget.getWidgetByName("SB_Y").setValue(self._y) |
||||||
|
self.widget.getWidgetByName("SB_Z").setValue(self._z) |
||||||
|
self.widget.getWidgetByName("SB_Tilt").setValue(self._tilt) |
||||||
|
self.widget.getWidgetByName("SB_Size").setValue(self._size) |
||||||
|
|
||||||
|
def size(self) -> int: |
||||||
|
return self._size |
||||||
|
def setSize(self, value:int): |
||||||
|
self._size = value |
||||||
|
self._updateBox() |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def tilt(self) -> float: |
||||||
|
return self._tilt |
||||||
|
def setTilt(self, value:float): |
||||||
|
self._tilt = value |
||||||
|
self._updateBox() |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
def _updateBox(self): |
||||||
|
size = float(self._size) |
||||||
|
w = 1 + 2*size*abs(math.cos(self._tilt)) |
||||||
|
h = 1 + size*abs(math.sin(self._tilt)) |
||||||
|
self.box = ( |
||||||
|
int(self._x-w/2), |
||||||
|
int(self._y-h/2), |
||||||
|
int(w), int(h), |
||||||
|
) |
||||||
|
|
||||||
|
def getBox(self) -> Tuple[int,int,int,int]: |
||||||
|
return self.box |
||||||
|
|
||||||
|
class _Camera(_Movable): |
||||||
|
__slots__= ('_tilt') |
||||||
|
_tilt:float |
||||||
|
def __init__(self, tilt:float=0, **kwargs): |
||||||
|
self._tilt = tilt |
||||||
|
super().__init__(**kwargs) |
||||||
|
self._loadStuff() |
||||||
|
|
||||||
|
def _loadStuff(self): |
||||||
|
self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.camera.tui.json")) |
||||||
|
self.widget.getWidgetByName("SB_X").valueChanged.connect(self.setX) |
||||||
|
self.widget.getWidgetByName("SB_Y").valueChanged.connect(self.setY) |
||||||
|
self.widget.getWidgetByName("SB_Z").valueChanged.connect(self.setZ) |
||||||
|
self.widget.getWidgetByName("SB_Tilt").valueChanged.connect(self.setTilt) |
||||||
|
self.updated.connect(self._updated) |
||||||
|
self._updated() |
||||||
|
|
||||||
|
def serialize(self) -> Dict: |
||||||
|
ret = super().serialize() |
||||||
|
return ret | { |
||||||
|
'tilt': self._tilt, |
||||||
|
} |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def deserialize(data:Dict) -> '_Camera': |
||||||
|
return _Camera(**data) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _updated(self): |
||||||
|
self.widget.getWidgetByName("SB_X").setValue(self._x) |
||||||
|
self.widget.getWidgetByName("SB_Y").setValue(self._y) |
||||||
|
self.widget.getWidgetByName("SB_Z").setValue(self._z) |
||||||
|
self.widget.getWidgetByName("SB_Tilt").setValue(self._tilt) |
||||||
|
|
||||||
|
def tilt(self) -> float: |
||||||
|
return self._tilt |
||||||
|
def setTilt(self, value:float): |
||||||
|
self._tilt = value |
||||||
|
self.updated.emit() |
||||||
|
|
||||||
|
class _State(): |
||||||
|
__slots__ = ( |
||||||
|
'camera','images', 'toolbox', |
||||||
|
'_currentMovable','highlightedMovable', |
||||||
|
'currentMovableUpdated','updated') |
||||||
|
camera: _Camera |
||||||
|
images: List[_Image] |
||||||
|
_currentMovable: Optional[_Movable] |
||||||
|
highlightedMovable: Optional[_Movable] |
||||||
|
currentMovableUpdated: ttk.pyTTkSignal |
||||||
|
updated: ttk.pyTTkSignal |
||||||
|
def __init__(self, camera: _Camera, images: List[_Image], toolbox: Optional[_Toolbox]=None): |
||||||
|
if not toolbox: |
||||||
|
self.toolbox = _Toolbox() |
||||||
|
else: |
||||||
|
self.toolbox = toolbox |
||||||
|
self.currentMovableUpdated = ttk.pyTTkSignal(_Movable) |
||||||
|
self.updated = ttk.pyTTkSignal() |
||||||
|
self.camera = camera |
||||||
|
self.images = images |
||||||
|
self._currentMovable = None |
||||||
|
self.highlightedMovable = None |
||||||
|
self.camera.updated.connect(self.updated.emit) |
||||||
|
self.toolbox.updated.connect(self.updated.emit) |
||||||
|
for img in images: |
||||||
|
img.updated.connect(self.updated.emit) |
||||||
|
|
||||||
|
@property |
||||||
|
def currentMovable(self) -> Optional[_Movable]: |
||||||
|
return self._currentMovable |
||||||
|
@currentMovable.setter |
||||||
|
def currentMovable(self, value: Optional[_Movable]): |
||||||
|
if self._currentMovable != value: |
||||||
|
self._currentMovable = value |
||||||
|
if value: |
||||||
|
self.currentMovableUpdated.emit(value) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot(str) |
||||||
|
def save(self, fileName): |
||||||
|
data = { |
||||||
|
'camera': self.camera.serialize(), |
||||||
|
'images': [img.serialize() for img in self.images], |
||||||
|
'toolbox': self.toolbox.serialize() |
||||||
|
} |
||||||
|
with open(fileName, "w") as f: |
||||||
|
json.dump(data, f, indent=4) |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def load(fileName) -> '_State': |
||||||
|
with open(fileName, "r") as f: |
||||||
|
data = json.load(f) |
||||||
|
toolbox = _Toolbox.deserialize(data['toolbox']) |
||||||
|
images = [] |
||||||
|
for imgData in data['images']: |
||||||
|
images.append(_Image.deserialize(imgData)) |
||||||
|
state = _State( |
||||||
|
toolbox=toolbox, |
||||||
|
camera=_Camera.deserialize(data['camera']), |
||||||
|
images=images) |
||||||
|
|
||||||
|
return state |
||||||
|
|
||||||
|
class Perspectivator(ttk.TTkWidget): |
||||||
|
__slots__ = ('_state') |
||||||
|
_state:_State |
||||||
|
def __init__(self, state:_State, **kwargs): |
||||||
|
self._state = state |
||||||
|
super().__init__(**kwargs) |
||||||
|
self.setFocusPolicy(ttk.TTkK.ClickFocus) |
||||||
|
state.updated.connect(self.update) |
||||||
|
|
||||||
|
def mousePressEvent(self, evt): |
||||||
|
self._state.highlightedMovable = None |
||||||
|
self._state.currentMovable = None |
||||||
|
cx,cy = self._state.camera.x(),self._state.camera.y() |
||||||
|
if cx-1 <= evt.x <= cx+2 and cy-1 <= evt.y <= cy+1: |
||||||
|
self._state.currentMovable = self._state.camera |
||||||
|
self._state.camera.data |= { |
||||||
|
'mx':self._state.camera.x()-evt.x, |
||||||
|
'my':self._state.camera.y()-evt.y} |
||||||
|
else: |
||||||
|
for image in self._state.images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: |
||||||
|
image.data |= { |
||||||
|
'mx':image.x()-evt.x, |
||||||
|
'my':image.y()-evt.y} |
||||||
|
self._state.currentMovable = image |
||||||
|
index = self._state.images.index(image) |
||||||
|
self._state.images.pop(index) |
||||||
|
self._state.images.append(image) |
||||||
|
break |
||||||
|
if self._state.currentMovable: |
||||||
|
self._state.currentMovable.selected.emit(self._state.currentMovable) |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseMoveEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
self._state.highlightedMovable = None |
||||||
|
cx,cy = self._state.camera.x(),self._state.camera.y() |
||||||
|
if cx-1 <= evt.x <= cx+2 and cy-1 <= evt.y <= cy+1: |
||||||
|
self._state.highlightedMovable = self._state.camera |
||||||
|
else: |
||||||
|
for image in self._state.images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: |
||||||
|
self._state.highlightedMovable = image |
||||||
|
break |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseReleaseEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
self._state.highlightedMovable = None |
||||||
|
self._state.currentMovable = None |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseDragEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
if not (movable:=self._state.currentMovable): |
||||||
|
pass |
||||||
|
elif evt.key == ttk.TTkK.RightButton and isinstance(movable,_Image): |
||||||
|
x = evt.x-movable.x() |
||||||
|
y = evt.y-movable.y() |
||||||
|
movable.setTilt(math.atan2(x,y*2)) |
||||||
|
self.update() |
||||||
|
elif evt.key == ttk.TTkK.LeftButton: |
||||||
|
mx,my = movable.data['mx'],movable.data['my'] |
||||||
|
movable.setX(evt.x+mx) |
||||||
|
movable.setY(evt.y+my) |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def wheelEvent(self, evt:ttk.TTkMouseEvent) -> bool: |
||||||
|
if not ((image:=self._state.highlightedMovable) and isinstance(image,_Image)): |
||||||
|
pass |
||||||
|
elif evt.evt == ttk.TTkK.WHEEL_Up: |
||||||
|
image.setSize(min(image.size()+1,50)) |
||||||
|
self.update() |
||||||
|
elif evt.evt == ttk.TTkK.WHEEL_Down: |
||||||
|
image.setSize(max(image.size()-1,5)) |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def getImage(self, data:RenderData) -> Image: |
||||||
|
return self.getImagePil(data) |
||||||
|
|
||||||
|
def getImagePil(self, data:RenderData) -> Image: |
||||||
|
screen_width, screen_height = data.resolution |
||||||
|
w,h = screen_width,screen_height |
||||||
|
|
||||||
|
fog = self._state.toolbox.fog() |
||||||
|
fogNear=fog[0] |
||||||
|
fogFar=fog[1] |
||||||
|
bgColor=self._state.toolbox.bg() |
||||||
|
offY=self._state.toolbox.offset_y()*h/600 |
||||||
|
mirror = self._state.toolbox.mirror() |
||||||
|
|
||||||
|
ww,wh = self.size() |
||||||
|
cam_x = self._state.camera.x() |
||||||
|
cam_y = self._state.camera.y() |
||||||
|
cam_z = self._state.camera.z() |
||||||
|
cam_tilt = self._state.camera.tilt() |
||||||
|
observer = (cam_x , cam_z , cam_y ) # Observer's position |
||||||
|
# observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position |
||||||
|
dz = 10*math.cos(math.pi * cam_tilt/360) |
||||||
|
dy = 10*math.sin(math.pi * cam_tilt/360) |
||||||
|
look_at = (cam_x , cam_z-dy, cam_y+dz) # Observer is looking along the positive Z-axis |
||||||
|
|
||||||
|
prw,prh = screen_width/2, screen_height/2 |
||||||
|
|
||||||
|
# step1, sort Images based on the distance |
||||||
|
images = sorted(self._state.images,key=lambda img:img.getBox()[1]) |
||||||
|
znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF |
||||||
|
for img in images: |
||||||
|
ix,iy,iw,ih = img.getBox() |
||||||
|
iz = img.z() |
||||||
|
ih-=1 |
||||||
|
znear=min(znear,iy,iy+ih) |
||||||
|
zfar=max(zfar,iy,iy+ih) |
||||||
|
isz = img.size()*2 |
||||||
|
if math.pi/2 <= img.tilt() < math.pi or -math.pi <= img.tilt() < 0: |
||||||
|
zleft = iy |
||||||
|
zright = iy+ih |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , iz+isz , iy+ih )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , iz+isz , iy )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , iz , iy+ih )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , iz , iy )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , -iz-isz , iy+ih )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , -iz-isz , iy )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , -iz , iy+ih )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , -iz , iy )) |
||||||
|
else: |
||||||
|
zleft = iy+ih |
||||||
|
zright = iy |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , iz+isz , iy )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , iz+isz , iy+ih )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , iz , iy )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , iz , iy+ih )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , -iz-isz , iy )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , -iz-isz , iy+ih )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , -iz , iy )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , -iz , iy+ih )) |
||||||
|
img.data |= { |
||||||
|
'zleft':zleft, |
||||||
|
'zright':zright, |
||||||
|
'top' : { |
||||||
|
'p1':(int((ip1x+1)*prw) , int(offY+(ip1y+1)*prh)), |
||||||
|
'p2':(int((ip2x+1)*prw) , int(offY+(ip2y+1)*prh)), |
||||||
|
'p3':(int((ip3x+1)*prw) , int(offY+(ip3y+1)*prh)), |
||||||
|
'p4':(int((ip4x+1)*prw) , int(offY+(ip4y+1)*prh)), |
||||||
|
}, |
||||||
|
'bottom' : { |
||||||
|
'p1':(int((ip5x+1)*prw) , int(offY+(ip5y+1)*prh)), |
||||||
|
'p2':(int((ip6x+1)*prw) , int(offY+(ip6y+1)*prh)), |
||||||
|
'p3':(int((ip7x+1)*prw) , int(offY+(ip7y+1)*prh)), |
||||||
|
'p4':(int((ip8x+1)*prw) , int(offY+(ip8y+1)*prh)), |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
# step2, get all the layers and masks for alla the images |
||||||
|
for img in images: |
||||||
|
image = img.image |
||||||
|
|
||||||
|
imageTop = image.copy() |
||||||
|
imageBottom = image.copy() |
||||||
|
imageTopAlpha = imageTop.split()[-1] |
||||||
|
imageBottomAlpha = imageBottom.split()[-1] |
||||||
|
|
||||||
|
imw, imh = image.size |
||||||
|
|
||||||
|
# Create a gradient mask for the mirrored image |
||||||
|
gradient = Image.new("L", (imw, imh), 0) |
||||||
|
draw = ImageDraw.Draw(gradient) |
||||||
|
# Fading Math: |
||||||
|
# ----|--f--y___h-----t--- |
||||||
|
# av = 1-(y-f)/(t-f) |
||||||
|
# bv = 1-(y+h-f)/(t-f) |
||||||
|
_f,_t = mirror |
||||||
|
if _f == _t: |
||||||
|
_f-=0.01 |
||||||
|
_y = img.z() |
||||||
|
_h = img.size()*imh/imw |
||||||
|
_av = 1-(-_y-_f)/(_t-_f) |
||||||
|
_bv = 1-(-_y-_h-_f)/(_t-_f) |
||||||
|
for i in range(imh): |
||||||
|
_p = (i/imh) |
||||||
|
alpha = int(min(1,max(0,((_p)*_av+(1-_p)*_bv)))*255) # alpha goes from 0 to 204 |
||||||
|
draw.rectangle((0, i, imw, i), fill=alpha) |
||||||
|
# Apply the mirror mask to the image |
||||||
|
imageBottomAlphaGradient = ImageChops.multiply(imageBottomAlpha, gradient) |
||||||
|
|
||||||
|
# Create a gradient mask for the fog |
||||||
|
gradient = Image.new("L", (imw, imh), 0) |
||||||
|
draw = ImageDraw.Draw(gradient) |
||||||
|
for i in range(imw): |
||||||
|
an = 255-fogNear |
||||||
|
af = 255-fogFar |
||||||
|
zl = img.data['zleft'] |
||||||
|
zr = img.data['zright'] |
||||||
|
zi = (i/imw)*(zr-zl)+zl |
||||||
|
znorm = (zi-znear)/(zfar-znear) |
||||||
|
alpha = znorm*(an-af)+af |
||||||
|
draw.rectangle((i, 0, i, imh), fill=int(alpha)) |
||||||
|
# resultAlpha.show() |
||||||
|
imageTop.putalpha(ImageChops.multiply(imageTopAlpha, gradient)) |
||||||
|
imageBottom.putalpha(ImageChops.multiply(imageBottomAlphaGradient, gradient)) |
||||||
|
|
||||||
|
# Define the source and destination points |
||||||
|
src_points = [(imw, 0), (0, 0), (imw, imh), (0, imh)] |
||||||
|
dst_top = [ |
||||||
|
img.data['top']['p1'], |
||||||
|
img.data['top']['p2'], |
||||||
|
img.data['top']['p3'], |
||||||
|
img.data['top']['p4'], |
||||||
|
] |
||||||
|
dst_bottom = [ |
||||||
|
img.data['bottom']['p1'], |
||||||
|
img.data['bottom']['p2'], |
||||||
|
img.data['bottom']['p3'], |
||||||
|
img.data['bottom']['p4'], |
||||||
|
] |
||||||
|
def _transform(_img:Image, _dst:List) -> Image: |
||||||
|
return _img.transform( |
||||||
|
(w, h), Image.Transform.PERSPECTIVE, |
||||||
|
find_coeffs(_dst, src_points), |
||||||
|
Image.Resampling.BICUBIC) |
||||||
|
blurRadius = (math.sqrt(w*h)*4/math.sqrt(800*600)) |
||||||
|
img.data['imageTop'] = _transform(imageTop, dst_top) |
||||||
|
img.data['imageBottom'] = _transform(imageBottom, dst_bottom).filter(ImageFilter.BoxBlur(blurRadius)) |
||||||
|
img.data['imageTopAlpha'] = _transform(imageTopAlpha, dst_top) |
||||||
|
img.data['imageBottomAlpha'] = _transform(imageBottomAlpha, dst_bottom).filter(ImageFilter.BoxBlur(blurRadius)) |
||||||
|
|
||||||
|
# def _customBlur(_img:Image, _alpha:Image) -> Image: |
||||||
|
# thresholds = [(0,70), (70,150), (150,255)] |
||||||
|
# blur_radius = [7, 4, 0] |
||||||
|
# _out = Image.new("RGBA", _img.size, (0, 0, 0, 0)) |
||||||
|
# # Create a new image to store the blurred result |
||||||
|
# for (_f,_t),_r in zip(thresholds,blur_radius): |
||||||
|
# # Create a mask for the current threshold |
||||||
|
# _mask = _alpha.point(lambda p: p if _f < p <= _t else 0) |
||||||
|
# # Apply Gaussian blur to the image using the mask |
||||||
|
# _blurred = _img.filter(ImageFilter.BoxBlur(radius=_r)) |
||||||
|
# _blurred.putalpha(_mask) |
||||||
|
# # Composite the blurred image with the original image using the mask |
||||||
|
# _out = Image.alpha_composite(_out,_blurred) |
||||||
|
# #_alpha.show() |
||||||
|
# #_mask.show() |
||||||
|
# #_blurred.show() |
||||||
|
# #_out.show() |
||||||
|
# pass |
||||||
|
# return _out |
||||||
|
|
||||||
|
# img.data['imageBottom'] = _customBlur( |
||||||
|
# img.data['imageBottom'], img.data['imageBottom'].split()[-1]) |
||||||
|
|
||||||
|
# return image |
||||||
|
# Apply blur to the alpha channel |
||||||
|
# alpha = image.split()[-1] |
||||||
|
# alpha = alpha.filter(ImageFilter.GaussianBlur(radius=5)) |
||||||
|
# image.putalpha(alpha) |
||||||
|
# image = image.filter(ImageFilter.GaussianBlur(radius=3)) |
||||||
|
|
||||||
|
# Paste the processed image onto the output image |
||||||
|
|
||||||
|
# Create a new image with a transparent background |
||||||
|
outImage = Image.new("RGBA", (w, h), bgColor) |
||||||
|
|
||||||
|
# Apply the masks and Draw all the images |
||||||
|
for img in images: |
||||||
|
imageTop = img.data['imageTop'] |
||||||
|
imageBottom = img.data['imageBottom'] |
||||||
|
imageTopAlpha = imageTop.split()[-1] |
||||||
|
imageBottomAlpha = imageBottom.split()[-1] |
||||||
|
for maskImg in reversed(images): |
||||||
|
if img==maskImg: |
||||||
|
break |
||||||
|
maskTop = ImageOps.invert(maskImg.data['imageTopAlpha']) |
||||||
|
maskBottom = ImageOps.invert(maskImg.data['imageBottomAlpha']) |
||||||
|
imageTopAlpha = ImageChops.multiply(imageTopAlpha, maskTop) |
||||||
|
imageBottomAlpha = ImageChops.multiply(imageBottomAlpha, maskTop) |
||||||
|
imageBottomAlpha = ImageChops.multiply(imageBottomAlpha, maskBottom) |
||||||
|
|
||||||
|
|
||||||
|
imageTop.putalpha(imageTopAlpha) |
||||||
|
imageBottom.putalpha(imageBottomAlpha) |
||||||
|
|
||||||
|
# imageBottom.show() |
||||||
|
|
||||||
|
# outImage.paste(imageBottom,box=None,mask=imageBottom) |
||||||
|
# outImage.paste(imageTop,box=None,mask=imageTop) |
||||||
|
|
||||||
|
outImage = Image.alpha_composite(outImage,imageBottom) |
||||||
|
outImage = Image.alpha_composite(outImage,imageTop) |
||||||
|
# imageBottom.show() |
||||||
|
|
||||||
|
return outImage |
||||||
|
|
||||||
|
def paintEvent(self, canvas): |
||||||
|
w,h = self.size() |
||||||
|
cx,cy=self._state.camera.x(),self._state.camera.y() |
||||||
|
if self._state.highlightedMovable == self._state.camera: |
||||||
|
canvas.drawTTkString(pos=(cx-1,cy),text=ttk.TTkString("<😘>")) |
||||||
|
elif self._state.currentMovable == self._state.camera: |
||||||
|
canvas.drawTTkString(pos=(cx,cy),text=ttk.TTkString("😍")) |
||||||
|
else: |
||||||
|
canvas.drawTTkString(pos=(cx,cy),text=ttk.TTkString("😎")) |
||||||
|
|
||||||
|
# Draw Fov |
||||||
|
for y in range(cy): |
||||||
|
canvas.drawChar(char='/', pos=(cx+cy-y+1,y)) |
||||||
|
canvas.drawChar(char='\\',pos=(cx-cy+y,y)) |
||||||
|
|
||||||
|
# Draw Images |
||||||
|
for image in self._state.images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
canvas.drawText(pos=(ix,iy-1),text=f"{image.tilt():.2f}", color=ttk.TTkColor.YELLOW) |
||||||
|
canvas.drawText(pos=(ix+5,iy-1),text=f"{image.fileName}", color=ttk.TTkColor.CYAN) |
||||||
|
if image == self._state.highlightedMovable: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.GREEN) |
||||||
|
elif image == self._state.currentMovable: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.YELLOW) |
||||||
|
else: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+') |
||||||
|
if ih > iw > 1: |
||||||
|
for dy in range(ih): |
||||||
|
dx = iw*dy//ih |
||||||
|
if ( |
||||||
|
math.pi/2 < image.tilt() < math.pi or |
||||||
|
-math.pi/2 < image.tilt() < 0 ): |
||||||
|
canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) |
||||||
|
else: |
||||||
|
canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) |
||||||
|
elif iw >= ih > 1: |
||||||
|
for dx in range(iw): |
||||||
|
dy = ih*dx//iw |
||||||
|
if ( |
||||||
|
math.pi/2 < image.tilt() < math.pi or |
||||||
|
-math.pi/2 < image.tilt() < 0 ): |
||||||
|
canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) |
||||||
|
else: |
||||||
|
canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) |
||||||
|
|
||||||
|
class _Preview(ttk.TTkWidget): |
||||||
|
__slots__ = ('_canvasImage') |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self._canvasImage = ttk.TTkCanvas(width=20,height=3) |
||||||
|
self._canvasImage.drawText(pos=(0,0),text="Preview...") |
||||||
|
super().__init__(**kwargs) |
||||||
|
|
||||||
|
def updateCanvas(self, img:Image): |
||||||
|
w,h = img.size |
||||||
|
pixels = img.load() |
||||||
|
self._canvasImage.resize(w,h//2) |
||||||
|
self._canvasImage.updateSize() |
||||||
|
for x in range(w): |
||||||
|
for y in range(h//2): |
||||||
|
bg = 100 if (x//4+y//2)%2 else 150 |
||||||
|
p1 = pixels[x,y*2] |
||||||
|
p2 = pixels[x,y*2+1] |
||||||
|
def _c2hex(p) -> str: |
||||||
|
a = p[3]/255 |
||||||
|
r = int(bg*(1-a)+a*p[0]) |
||||||
|
g = int(bg*(1-a)+a*p[1]) |
||||||
|
b = int(bg*(1-a)+a*p[2]) |
||||||
|
return f"#{r<<16|g<<8|b:06x}" |
||||||
|
c = ttk.TTkColor.fgbg(_c2hex(p2),_c2hex(p1)) |
||||||
|
self._canvasImage.drawChar(pos=(x,y), char='▄', color=c) |
||||||
|
self.update() |
||||||
|
|
||||||
|
def paintEvent(self, canvas): |
||||||
|
w,h = self.size() |
||||||
|
canvas.paintCanvas(self._canvasImage, (0,0,w,h), (0,0,w,h), (0,0,w,h)) |
||||||
|
|
||||||
|
class ControlPanel(ttk.TTkSplitter): |
||||||
|
__slots__ = ( |
||||||
|
'previewPressed','renderPressed','_toolbox', '_previewImage', '_state', '_movableLayout','_threadData') |
||||||
|
def __init__(self, state:_State, **kwargs): |
||||||
|
self._threadData:_ThreadingData = _ThreadingData() |
||||||
|
self.previewPressed = ttk.pyTTkSignal(RenderData) |
||||||
|
self.renderPressed = ttk.pyTTkSignal(RenderData) |
||||||
|
self._movableLayout = ttk.TTkGridLayout() |
||||||
|
self._movableLayout.addItem(ttk.TTkLayout(),2,0) |
||||||
|
self._state = state |
||||||
|
super().__init__(**kwargs|{"orientation":ttk.TTkK.VERTICAL}) |
||||||
|
self._previewImage = _Preview() |
||||||
|
|
||||||
|
self._state.toolbox.widget.getWidgetByName("Btn_Render").clicked.connect(self._renderClicked) |
||||||
|
self._state.toolbox.widget.getWidgetByName("Btn_Preview").clicked.connect(self._previewChanged) |
||||||
|
self._state.toolbox.widget.getWidgetByName("Btn_SaveCfg").clicked.connect(self._saveCfg) |
||||||
|
self._state.updated.connect(self._previewChanged) |
||||||
|
self.addWidget(self._previewImage,size=5) |
||||||
|
self.addWidget(self._state.toolbox.widget) |
||||||
|
self.addItem(self._movableLayout) |
||||||
|
state.currentMovableUpdated.connect(self._movableChanged) |
||||||
|
self._threadData.timer.timeout.connect(self._previewThread) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot(_Movable) |
||||||
|
def _movableChanged(self, movable:_Movable): |
||||||
|
if isinstance(movable,_Movable): |
||||||
|
self._movableLayout.addWidget(movable.name,0,0) |
||||||
|
self._movableLayout.addWidget(movable.widget,1,0) |
||||||
|
else: |
||||||
|
raise ValueError(f"Unknown movable {movable}") |
||||||
|
|
||||||
|
def drawPreview(self, img:Image): |
||||||
|
self._previewImage.updateCanvas(img) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _saveCfg(self): |
||||||
|
filePath = os.path.join(os.path.abspath('.'),'perspectivator.cfg.json') |
||||||
|
filePicker = ttk.TTkFileDialogPicker( |
||||||
|
pos = (3,3), size=(80,30), |
||||||
|
acceptMode=ttk.TTkK.AcceptMode.AcceptSave, |
||||||
|
caption="Save As...", |
||||||
|
fileMode=ttk.TTkK.FileMode.AnyFile, |
||||||
|
path=filePath, |
||||||
|
filter="TTk Tui Files (*.cfg.json);;Json Files (*.json);;All Files (*)") |
||||||
|
filePicker.pathPicked.connect(self._state.save) |
||||||
|
ttk.TTkHelper.overlay(None, filePicker, 5, 5, True) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _renderClicked(self): |
||||||
|
w,h = self._state.toolbox.resolution() |
||||||
|
# fog = self._state.toolbox.fog() |
||||||
|
mult = { |
||||||
|
'0X':1,'2X':2,'3X':3,'4X':4}.get( |
||||||
|
self._state.toolbox.widget.getWidgetByName("CB_AA").currentText(),1) |
||||||
|
waa = w*mult |
||||||
|
haa = h*mult |
||||||
|
data = RenderData( |
||||||
|
# outFile=self._state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii(), |
||||||
|
# fogNear=fog[0], |
||||||
|
# fogFar=fog[1], |
||||||
|
# bgColor=self._state.toolbox.bg(), |
||||||
|
resolution=(waa,haa), |
||||||
|
# offY=self._state.toolbox.offset_y(), |
||||||
|
# show = self._state.toolbox.widget.getWidgetByName("CB_Show").isChecked(), |
||||||
|
# mirror = self._state.toolbox.mirror() |
||||||
|
) |
||||||
|
self.renderPressed.emit(data) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _previewChanged(self): |
||||||
|
if not self._state.toolbox.widget.getWidgetByName("Btn_Preview").isChecked(): |
||||||
|
return |
||||||
|
self._threadData.timer.start() |
||||||
|
|
||||||
|
def _previewThread(self): |
||||||
|
w,h = self._previewImage.size() |
||||||
|
# fog = self._state.toolbox.fog() |
||||||
|
data = RenderData( |
||||||
|
# outFile=self._state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii(), |
||||||
|
# fogNear=fog[0], |
||||||
|
# fogFar=fog[1], |
||||||
|
# bgColor=self._state.toolbox.bg(), |
||||||
|
resolution=(w,h*2), |
||||||
|
# offY=self._state.toolbox.offset_y(), |
||||||
|
# show = self._state.toolbox.widget.getWidgetByName("CB_Show").isChecked(), |
||||||
|
# mirror = self._state.toolbox.mirror() |
||||||
|
) |
||||||
|
self.previewPressed.emit(data) |
||||||
|
|
||||||
|
def main(): |
||||||
|
parser = argparse.ArgumentParser() |
||||||
|
parser.add_argument('filename', type=str, nargs='+', |
||||||
|
help='the images to compose or the json config file') |
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) |
||||||
|
|
||||||
|
root = ttk.TTk( |
||||||
|
layout=ttk.TTkGridLayout(), |
||||||
|
title="TTkode", |
||||||
|
mouseTrack=True) |
||||||
|
|
||||||
|
|
||||||
|
if len(args.filename) == 1 and args.filename[0].endswith('.json'): |
||||||
|
_state = _State.load(args.filename[0]) |
||||||
|
else: |
||||||
|
_images = [] |
||||||
|
_camera = _Camera(x=25,y=25,z=5, name="Camera") |
||||||
|
for fileName in args.filename: |
||||||
|
d=len(_images)*2 |
||||||
|
image = _Image(x=25+d,y=15+d, z=0, size=5, tilt=0,fileName=fileName, name=fileName) |
||||||
|
_camera.setX(_camera.x()+1) |
||||||
|
_camera.setY(_camera.y()+1) |
||||||
|
_images.append(image) |
||||||
|
_state = _State( |
||||||
|
camera=_camera, |
||||||
|
images=_images) |
||||||
|
|
||||||
|
perspectivator = Perspectivator(state=_state) |
||||||
|
controlPanel = ControlPanel(state=_state) |
||||||
|
|
||||||
|
at = ttk.TTkAppTemplate() |
||||||
|
at.setWidget(widget=perspectivator,position=at.MAIN) |
||||||
|
at.setWidget(widget=controlPanel,position=at.RIGHT, size=30) |
||||||
|
|
||||||
|
root.layout().addWidget(at) |
||||||
|
|
||||||
|
def _render(data:RenderData): |
||||||
|
outImage = perspectivator.getImage(data) |
||||||
|
# outImage.save(filename='outImage.png') |
||||||
|
|
||||||
|
bbox = outImage.getbbox() |
||||||
|
if bbox: |
||||||
|
outImage = outImage.crop(bbox) |
||||||
|
|
||||||
|
outw,outh = _state.toolbox.resolution() |
||||||
|
cropw,croph = outImage.size |
||||||
|
|
||||||
|
outImage = outImage.resize((outw,outh*croph//cropw), Image.LANCZOS) |
||||||
|
outImage.save(_state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii()) |
||||||
|
if _state.toolbox.widget.getWidgetByName("CB_Show").isChecked(): |
||||||
|
outImage.show() |
||||||
|
|
||||||
|
def _preview(data): |
||||||
|
img = perspectivator.getImage(data) |
||||||
|
controlPanel.drawPreview(img) |
||||||
|
|
||||||
|
controlPanel.renderPressed.connect(_render) |
||||||
|
controlPanel.previewPressed.connect(_preview) |
||||||
|
|
||||||
|
root.mainloop() |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
||||||
@ -0,0 +1,798 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import sys,os |
||||||
|
import math |
||||||
|
import argparse |
||||||
|
from dataclasses import dataclass |
||||||
|
from typing import Optional,Tuple,List,Dict |
||||||
|
|
||||||
|
import numpy as np |
||||||
|
|
||||||
|
from wand.image import Image |
||||||
|
from wand.drawing import Drawing |
||||||
|
from wand.color import Color |
||||||
|
|
||||||
|
# from PIL import Image, ImageDraw, ImageFilter |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sys.path.append(os.path.join(sys.path[0],'../..')) |
||||||
|
|
||||||
|
import TermTk as ttk |
||||||
|
|
||||||
|
@dataclass |
||||||
|
class RenderData: |
||||||
|
cameraY: int |
||||||
|
cameraAngle: float |
||||||
|
bgColor: Tuple[int,int,int,int] |
||||||
|
resolution: Tuple[int,int] |
||||||
|
|
||||||
|
def project_point(camera_position, look_at, point_3d): |
||||||
|
""" |
||||||
|
Project a 3D point into a normalized projection matrix. |
||||||
|
|
||||||
|
Args: |
||||||
|
camera_position: The 3D position of the camera (x, y, z). |
||||||
|
look_at: The 3D position the camera is looking at (x, y, z). |
||||||
|
point_3d: The 3D point to project (x, y, z). |
||||||
|
|
||||||
|
Returns: |
||||||
|
The 2D coordinates of the projected point in normalized space. |
||||||
|
""" |
||||||
|
# Step 1: Calculate the forward, right, and up vectors |
||||||
|
def normalize(v): |
||||||
|
return v / np.linalg.norm(v) |
||||||
|
|
||||||
|
forward = normalize(np.array(look_at) - np.array(camera_position)) |
||||||
|
right = normalize(np.cross(forward, [0, 1, 0])) |
||||||
|
up = np.cross(right, forward) |
||||||
|
|
||||||
|
# Step 2: Create the view matrix |
||||||
|
view_matrix = np.array([ |
||||||
|
[ right[0] , right[1] , right[2] , -np.dot( right , camera_position)], |
||||||
|
[ up[0] , up[1] , up[2] , -np.dot( up , camera_position)], |
||||||
|
[-forward[0] , -forward[1] , -forward[2] , np.dot(forward , camera_position)], |
||||||
|
[ 0 , 0 , 0 , 1] |
||||||
|
]) |
||||||
|
|
||||||
|
# Step 3: Create the projection matrix |
||||||
|
near = 1.0 # Near plane normalized to 1 |
||||||
|
width = 1.0 # Width of the near plane |
||||||
|
height = 1.0 # Height of the near plane |
||||||
|
aspect_ratio = width / height |
||||||
|
|
||||||
|
projection_matrix = np.array([ |
||||||
|
[1 / aspect_ratio, 0, 0, 0], |
||||||
|
[ 0, 1, 0, 0], |
||||||
|
[ 0, 0, -1, -2 * near], |
||||||
|
[ 0, 0, -1, 0] |
||||||
|
]) |
||||||
|
|
||||||
|
# Step 4: Transform the 3D point into clip space |
||||||
|
point_3d_homogeneous = np.array([point_3d[0], point_3d[1], point_3d[2], 1]) |
||||||
|
view_space_point = view_matrix @ point_3d_homogeneous |
||||||
|
clip_space_point = projection_matrix @ view_space_point |
||||||
|
|
||||||
|
# Step 5: Perform perspective divide to get normalized device coordinates (NDC) |
||||||
|
if clip_space_point[3] == 0: |
||||||
|
raise ValueError("Invalid projection: w component is zero.") |
||||||
|
ndc_x = clip_space_point[0] / clip_space_point[3] |
||||||
|
ndc_y = clip_space_point[1] / clip_space_point[3] |
||||||
|
|
||||||
|
return ndc_x, ndc_y |
||||||
|
|
||||||
|
def project_3d_to_2d(observer, look_at, fov_h, fov_v, screen_width, screen_height, point_3d): |
||||||
|
""" |
||||||
|
Project a 3D point onto a 2D screen. |
||||||
|
|
||||||
|
Args: |
||||||
|
observer: The observer's position in 3D space (x, y, z). |
||||||
|
look_at: The point the observer is looking at in 3D space (x, y, z). |
||||||
|
fov_h: Horizontal field of view in radians. |
||||||
|
fov_v: Vertical field of view in radians. |
||||||
|
screen_width: Width of the 2D screen. |
||||||
|
screen_height: Height of the 2D screen. |
||||||
|
point_3d: The 3D point to project (x, y, z). |
||||||
|
|
||||||
|
Returns: |
||||||
|
The 2D coordinates of the projected point (x, y) on the screen. |
||||||
|
""" |
||||||
|
# Step 1: Calculate the forward, right, and up vectors |
||||||
|
def normalize(v): |
||||||
|
length = math.sqrt(sum(coord ** 2 for coord in v)) |
||||||
|
return tuple(coord / length for coord in v) |
||||||
|
|
||||||
|
forward = normalize((look_at[0] - observer[0], look_at[1] - observer[1], look_at[2] - observer[2])) |
||||||
|
right = normalize(( |
||||||
|
forward[1] * 0 - forward[2] * 1, |
||||||
|
forward[2] * 0 - forward[0] * 0, |
||||||
|
forward[0] * 1 - forward[1] * 0 |
||||||
|
)) |
||||||
|
up = ( |
||||||
|
right[1] * forward[2] - right[2] * forward[1], |
||||||
|
right[2] * forward[0] - right[0] * forward[2], |
||||||
|
right[0] * forward[1] - right[1] * forward[0] |
||||||
|
) |
||||||
|
|
||||||
|
# Step 2: Transform the 3D point into the observer's coordinate system |
||||||
|
relative_point = ( |
||||||
|
point_3d[0] - observer[0], |
||||||
|
point_3d[1] - observer[1], |
||||||
|
point_3d[2] - observer[2] |
||||||
|
) |
||||||
|
x_in_view = sum(relative_point[i] * right[i] for i in range(3)) |
||||||
|
y_in_view = sum(relative_point[i] * up[i] for i in range(3)) |
||||||
|
z_in_view = sum(relative_point[i] * forward[i] for i in range(3)) |
||||||
|
|
||||||
|
# Step 3: Perform perspective projection |
||||||
|
if z_in_view <= 0: |
||||||
|
raise ValueError("The point is behind the observer and cannot be projected.") |
||||||
|
|
||||||
|
aspect_ratio = screen_width / screen_height |
||||||
|
tan_fov_h = math.tan(fov_h / 2) |
||||||
|
tan_fov_v = math.tan(fov_v / 2) |
||||||
|
|
||||||
|
ndc_x = x_in_view / (z_in_view * tan_fov_h * aspect_ratio) |
||||||
|
ndc_y = y_in_view / (z_in_view * tan_fov_v) |
||||||
|
|
||||||
|
# Step 4: Map normalized device coordinates (NDC) to screen coordinates |
||||||
|
screen_x = (ndc_x + 1) / 2 * screen_width |
||||||
|
screen_y = (1 - ndc_y) / 2 * screen_height |
||||||
|
|
||||||
|
return int(screen_x), int(screen_y) |
||||||
|
|
||||||
|
class _Image(): |
||||||
|
__slots__ = ('x','y','size','tilt','fileName', 'box', 'data') |
||||||
|
x:int |
||||||
|
y:int |
||||||
|
size:int |
||||||
|
tilt:float |
||||||
|
fileName:str |
||||||
|
data:Dict |
||||||
|
box:Tuple[int,int,int,int] |
||||||
|
def __init__(self,x:int,y:int,size:int,tilt:float,fileName:str): |
||||||
|
if not os.path.isfile(fileName): |
||||||
|
raise ValueError(f"{fileName} is not a file") |
||||||
|
self.x = x |
||||||
|
self.y = y |
||||||
|
self.size = size |
||||||
|
self.tilt = tilt |
||||||
|
self.fileName = fileName |
||||||
|
self.data={} |
||||||
|
self._updateBox() |
||||||
|
|
||||||
|
def _updateBox(self): |
||||||
|
size:float = float(self.size) |
||||||
|
w:float = 1 + 2*size*abs(math.cos(self.tilt)) |
||||||
|
h:float = 1 + size*abs(math.sin(self.tilt)) |
||||||
|
self.box = ( |
||||||
|
int(self.x-w/2), |
||||||
|
int(self.y-h/2), |
||||||
|
int(w), int(h), |
||||||
|
) |
||||||
|
|
||||||
|
def getBox(self) -> Tuple[int,int,int,int]: |
||||||
|
return self.box |
||||||
|
|
||||||
|
class Perspectivator(ttk.TTkWidget): |
||||||
|
__slots__ = ('_images','_currentImage','_highlightedImage') |
||||||
|
_highlightedImage:Optional[_Image] |
||||||
|
_currentImage:Optional[_Image] |
||||||
|
_images:List[_Image] |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self._highlightedImage = None |
||||||
|
self._currentImage = None |
||||||
|
self._images = [] |
||||||
|
super().__init__(**kwargs) |
||||||
|
self.setFocusPolicy(ttk.TTkK.ClickFocus) |
||||||
|
|
||||||
|
def addImage(self, fileName:str): |
||||||
|
d=len(self._images)*2 |
||||||
|
image = _Image(x=25+d,y=5+d, size=5, tilt=0,fileName=fileName) |
||||||
|
self._images.append(image) |
||||||
|
self.update() |
||||||
|
|
||||||
|
def mousePressEvent(self, evt): |
||||||
|
self._highlightedImage = None |
||||||
|
self._currentImage = None |
||||||
|
for image in self._images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: |
||||||
|
image.data = { |
||||||
|
'mx':image.x-evt.x, |
||||||
|
'my':image.y-evt.y} |
||||||
|
self._currentImage = image |
||||||
|
index = self._images.index(image) |
||||||
|
self._images.pop(index) |
||||||
|
self._images.append(image) |
||||||
|
break |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseMoveEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
self._highlightedImage = None |
||||||
|
for image in self._images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: |
||||||
|
self._highlightedImage = image |
||||||
|
break |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseReleaseEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
self._highlightedImage = None |
||||||
|
self._currentImage = None |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def mouseDragEvent(self, evt:ttk.TTkMouseEvent): |
||||||
|
if not (image:=self._currentImage): |
||||||
|
pass |
||||||
|
elif evt.key == ttk.TTkK.RightButton: |
||||||
|
x = evt.x-image.x |
||||||
|
y = evt.y-image.y |
||||||
|
image.tilt = math.atan2(x,y*2) |
||||||
|
image._updateBox() |
||||||
|
self.update() |
||||||
|
elif evt.key == ttk.TTkK.LeftButton: |
||||||
|
mx,my = image.data['mx'],image.data['my'] |
||||||
|
image.x = evt.x+mx |
||||||
|
image.y = evt.y+my |
||||||
|
image._updateBox() |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
def wheelEvent(self, evt:ttk.TTkMouseEvent) -> bool: |
||||||
|
if not (image:=self._highlightedImage): |
||||||
|
pass |
||||||
|
elif evt.evt == ttk.TTkK.WHEEL_Up: |
||||||
|
image.size = min(image.size+1,50) |
||||||
|
image._updateBox() |
||||||
|
self.update() |
||||||
|
elif evt.evt == ttk.TTkK.WHEEL_Down: |
||||||
|
image.size = max(image.size-1,5) |
||||||
|
image._updateBox() |
||||||
|
self.update() |
||||||
|
return True |
||||||
|
|
||||||
|
@ttk.pyTTkSlot(RenderData) |
||||||
|
def render(self, data:RenderData): |
||||||
|
outImage = self.getImage(data) |
||||||
|
outImage.save(filename='outImage.png') |
||||||
|
# outImage.save('outImage.png') |
||||||
|
|
||||||
|
def getImage(self, data:RenderData) -> Image: |
||||||
|
return self.getImageWand(data) |
||||||
|
|
||||||
|
def getImageWand(self, data:RenderData) -> Image: |
||||||
|
ww,wh = self.size() |
||||||
|
observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position |
||||||
|
dz = 10*math.cos(math.pi + math.pi * data.cameraAngle/360) |
||||||
|
dy = 10*math.sin(math.pi + math.pi * data.cameraAngle/360) |
||||||
|
look_at = (ww/2 , dy-data.cameraY , wh-3+dz) # Observer is looking along the positive Z-axis |
||||||
|
screen_width, screen_height = data.resolution |
||||||
|
w,h = screen_width,screen_height |
||||||
|
prw,prh = screen_width/2, screen_height/2 |
||||||
|
|
||||||
|
# step1, sort Images based on the distance |
||||||
|
images = sorted(self._images,key=lambda img:img.getBox()[1]) |
||||||
|
znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF |
||||||
|
for img in images: |
||||||
|
ix,iy,iw,ih = img.getBox() |
||||||
|
ih-=1 |
||||||
|
znear=min(znear,iy,iy+ih) |
||||||
|
zfar=max(zfar,iy,iy+ih) |
||||||
|
isz = img.size*2 |
||||||
|
if math.pi/2 <= img.tilt < math.pi or -math.pi <= img.tilt < 0: |
||||||
|
zleft = iy |
||||||
|
zright = iy+ih |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy+ih )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy+ih )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , 0 , iy )) |
||||||
|
else: |
||||||
|
zleft = iy+ih |
||||||
|
zright = iy |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy+ih )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy+ih )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy+ih )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0.1 , iy )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , 0.1 , iy+ih )) |
||||||
|
img.data = { |
||||||
|
'zleft':zleft, |
||||||
|
'zright':zright, |
||||||
|
'top' : { |
||||||
|
'p1':(int((ip1x+1)*prw) , int((ip1y+1)*prh)), |
||||||
|
'p2':(int((ip2x+1)*prw) , int((ip2y+1)*prh)), |
||||||
|
'p3':(int((ip3x+1)*prw) , int((ip3y+1)*prh)), |
||||||
|
'p4':(int((ip4x+1)*prw) , int((ip4y+1)*prh)), |
||||||
|
}, |
||||||
|
'bottom' : { |
||||||
|
'p1':(int((ip5x+1)*prw) , int((ip5y+1)*prh)), |
||||||
|
'p2':(int((ip6x+1)*prw) , int((ip6y+1)*prh)), |
||||||
|
'p3':(int((ip7x+1)*prw) , int((ip7y+1)*prh)), |
||||||
|
'p4':(int((ip8x+1)*prw) , int((ip8y+1)*prh)), |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
# with Image(width=w, height=h, background=Color('yellow')) as outImage: |
||||||
|
# with Image(width=w, height=h, background=Color(f'rgba{data.bgColor}')) as outImage: |
||||||
|
# with Image(width=w, height=h) as outImage: |
||||||
|
outImage = Image(width=w, height=h, background=Color(f'rgba{data.bgColor}')) |
||||||
|
|
||||||
|
# step2, render the mirrored images |
||||||
|
for img in images: |
||||||
|
with Image(filename=img.fileName) as image: |
||||||
|
image.background_color = Color('transparent') |
||||||
|
image.virtual_pixel = 'background' |
||||||
|
imw,imh = image.width, image.height |
||||||
|
# with Image(width=imw, height=imh) as gradient: |
||||||
|
with Image(width=imw, height=imh, pseudo='gradient:rgba(0,0,0,0)-rgba(1,1,1,0.8)') as gradient: |
||||||
|
gradient.alpha_channel = 'activate' |
||||||
|
# outImage.composite(gradient,0,0) |
||||||
|
gradient.transparent_color(Color('white'), alpha=0.0, fuzz=0) |
||||||
|
# Apply the mask to the image |
||||||
|
image.composite_channel('alpha', gradient, 'copy_alpha', 0, 0) |
||||||
|
prw,prh = screen_width/2, screen_height/2 |
||||||
|
image.distort('perspective', [ |
||||||
|
# From: to: |
||||||
|
imw , 0 , *img.data['bottom']['p1'] , |
||||||
|
0 , 0 , *img.data['bottom']['p2'] , |
||||||
|
imw , imh , *img.data['bottom']['p3'] , |
||||||
|
0 , imh , *img.data['bottom']['p4'] |
||||||
|
]) |
||||||
|
# # Mask the image |
||||||
|
# image.alpha_channel = 'activate' |
||||||
|
# for maskImg in reversed(images): |
||||||
|
# if img==maskImg: |
||||||
|
# break |
||||||
|
# with Drawing() as draw: |
||||||
|
# # draw.fill_color = Color('rgba(0, 0, 1, 0.5)') |
||||||
|
# draw.fill_color = Color('transparent') |
||||||
|
# draw.fill_opacity = 0.1 # Fully opaque |
||||||
|
# points = [ |
||||||
|
# maskImg.data['bottom']['p1'], |
||||||
|
# maskImg.data['bottom']['p2'], |
||||||
|
# maskImg.data['bottom']['p4'], |
||||||
|
# maskImg.data['bottom']['p3'], |
||||||
|
# ] |
||||||
|
# draw.polygon(points) |
||||||
|
# draw(image) |
||||||
|
# Mask the image |
||||||
|
image.alpha_channel = 'activate' |
||||||
|
for maskImg in reversed(images): |
||||||
|
if img==maskImg: |
||||||
|
break |
||||||
|
with Image(width=imw, height=imh, background=Color('transparent')) as transparent_img: |
||||||
|
with Drawing() as draw: |
||||||
|
draw.fill_color = Color('black') # Opaque |
||||||
|
draw.fill_opacity = 1.0 # Fully opaque |
||||||
|
draw.stroke_color = Color('black') |
||||||
|
draw.stroke_width = 0 |
||||||
|
points = [ |
||||||
|
maskImg.data['top']['p1'], |
||||||
|
maskImg.data['top']['p2'], |
||||||
|
maskImg.data['top']['p4'], |
||||||
|
maskImg.data['top']['p3'], |
||||||
|
] |
||||||
|
draw.polygon(points) |
||||||
|
points = [ |
||||||
|
maskImg.data['bottom']['p1'], |
||||||
|
maskImg.data['bottom']['p2'], |
||||||
|
maskImg.data['bottom']['p4'], |
||||||
|
maskImg.data['bottom']['p3'], |
||||||
|
] |
||||||
|
draw.polygon(points) |
||||||
|
draw(transparent_img) |
||||||
|
image.composite(transparent_img, left=0, top=0, operator='dst_out') |
||||||
|
|
||||||
|
|
||||||
|
# Blur the mirrored image based on the alpha |
||||||
|
alpha_channel = image.clone() |
||||||
|
alpha_channel.alpha_channel = 'extract' |
||||||
|
alpha_channel.blur(radius=5, sigma=3) |
||||||
|
image.composite_channel(channel='alpha', image=alpha_channel, operator='copy_alpha') |
||||||
|
image.blur(radius=5, sigma=3) |
||||||
|
|
||||||
|
outImage.composite(image,0,0) |
||||||
|
|
||||||
|
# step3, render the top images |
||||||
|
for img in images: |
||||||
|
with Image(filename=img.fileName) as image: |
||||||
|
image.background_color = Color('transparent') |
||||||
|
image.virtual_pixel = 'background' |
||||||
|
imw,imh = image.width, image.height |
||||||
|
|
||||||
|
if zfar-znear != 0: |
||||||
|
zl = 180 + 75*(img.data['zleft']-znear )/(zfar-znear) |
||||||
|
zr = 180 + 75*(img.data['zright']-znear)/(zfar-znear) |
||||||
|
else: |
||||||
|
zl=zr=255 |
||||||
|
|
||||||
|
# apply gradient transparency based on the distance |
||||||
|
with Image(width=imh, height=imw, pseudo=f'gradient:rgba({zl},{zl},{zl},1)-rgba({zr},{zr},{zr},1)') as gradient: |
||||||
|
gradient.rotate(-90) |
||||||
|
# image.composite(gradient) |
||||||
|
alphaImage = image.clone() |
||||||
|
alphaImage.alpha_channel = 'extract' |
||||||
|
alphaImage.composite(gradient,left=0,top=0,operator='multiply') |
||||||
|
alphaImage.alpha_channel = 'copy' |
||||||
|
image.composite_channel(channel='alpha', image=alphaImage, operator='copy_alpha') |
||||||
|
|
||||||
|
prw,prh = screen_width/2, screen_height/2 |
||||||
|
image.distort('perspective', [ |
||||||
|
# From: to: |
||||||
|
imw , 0 , *img.data['top']['p1'] , |
||||||
|
0 , 0 , *img.data['top']['p2'] , |
||||||
|
imw , imh , *img.data['top']['p3'] , |
||||||
|
0 , imh , *img.data['top']['p4'] |
||||||
|
]) |
||||||
|
|
||||||
|
image.alpha_channel = 'activate' |
||||||
|
for maskImg in reversed(images): |
||||||
|
if img==maskImg: |
||||||
|
break |
||||||
|
with Image(width=imw, height=imh, background=Color('transparent')) as transparent_img: |
||||||
|
with Drawing() as draw: |
||||||
|
draw.fill_color = Color('black') # Opaque |
||||||
|
draw.fill_opacity = 1.0 # Fully opaque |
||||||
|
draw.stroke_color = Color('black') |
||||||
|
draw.stroke_width = 0 |
||||||
|
points = [ |
||||||
|
maskImg.data['top']['p1'], |
||||||
|
maskImg.data['top']['p2'], |
||||||
|
maskImg.data['top']['p4'], |
||||||
|
maskImg.data['top']['p3'], |
||||||
|
] |
||||||
|
draw.polygon(points) |
||||||
|
draw(transparent_img) |
||||||
|
image.composite(transparent_img, left=0, top=0, operator='dst_out') |
||||||
|
|
||||||
|
outImage.composite(image,0,0) |
||||||
|
return outImage |
||||||
|
|
||||||
|
|
||||||
|
def getImagePil(self, data:RenderData) -> Image: |
||||||
|
ww,wh = self.size() |
||||||
|
observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position |
||||||
|
dz = 10*math.cos(math.pi + math.pi * data.cameraAngle/360) |
||||||
|
dy = 10*math.sin(math.pi + math.pi * data.cameraAngle/360) |
||||||
|
look_at = (ww/2 , dy-data.cameraY , wh-3+dz) # Observer is looking along the positive Z-axis |
||||||
|
screen_width, screen_height = data.resolution |
||||||
|
w,h = screen_width,screen_height |
||||||
|
prw,prh = screen_width/2, screen_height/2 |
||||||
|
|
||||||
|
# step1, sort Images based on the distance |
||||||
|
images = sorted(self._images,key=lambda img:img.getBox()[1]) |
||||||
|
znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF |
||||||
|
for img in images: |
||||||
|
ix,iy,iw,ih = img.getBox() |
||||||
|
ih-=1 |
||||||
|
znear=min(znear,iy,iy+ih) |
||||||
|
zfar=max(zfar,iy,iy+ih) |
||||||
|
isz = img.size*2 |
||||||
|
if math.pi/2 <= img.tilt < math.pi or -math.pi <= img.tilt < 0: |
||||||
|
zleft = iy |
||||||
|
zright = iy+ih |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy+ih )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy+ih )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , 0 , iy )) |
||||||
|
else: |
||||||
|
zleft = iy+ih |
||||||
|
zright = iy |
||||||
|
ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy )) |
||||||
|
ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy+ih )) |
||||||
|
ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy )) |
||||||
|
ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy+ih )) |
||||||
|
ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy )) |
||||||
|
ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy+ih )) |
||||||
|
ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0.1 , iy )) |
||||||
|
ip8x,ip8y = project_point(observer,look_at,(ix , 0.1 , iy+ih )) |
||||||
|
img.data = { |
||||||
|
'zleft':zleft, |
||||||
|
'zright':zright, |
||||||
|
'top' : { |
||||||
|
'p1':(int((ip1x+1)*prw) , int((ip1y+1)*prh)), |
||||||
|
'p2':(int((ip2x+1)*prw) , int((ip2y+1)*prh)), |
||||||
|
'p3':(int((ip3x+1)*prw) , int((ip3y+1)*prh)), |
||||||
|
'p4':(int((ip4x+1)*prw) , int((ip4y+1)*prh)), |
||||||
|
}, |
||||||
|
'bottom' : { |
||||||
|
'p1':(int((ip5x+1)*prw) , int((ip5y+1)*prh)), |
||||||
|
'p2':(int((ip6x+1)*prw) , int((ip6y+1)*prh)), |
||||||
|
'p3':(int((ip7x+1)*prw) , int((ip7y+1)*prh)), |
||||||
|
'p4':(int((ip8x+1)*prw) , int((ip8y+1)*prh)), |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
# Create a new image with a transparent background |
||||||
|
outImage = Image.new("RGBA", (w, h), data.bgColor) |
||||||
|
|
||||||
|
# step2, render the mirrored images |
||||||
|
for img in images: |
||||||
|
try: |
||||||
|
image = Image.open(img.fileName).convert("RGBA") |
||||||
|
except FileNotFoundError: |
||||||
|
print(f"Error: File not found: {img.fileName}") |
||||||
|
continue |
||||||
|
|
||||||
|
imw, imh = image.size |
||||||
|
|
||||||
|
# Create a gradient mask |
||||||
|
gradient = Image.new("L", (imw, imh), 0) |
||||||
|
draw = ImageDraw.Draw(gradient) |
||||||
|
for i in range(imh): |
||||||
|
alpha = int((i / imh) * 204) # alpha goes from 0 to 204 |
||||||
|
draw.rectangle((0, i, imw, i), fill=alpha) |
||||||
|
|
||||||
|
# Apply the gradient mask to the image |
||||||
|
image.putalpha(gradient) |
||||||
|
|
||||||
|
# Perspective distortion |
||||||
|
points_bottom = [ |
||||||
|
img.data['bottom']['p1'][0], img.data['bottom']['p1'][1], |
||||||
|
img.data['bottom']['p2'][0], img.data['bottom']['p2'][1], |
||||||
|
img.data['bottom']['p3'][0], img.data['bottom']['p3'][1], |
||||||
|
img.data['bottom']['p4'][0], img.data['bottom']['p4'][1] |
||||||
|
] |
||||||
|
image = image.transform((w, h), Image.PERSPECTIVE, points_bottom, Image.BILINEAR) |
||||||
|
|
||||||
|
# Apply blur to the alpha channel |
||||||
|
alpha = image.split()[-1] |
||||||
|
alpha = alpha.filter(ImageFilter.GaussianBlur(radius=5)) |
||||||
|
image.putalpha(alpha) |
||||||
|
image = image.filter(ImageFilter.GaussianBlur(radius=3)) |
||||||
|
|
||||||
|
# Paste the processed image onto the output image |
||||||
|
outImage.paste(image, (0, 0), image) |
||||||
|
return outImage |
||||||
|
# step3, render the top images |
||||||
|
for img in images: |
||||||
|
try: |
||||||
|
image = Image.open(img.fileName).convert("RGBA") |
||||||
|
except FileNotFoundError: |
||||||
|
print(f"Error: File not found: {img.fileName}") |
||||||
|
continue |
||||||
|
|
||||||
|
imw, imh = image.size |
||||||
|
|
||||||
|
if zfar-znear != 0: |
||||||
|
zl = 180 + 75*(img.data['zleft']-znear )/(zfar-znear) |
||||||
|
zr = 180 + 75*(img.data['zright']-znear)/(zfar-znear) |
||||||
|
else: |
||||||
|
zl=zr=255 |
||||||
|
|
||||||
|
# apply gradient transparency based on the distance |
||||||
|
gradient = Image.new("L", (imw, imh), 0) |
||||||
|
draw = ImageDraw.Draw(gradient) |
||||||
|
for i in range(imh): |
||||||
|
alpha = int(zl + (zr - zl) * (i / imh)) |
||||||
|
draw.rectangle((0, i, imw, i), fill=alpha) |
||||||
|
image.putalpha(gradient) |
||||||
|
|
||||||
|
# Perspective distortion |
||||||
|
points_top = [ |
||||||
|
img.data['top']['p1'][0], img.data['top']['p1'][1], |
||||||
|
img.data['top']['p2'][0], img.data['top']['p2'][1], |
||||||
|
img.data['top']['p3'][0], img.data['top']['p3'][1], |
||||||
|
img.data['top']['p4'][0], img.data['top']['p4'][1] |
||||||
|
] |
||||||
|
image = image.transform((w, h), Image.PERSPECTIVE, points_top, Image.BILINEAR) |
||||||
|
|
||||||
|
# Mask the image |
||||||
|
for maskImg in reversed(images): |
||||||
|
if img==maskImg: |
||||||
|
break |
||||||
|
mask = Image.new("L", (imw, imh), 0) |
||||||
|
draw = ImageDraw.Draw(mask) |
||||||
|
points = [ |
||||||
|
maskImg.data['top']['p1'], |
||||||
|
maskImg.data['top']['p2'], |
||||||
|
maskImg.data['top']['p4'], |
||||||
|
maskImg.data['top']['p3'], |
||||||
|
maskImg.data['bottom']['p1'], |
||||||
|
maskImg.data['bottom']['p2'], |
||||||
|
maskImg.data['bottom']['p4'], |
||||||
|
maskImg.data['bottom']['p3'], |
||||||
|
] |
||||||
|
draw.polygon(points, fill=255) |
||||||
|
image.putalpha(mask) |
||||||
|
|
||||||
|
# Paste the processed image onto the output image |
||||||
|
outImage.paste(image, (0, 0), image) |
||||||
|
|
||||||
|
return outImage |
||||||
|
|
||||||
|
def paintEvent(self, canvas): |
||||||
|
w,h = self.size() |
||||||
|
canvas.drawTTkString(pos=(w//2,h-3),text=ttk.TTkString("😎")) |
||||||
|
# Draw Fov |
||||||
|
for y in range(h-3): |
||||||
|
canvas.drawChar(char='/', pos=(w//2+(h-3)-y,y)) |
||||||
|
canvas.drawChar(char='\\',pos=(w//2-(h-3)+y,y)) |
||||||
|
for image in self._images: |
||||||
|
ix,iy,iw,ih = image.getBox() |
||||||
|
canvas.drawText(pos=(ix,iy-1),text=f"{image.tilt:.2f}", color=ttk.TTkColor.YELLOW) |
||||||
|
canvas.drawText(pos=(ix+5,iy-1),text=f"{image.fileName}", color=ttk.TTkColor.BLUE) |
||||||
|
if image == self._highlightedImage: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.GREEN) |
||||||
|
elif image == self._currentImage: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.YELLOW) |
||||||
|
else: |
||||||
|
canvas.fill(pos=(ix,iy),size=(iw,ih),char='+') |
||||||
|
if ih > iw > 1: |
||||||
|
for dy in range(ih): |
||||||
|
dx = iw*dy//ih |
||||||
|
if ( |
||||||
|
math.pi/2 < image.tilt < math.pi or |
||||||
|
-math.pi/2 < image.tilt < 0 ): |
||||||
|
canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) |
||||||
|
else: |
||||||
|
canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) |
||||||
|
elif iw >= ih > 1: |
||||||
|
for dx in range(iw): |
||||||
|
dy = ih*dx//iw |
||||||
|
if ( |
||||||
|
math.pi/2 < image.tilt < math.pi or |
||||||
|
-math.pi/2 < image.tilt < 0 ): |
||||||
|
canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) |
||||||
|
else: |
||||||
|
canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) |
||||||
|
|
||||||
|
class _Preview(ttk.TTkWidget): |
||||||
|
__slots__ = ('_canvasImage') |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self._canvasImage = ttk.TTkCanvas() |
||||||
|
super().__init__(**kwargs) |
||||||
|
|
||||||
|
def updateCanvas(self, img:Image): |
||||||
|
w,h = img.width, img.height |
||||||
|
self._canvasImage.resize(w,h//2) |
||||||
|
self._canvasImage.updateSize() |
||||||
|
for x in range(img.width): |
||||||
|
for y in range(img.height//2): |
||||||
|
bg = 100 if (x//4+y//2)%2 else 150 |
||||||
|
p1 = img[x,y*2] |
||||||
|
p2 = img[x,y*2+1] |
||||||
|
def _c2hex(p) -> str: |
||||||
|
a = p.alpha |
||||||
|
r = int(bg*(1-a)+255*a*p.red) |
||||||
|
g = int(bg*(1-a)+255*a*p.green) |
||||||
|
b = int(bg*(1-a)+255*a*p.blue) |
||||||
|
return f"#{r<<16|g<<8|b:06x}" |
||||||
|
c = ttk.TTkColor.fgbg(_c2hex(p2),_c2hex(p1)) |
||||||
|
self._canvasImage.drawChar(pos=(x,y), char='▄', color=c) |
||||||
|
self._canvasImage.drawText(pos=(0,1), text="Eugenio") |
||||||
|
|
||||||
|
self.update() |
||||||
|
|
||||||
|
def paintEvent(self, canvas): |
||||||
|
w,h = self.size() |
||||||
|
canvas.paintCanvas(self._canvasImage, (0,0,w,h), (0,0,w,h), (0,0,w,h)) |
||||||
|
canvas.drawText(pos=(0,0), text="Eugenio") |
||||||
|
|
||||||
|
class ControlPanel(ttk.TTkContainer): |
||||||
|
__slots__ = ( |
||||||
|
'previewPressed','renderPressed','_toolbox', '_previewImage') |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self.previewPressed = ttk.pyTTkSignal(RenderData) |
||||||
|
self.renderPressed = ttk.pyTTkSignal(RenderData) |
||||||
|
super().__init__(**kwargs|{'layout':ttk.TTkGridLayout()}) |
||||||
|
self._previewImage = _Preview(minHeight=20,maxHeight=20) |
||||||
|
|
||||||
|
self._toolbox = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"toolbox.tui.json")) |
||||||
|
self._toolbox.getWidgetByName("Btn_Render").clicked.connect(self._renderClicked) |
||||||
|
self._toolbox.getWidgetByName("Btn_Preview").clicked.connect(self._previewClicked) |
||||||
|
# self._toolbox.getWidgetByName("SB_CamY") |
||||||
|
# self._toolbox.getWidgetByName("SB_CamA") |
||||||
|
# self._toolbox.getWidgetByName("CB_Bg") |
||||||
|
# self._toolbox.getWidgetByName("BG_Color") |
||||||
|
self.layout().addWidget(self._previewImage,0,0) |
||||||
|
self.layout().addWidget(self._toolbox,1,0) |
||||||
|
self.layout().addItem(ttk.TTkLayout(),2,0) |
||||||
|
|
||||||
|
def drawPreview(self, img:Image): |
||||||
|
self._previewImage.updateCanvas(img) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _renderClicked(self): |
||||||
|
if self._toolbox.getWidgetByName("CB_Bg").isChecked(): |
||||||
|
btnColor = self._toolbox.getWidgetByName("BG_Color").color() |
||||||
|
color = (*btnColor.fgToRGB(),1) |
||||||
|
else: |
||||||
|
color = (0,0,0,0) |
||||||
|
data = RenderData( |
||||||
|
cameraAngle=self._toolbox.getWidgetByName("SB_CamA").value(), |
||||||
|
cameraY=self._toolbox.getWidgetByName("SB_CamY").value(), |
||||||
|
bgColor=color, |
||||||
|
resolution=(800,600) |
||||||
|
) |
||||||
|
self.renderPressed.emit(data) |
||||||
|
|
||||||
|
@ttk.pyTTkSlot() |
||||||
|
def _previewClicked(self): |
||||||
|
w,h = self.size() |
||||||
|
if self._toolbox.getWidgetByName("CB_Bg").isChecked(): |
||||||
|
btnColor = self._toolbox.getWidgetByName("BG_Color").color() |
||||||
|
color = (*btnColor.fgToRGB(),1) |
||||||
|
else: |
||||||
|
color = (0,0,0,0) |
||||||
|
data = RenderData( |
||||||
|
cameraAngle=self._toolbox.getWidgetByName("SB_CamA").value(), |
||||||
|
cameraY=self._toolbox.getWidgetByName("SB_CamY").value(), |
||||||
|
bgColor=color, |
||||||
|
resolution=(w,40) |
||||||
|
) |
||||||
|
self.previewPressed.emit(data) |
||||||
|
|
||||||
|
def main(): |
||||||
|
parser = argparse.ArgumentParser() |
||||||
|
parser.add_argument('filename', type=str, nargs='*', |
||||||
|
help='the filename/s') |
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) |
||||||
|
|
||||||
|
root = ttk.TTk( |
||||||
|
layout=ttk.TTkGridLayout(), |
||||||
|
title="TTkode", |
||||||
|
mouseTrack=True) |
||||||
|
|
||||||
|
perspectivator = Perspectivator() |
||||||
|
controlPanel = ControlPanel() |
||||||
|
|
||||||
|
at = ttk.TTkAppTemplate() |
||||||
|
at.setWidget(widget=perspectivator,position=at.MAIN) |
||||||
|
at.setWidget(widget=controlPanel,position=at.RIGHT, size=30) |
||||||
|
|
||||||
|
for file in args.filename: |
||||||
|
perspectivator.addImage(file) |
||||||
|
|
||||||
|
root.layout().addWidget(at) |
||||||
|
|
||||||
|
def _preview(data): |
||||||
|
img = perspectivator.getImage(data) |
||||||
|
controlPanel.drawPreview(img) |
||||||
|
|
||||||
|
controlPanel.renderPressed.connect(perspectivator.render) |
||||||
|
controlPanel.previewPressed.connect(_preview) |
||||||
|
|
||||||
|
root.mainloop() |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
||||||
@ -0,0 +1,346 @@ |
|||||||
|
{ |
||||||
|
"type": "TTkUi/Document", |
||||||
|
"version": "2.1.0", |
||||||
|
"tui": { |
||||||
|
"class": "TTkContainer", |
||||||
|
"params": { |
||||||
|
"Name": "MainWindow", |
||||||
|
"Position": [ |
||||||
|
4, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
80, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Min Width": 0, |
||||||
|
"Min Height": 0, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout" |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
80, |
||||||
|
4 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [ |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "z:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 2, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-1", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "x:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-2", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "y:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 1, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-3", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "tilt:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 3, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_X", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Y", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 1, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Z", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1, |
||||||
|
"Minimum": 0, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 2, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Tilt", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 0, |
||||||
|
"Minimum": -179, |
||||||
|
"Maximum": 180 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 3, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"connections": [] |
||||||
|
} |
||||||
@ -0,0 +1,421 @@ |
|||||||
|
{ |
||||||
|
"type": "TTkUi/Document", |
||||||
|
"version": "2.1.0", |
||||||
|
"tui": { |
||||||
|
"class": "TTkContainer", |
||||||
|
"params": { |
||||||
|
"Name": "MainWindow", |
||||||
|
"Position": [ |
||||||
|
4, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
80, |
||||||
|
5 |
||||||
|
], |
||||||
|
"Min Width": 0, |
||||||
|
"Min Height": 0, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout" |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
80, |
||||||
|
5 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [ |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "z:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 2, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-1", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "x:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-2", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "y:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 1, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-3", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "tilt:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 3, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_X", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Y", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 1, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Z", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 1000, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 2, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Tilt", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 10, |
||||||
|
"Minimum": -10, |
||||||
|
"Maximum": 10 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 3, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Size", |
||||||
|
"Position": [ |
||||||
|
40, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 10, |
||||||
|
"Minimum": -10, |
||||||
|
"Maximum": 10 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
38, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 4, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-4", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
40, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Size:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 4, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"connections": [] |
||||||
|
} |
||||||
@ -0,0 +1,921 @@ |
|||||||
|
{ |
||||||
|
"type": "TTkUi/Document", |
||||||
|
"version": "2.1.0", |
||||||
|
"tui": { |
||||||
|
"class": "TTkContainer", |
||||||
|
"params": { |
||||||
|
"Name": "MainWindow", |
||||||
|
"Position": [ |
||||||
|
5, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
70, |
||||||
|
16 |
||||||
|
], |
||||||
|
"Min Width": 0, |
||||||
|
"Min Height": 16, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 16, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout" |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
70, |
||||||
|
16 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [ |
||||||
|
{ |
||||||
|
"class": "TTkButton", |
||||||
|
"params": { |
||||||
|
"Name": "Btn_Preview", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
35, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Min Width": 9, |
||||||
|
"Min Height": 3, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 3, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Preview", |
||||||
|
"Border": true, |
||||||
|
"Checkable": true, |
||||||
|
"Checked": false |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkButton", |
||||||
|
"params": { |
||||||
|
"Name": "Btn_Render", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
70, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Min Width": 8, |
||||||
|
"Min Height": 3, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 3, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Render", |
||||||
|
"Border": true, |
||||||
|
"Checkable": false, |
||||||
|
"Checked": false |
||||||
|
}, |
||||||
|
"row": 1, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkButton", |
||||||
|
"params": { |
||||||
|
"Name": "Btn_SaveCfg", |
||||||
|
"Position": [ |
||||||
|
35, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
35, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Min Width": 12, |
||||||
|
"Min Height": 3, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Save (Cfg)", |
||||||
|
"Border": true, |
||||||
|
"Checkable": false, |
||||||
|
"Checked": false |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 1, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
6, |
||||||
|
70, |
||||||
|
10 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [ |
||||||
|
{ |
||||||
|
"class": "TTkCheckbox", |
||||||
|
"params": { |
||||||
|
"Name": "CB_Show", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
15, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 7, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Show", |
||||||
|
"Tristate": false, |
||||||
|
"Checked": false, |
||||||
|
"Check State": 0 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel", |
||||||
|
"Position": [ |
||||||
|
9, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
9, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "File:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLineEdit", |
||||||
|
"params": { |
||||||
|
"Name": "LE_OutFile", |
||||||
|
"Position": [ |
||||||
|
16, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
22, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 1, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Input Type": 1, |
||||||
|
"Echo Mode": 0, |
||||||
|
"Text": "outImage.png" |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkCheckbox", |
||||||
|
"params": { |
||||||
|
"Name": "CB_Bg", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
15, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 13, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 15, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Background", |
||||||
|
"Tristate": false, |
||||||
|
"Checked": false, |
||||||
|
"Check State": 0 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkColorButtonPicker", |
||||||
|
"params": { |
||||||
|
"Name": "BG_Color", |
||||||
|
"Position": [ |
||||||
|
14, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
24, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 2, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": false, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "", |
||||||
|
"Border": false, |
||||||
|
"Checkable": false, |
||||||
|
"Checked": false, |
||||||
|
"Color": "\u001b[38;2;0;0;0m", |
||||||
|
"Return Type": 0 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-2", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
11, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 10, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Fog (Near)", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-3", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
11, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 9, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Fog (Far)", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkComboBox", |
||||||
|
"params": { |
||||||
|
"Name": "CB_ResPresets", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
5 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
20, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Layout": "TTkLayout", |
||||||
|
"Editable": false, |
||||||
|
"Text Align.": 4, |
||||||
|
"Insert Policy": 3 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
20, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Fog_Far", |
||||||
|
"Position": [ |
||||||
|
11, |
||||||
|
3 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
14, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 128, |
||||||
|
"Minimum": 0, |
||||||
|
"Maximum": 255 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
12, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_Fog_Near", |
||||||
|
"Position": [ |
||||||
|
11, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
14, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 0, |
||||||
|
"Minimum": 0, |
||||||
|
"Maximum": 255 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
12, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_HRes", |
||||||
|
"Position": [ |
||||||
|
1, |
||||||
|
6 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
8, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 800, |
||||||
|
"Minimum": 0, |
||||||
|
"Maximum": 5000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
6, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_VRes", |
||||||
|
"Position": [ |
||||||
|
10, |
||||||
|
6 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
8, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 600, |
||||||
|
"Minimum": 0, |
||||||
|
"Maximum": 5000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
6, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-1", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
11, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 8, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Offset-Y", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_OffY", |
||||||
|
"Position": [ |
||||||
|
11, |
||||||
|
4 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
14, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 0, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
12, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-4", |
||||||
|
"Position": [ |
||||||
|
19, |
||||||
|
6 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
3, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 3, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "AA:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkComboBox", |
||||||
|
"params": { |
||||||
|
"Name": "CB_AA", |
||||||
|
"Position": [ |
||||||
|
23, |
||||||
|
6 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
6, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 5, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 1, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
0 |
||||||
|
], |
||||||
|
"Layout": "TTkLayout", |
||||||
|
"Editable": false, |
||||||
|
"Text Align.": 4, |
||||||
|
"Insert Policy": 3 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
6, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-5", |
||||||
|
"Position": [ |
||||||
|
2, |
||||||
|
7 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
14, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 14, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "Mirror Fading:", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-6", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
8 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
6, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 6, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "From-Y", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkLabel", |
||||||
|
"params": { |
||||||
|
"Name": "TTkLabel-7", |
||||||
|
"Position": [ |
||||||
|
0, |
||||||
|
9 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
4, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Text": "To-Y", |
||||||
|
"Color": "\u001b[0m", |
||||||
|
"Alignment": 1 |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_FadingTo", |
||||||
|
"Position": [ |
||||||
|
9, |
||||||
|
9 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
10, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": 10, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 1000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
8, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"class": "TTkSpinBox", |
||||||
|
"params": { |
||||||
|
"Name": "SB_FadingFrom", |
||||||
|
"Position": [ |
||||||
|
9, |
||||||
|
8 |
||||||
|
], |
||||||
|
"Size": [ |
||||||
|
10, |
||||||
|
1 |
||||||
|
], |
||||||
|
"Min Width": 4, |
||||||
|
"Min Height": 1, |
||||||
|
"Max Width": 65536, |
||||||
|
"Max Height": 65536, |
||||||
|
"Visible": true, |
||||||
|
"Enabled": true, |
||||||
|
"ToolTip": "", |
||||||
|
"Padding": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
0, |
||||||
|
2 |
||||||
|
], |
||||||
|
"Layout": "TTkGridLayout", |
||||||
|
"Value": -3, |
||||||
|
"Minimum": -1000, |
||||||
|
"Maximum": 10000 |
||||||
|
}, |
||||||
|
"layout": { |
||||||
|
"class": "TTkGridLayout", |
||||||
|
"params": { |
||||||
|
"Geometry": [ |
||||||
|
0, |
||||||
|
0, |
||||||
|
8, |
||||||
|
1 |
||||||
|
] |
||||||
|
}, |
||||||
|
"children": [] |
||||||
|
}, |
||||||
|
"row": 0, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 1, |
||||||
|
"colspan": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"row": 2, |
||||||
|
"col": 0, |
||||||
|
"rowspan": 3, |
||||||
|
"colspan": 2 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"connections": [ |
||||||
|
{ |
||||||
|
"sender": "CB_Bg", |
||||||
|
"receiver": "BG_Color", |
||||||
|
"signal": "toggled(bool)", |
||||||
|
"slot": "setEnabled(bool)" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
Loading…
Reference in new issue