Files
Nexus/2023/scripts/animation_tools/dwpicker/shape.py
2025-11-23 23:31:18 +08:00

248 lines
8.6 KiB
Python

from copy import deepcopy
from .pyside import QtCore, QtGui
from .geometry import proportional_rect
from .languages import execute_code, EXECUTION_WARNING
from .path import expand_path
from .selection import select_targets
from .shapepath import (
get_shape_painter_path, get_screenspace_qpath, get_absolute_path,
get_default_path, get_worldspace_qpath)
from .templates import BUTTON
from .viewport import to_screenspace_coords
def build_multiple_shapes(targets, override):
shapes = [deepcopy(BUTTON) for _ in range(len(targets))]
for shape, target in zip(shapes, targets):
if override:
shape.update(override)
shape['action.targets'] = [target]
return [Shape(shape) for shape in shapes]
def rect_intersects_shape(
shape, unit_rect, viewport_rect=None,
force_world_space=True,
viewportmapper=None):
if force_world_space or shape.options['shape.space'] == 'world':
if shape.path and shape.options['shape'] == 'custom':
return shape.path.intersects(unit_rect)
return shape.rect.intersects(unit_rect)
if shape.path and shape.options['shape'] == 'custom':
path = get_screenspace_qpath(
path=shape.options['shape.path'],
point=(shape.options['shape.left'], shape.options['shape.top']),
anchor=shape.options['shape.anchor'],
viewport_size=viewportmapper.viewsize)
return path.intersects(viewport_rect)
rect = to_shape_space_rect(
rect=shape.rect,
shape=shape,
force_world_space=False,
viewportmapper=viewportmapper)
return rect.intersects(viewport_rect)
def to_shape_space(value, shape, force_world_space, viewportmapper):
if shape.options['shape.space'] == 'world' or force_world_space:
return viewportmapper.to_viewport(value)
return value
def to_shape_space_rect(rect, shape, force_world_space, viewportmapper):
if shape.options['shape.space'] == 'world' or force_world_space:
return viewportmapper.to_viewport_rect(rect)
rect = QtCore.QRectF(rect)
point = to_screenspace_coords(
rect.topLeft(), shape.options['shape.anchor'], viewportmapper.viewsize)
rect.moveTopLeft(point)
return rect
def cursor_in_shape(
shape,
world_cursor,
viewpoert_cursor=None,
force_world_space=True,
viewportmapper=None):
if force_world_space or shape.options['shape.space'] == 'world':
if shape.path and shape.options['shape'] == 'custom':
return shape.path.contains(world_cursor)
return shape.rect.contains(world_cursor)
if shape.path and shape.options['shape'] == 'custom':
path = get_screenspace_qpath(
path=shape.options['shape.path'],
point=(shape.options['shape.left'], shape.options['shape.top']),
anchor=shape.options['shape.anchor'],
viewport_size=viewportmapper.viewsize)
return path.contains(viewpoert_cursor)
rect = to_shape_space_rect(
rect=shape.rect,
shape=shape,
force_world_space=False,
viewportmapper=viewportmapper)
return rect.contains(viewpoert_cursor)
def get_shape_rect_from_options(options):
return QtCore.QRectF(
options['shape.left'],
options['shape.top'],
options['shape.width'],
options['shape.height'])
class Shape():
def __init__(self, options):
# This is necessary for temprary Shape object used in multiple shapes
# creation.
if 'children' not in options:
options['children'] = []
self.hovered = False
self.clicked = False
self.selected = False
self.options = options
self.rect = get_shape_rect_from_options(options)
self.pixmap = None
self.image_rect = None
self.path = get_shape_painter_path(self)
self.synchronize_image()
self._buffer_path = None
def set_clicked(self, cursor):
self.clicked = self.rect.contains(cursor)
def release(self, cursor):
self.clicked = False
self.hovered = self.rect.contains(cursor)
def update_path(self):
if self.options['shape'] == 'custom' and not self.options['shape.path']:
self.options['shape.path'] = get_default_path(self.options)
self.path = get_shape_painter_path(self)
self._buffer_path = None
def get_painter_path(self, force_world_space, viewportmapper=None):
if self.options['shape.space'] == 'world' or force_world_space:
if self._buffer_path is None:
left, top = self.options['shape.left'], self.options['shape.top']
path = self.options['shape.path'] or get_default_path(self.options)
self._buffer_path = get_worldspace_qpath(get_absolute_path((left, top), path))
return viewportmapper.to_viewport_path(self._buffer_path)
return get_screenspace_qpath(
path=self.options['shape.path'],
point=(self.options['shape.left'], self.options['shape.top']),
anchor=self.options['shape.anchor'],
viewport_size=viewportmapper.viewsize)
def synchronize_rect(self):
self.options['shape.left'] = self.rect.left()
self.options['shape.top'] = self.rect.top()
self.options['shape.width'] = self.rect.width()
self.options['shape.height'] = self.rect.height()
def bounding_rect(self):
if self.options['shape'] == 'custom':
return self.path.boundingRect()
return self.rect
def content_rect(self):
rect = (
self.rect if self.options['shape'] != 'custom' else
self.path.boundingRect())
if self.options['shape'] == 'round':
return proportional_rect(rect, 70)
return rect
def execute(self, command=None, button=None, shift=False, ctrl=False):
if command is not None:
commands = [command]
else:
commands = _find_commands(
self.options['action.commands'],
button, shift=shift, ctrl=ctrl)
for command in commands:
try:
execute_code(
language=command['language'],
code=command['command'],
shape=self,
deferred=command['deferred'],
compact_undo=command['force_compact_undo'])
except Exception as e:
import traceback
print(EXECUTION_WARNING.format(
object='shape',
name=self.options['text.content'],
error=e))
print(traceback.format_exc())
def select(self, selection_mode='replace'):
select_targets([self], selection_mode=selection_mode)
def targets(self):
return self.options['action.targets']
def set_targets(self, targets):
self.options['action.targets'] = targets
def is_interactive(self):
return bool(
[cmd for cmd in self.options['action.commands'] if cmd['enabled']])
def has_right_click_command(self):
return bool([
cmd for cmd in self.options['action.commands']
if cmd['enabled'] and cmd['button'] == 'right'])
def is_background(self):
return self.options['background']
def visibility_layer(self):
return self.options['visibility_layer']
def synchronize_image(self):
path = expand_path(self.options['image.path'])
self.pixmap = QtGui.QPixmap(path)
if self.options['image.fit'] and not self.options['image.ratio']:
self.image_rect = None
return
if not self.options['image.fit']:
self.image_rect = QtCore.QRectF(
self.rect.left(),
self.rect.top(),
self.options['image.width'],
self.options['image.height'])
self.image_rect.moveCenter(self.bounding_rect().center())
return
rect = self.bounding_rect()
ratio = self.options['image.width'] / self.options['image.height']
width = rect.width()
height = rect.width() / ratio
if rect.height() < height:
width = rect.height() * ratio
height = rect.height()
self.image_rect = QtCore.QRectF(rect.left(), rect.top(), width, height)
self.image_rect.moveCenter(rect.center())
def _find_commands(commands, button, ctrl=False, shift=False):
result = []
for command in commands:
conditions = (
command['button'] == button and
command['ctrl'] == ctrl and
command['shift'] == shift)
if conditions:
result.append(command)
return result