work
save startup blend for animation tab & whatnot
This commit is contained in:
@@ -18,7 +18,12 @@
|
||||
|
||||
import os
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional, Tuple, Union
|
||||
import math
|
||||
|
||||
from mathutils import Matrix
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
@@ -29,7 +34,6 @@ from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .image_utils import IMG
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
cached_images = {}
|
||||
@@ -38,6 +42,83 @@ cached_gpu_textures = {}
|
||||
|
||||
_cached_image_shader: Optional[gpu.types.GPUShader] = None
|
||||
|
||||
SEGMENTS_DEFAULT = 4
|
||||
|
||||
|
||||
def _resolve_region_dimensions(region) -> Tuple[float, float]:
|
||||
width = getattr(region, "width", None) if region else None
|
||||
height = getattr(region, "height", None) if region else None
|
||||
|
||||
if width is None or width <= 0 or height is None or height <= 0:
|
||||
ctx_region = getattr(bpy.context, "region", None)
|
||||
if ctx_region is not None:
|
||||
width = width or getattr(ctx_region, "width", None)
|
||||
height = height or getattr(ctx_region, "height", None)
|
||||
|
||||
width = float(width or 1.0)
|
||||
height = float(height or 1.0)
|
||||
return width, height
|
||||
|
||||
|
||||
@contextmanager
|
||||
def overlay_matrix_guard(region=None, *args, **kwargs):
|
||||
"""Ensure viewport overlays draw in screen space regardless of other handlers."""
|
||||
|
||||
pushed = False
|
||||
try:
|
||||
try:
|
||||
gpu.matrix.push()
|
||||
pushed = True
|
||||
gpu.matrix.load_identity()
|
||||
width, height = _resolve_region_dimensions(region)
|
||||
_set_overlay_projection(width, height)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to prepare overlay matrix state")
|
||||
yield
|
||||
finally:
|
||||
if pushed:
|
||||
try:
|
||||
gpu.matrix.pop()
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to restore overlay matrix state")
|
||||
|
||||
|
||||
def _ortho_projection_matrix(width: float, height: float, *, near=-100.0, far=100.0):
|
||||
"""Return a pixel-aligned orthographic projection matrix."""
|
||||
|
||||
if width <= 0.0:
|
||||
width = 1.0
|
||||
if height <= 0.0:
|
||||
height = 1.0
|
||||
if far == near:
|
||||
far = near + 0.001
|
||||
|
||||
sx = 2.0 / width
|
||||
sy = 2.0 / height
|
||||
sz = -2.0 / (far - near)
|
||||
tx = -1.0
|
||||
ty = -1.0
|
||||
tz = -(far + near) / (far - near)
|
||||
|
||||
return Matrix(
|
||||
(
|
||||
(sx, 0.0, 0.0, tx),
|
||||
(0.0, sy, 0.0, ty),
|
||||
(0.0, 0.0, sz, tz),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _set_overlay_projection(width: float, height: float):
|
||||
"""Set the 2D projection matrix used by BlenderKit overlays."""
|
||||
|
||||
try:
|
||||
projection = _ortho_projection_matrix(width, height)
|
||||
gpu.matrix.load_projection_matrix(projection)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("overlay_matrix_guard: Failed to load projection matrix")
|
||||
|
||||
|
||||
VERTEX_SHADER_LEGACY = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
@@ -169,6 +250,34 @@ def create_image_shader():
|
||||
return shader
|
||||
|
||||
|
||||
def get_ui_scale() -> float:
|
||||
"""Get the UI scale
|
||||
|
||||
Returns:
|
||||
The UI scale factor as a float.
|
||||
"""
|
||||
return bpy.context.preferences.system.dpi / 96.0
|
||||
# sources:
|
||||
# "sys" values are from system,
|
||||
# and for current monitor on which the blender is running
|
||||
sys_ps = bpy.context.preferences.system.pixel_size
|
||||
# dpi scaling 72 =-> 1.0, 126 -> 1.75 (dpi -> sys_ui_scale)
|
||||
# 96 / dpi gives correct scale factor
|
||||
sys_dpi = bpy.context.preferences.system.dpi
|
||||
# global scaler
|
||||
sys_sc = bpy.context.preferences.system.ui_scale
|
||||
# local ui scaler (can be set by user per blender window)
|
||||
view_sc = bpy.context.preferences.view.ui_scale
|
||||
|
||||
|
||||
def _get_flat_shader_2d():
|
||||
if app.version < (4, 0, 0):
|
||||
shader_name = "2D_UNIFORM_COLOR"
|
||||
else:
|
||||
shader_name = "UNIFORM_COLOR"
|
||||
return gpu.shader.from_builtin(shader_name)
|
||||
|
||||
|
||||
def draw_rect(x, y, width, height, color):
|
||||
"""Used for drawing 2D rectangle backgrounds."""
|
||||
xmax = x + width
|
||||
@@ -181,10 +290,7 @@ def draw_rect(x, y, width, height, color):
|
||||
)
|
||||
indices = ((0, 1, 2), (2, 3, 0))
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
shader = _get_flat_shader_2d()
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": points}, indices=indices)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
@@ -205,10 +311,7 @@ def draw_rect_outline(x, y, width, height, color, line_width=1.0):
|
||||
)
|
||||
indices = ((0, 1), (1, 2), (2, 3), (3, 0))
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
shader = _get_flat_shader_2d()
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": coords}, indices=indices)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
@@ -223,19 +326,286 @@ def draw_line2d(x1, y1, x2, y2, width, color):
|
||||
coords = ((x1, y1), (x2, y2))
|
||||
indices = ((0, 1),)
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
elif app.version < (4, 5, 0):
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
else:
|
||||
shader_info = create_shader_info()
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
shader = _get_flat_shader_2d()
|
||||
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": coords}, indices=indices)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(max(1.0, width))
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
gpu.state.line_width_set(1.0)
|
||||
|
||||
|
||||
def _parse_radius_value(
|
||||
value,
|
||||
*,
|
||||
max_radius: float,
|
||||
min_dimension: float,
|
||||
ui_scale: float = 1.0,
|
||||
) -> float:
|
||||
"""Return a clamped radius in pixels.
|
||||
|
||||
Accepts raw pixel values, strings with percentages (e.g. "50%"),
|
||||
mapping types containing ``percent``/``pct``/``ratio`` or ``px`` keys,
|
||||
and falls back to treating anything else as raw pixels.
|
||||
"""
|
||||
|
||||
def clamp_radius(radius: float) -> float:
|
||||
return max(0.0, min(radius, max_radius))
|
||||
|
||||
effective_scale = ui_scale if ui_scale > 0.0 else 1.0
|
||||
|
||||
def scale_px_value(px_value: float) -> float:
|
||||
return clamp_radius(px_value * effective_scale)
|
||||
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
if text.endswith("%"):
|
||||
number = text[:-1].strip()
|
||||
try:
|
||||
pct = float(number) / 100.0
|
||||
except ValueError:
|
||||
return 0.0
|
||||
radius_px = pct * min_dimension
|
||||
return clamp_radius(radius_px)
|
||||
# plain numeric string interpreted as pixels
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
return scale_px_value(value)
|
||||
|
||||
if isinstance(value, Mapping):
|
||||
if "percent" in value:
|
||||
try:
|
||||
pct = float(value["percent"]) / 100.0
|
||||
except (TypeError, ValueError):
|
||||
pct = 0.0
|
||||
radius_px = pct * min_dimension
|
||||
return clamp_radius(radius_px)
|
||||
if "pct" in value:
|
||||
try:
|
||||
pct = float(value["pct"]) / 100.0
|
||||
except (TypeError, ValueError):
|
||||
pct = 0.0
|
||||
radius_px = pct * min_dimension
|
||||
return clamp_radius(radius_px)
|
||||
if "ratio" in value:
|
||||
try:
|
||||
ratio = float(value["ratio"])
|
||||
except (TypeError, ValueError):
|
||||
ratio = 0.0
|
||||
radius_px = ratio * min_dimension
|
||||
return clamp_radius(radius_px)
|
||||
if "px" in value:
|
||||
try:
|
||||
px_value = float(value["px"])
|
||||
except (TypeError, ValueError):
|
||||
px_value = 0.0
|
||||
return scale_px_value(px_value)
|
||||
|
||||
try:
|
||||
numeric_value = float(value) # type: ignore
|
||||
except (TypeError, ValueError):
|
||||
numeric_value = 0.0
|
||||
return scale_px_value(numeric_value)
|
||||
|
||||
|
||||
def _rounded_rect_outline(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
radius: Union[tuple[Union[str, float], ...], str, float] = (0.0,),
|
||||
segments: int = SEGMENTS_DEFAULT,
|
||||
):
|
||||
if width <= 0 or height <= 0:
|
||||
return []
|
||||
min_dimension = min(width, height)
|
||||
max_radius = max(0.0, min_dimension / 2.0)
|
||||
|
||||
if isinstance(radius, (tuple, list)):
|
||||
raw_radii = list(radius)
|
||||
else:
|
||||
raw_radii = [radius]
|
||||
if not raw_radii:
|
||||
raw_radii = [0.0]
|
||||
ui_scale = get_ui_scale()
|
||||
parsed_radii = [
|
||||
_parse_radius_value(
|
||||
value,
|
||||
max_radius=max_radius,
|
||||
min_dimension=min_dimension,
|
||||
ui_scale=ui_scale,
|
||||
)
|
||||
for value in raw_radii
|
||||
]
|
||||
while len(parsed_radii) < 4:
|
||||
parsed_radii.append(parsed_radii[-1])
|
||||
radii = parsed_radii[:4]
|
||||
|
||||
r_tl, r_tr, r_br, r_bl = radii
|
||||
|
||||
if all(r == 0.0 for r in radii):
|
||||
outline = [
|
||||
(x, y),
|
||||
(x + width, y),
|
||||
(x + width, y + height),
|
||||
(x, y + height),
|
||||
]
|
||||
outline.append(outline[0])
|
||||
return outline
|
||||
|
||||
steps = max(1, int(segments))
|
||||
outline = []
|
||||
steps = max(1, int(segments))
|
||||
outline = []
|
||||
|
||||
def emit_corner(cx, cy, start_angle, end_angle, radius_value, fallback_point):
|
||||
if radius_value <= 0.0:
|
||||
outline.append(fallback_point)
|
||||
return
|
||||
for step in range(steps + 1):
|
||||
t = step / steps
|
||||
angle = start_angle + (end_angle - start_angle) * t
|
||||
outline.append(
|
||||
(
|
||||
cx + math.cos(angle) * radius_value,
|
||||
cy + math.sin(angle) * radius_value,
|
||||
)
|
||||
)
|
||||
|
||||
emit_corner(
|
||||
x + r_tl,
|
||||
y + height - r_tl,
|
||||
math.pi,
|
||||
math.pi / 2.0,
|
||||
r_tl,
|
||||
(x, y + height),
|
||||
)
|
||||
emit_corner(
|
||||
x + width - r_tr,
|
||||
y + height - r_tr,
|
||||
math.pi / 2.0,
|
||||
0.0,
|
||||
r_tr,
|
||||
(x + width, y + height),
|
||||
)
|
||||
emit_corner(
|
||||
x + width - r_br,
|
||||
y + r_br,
|
||||
0.0,
|
||||
-math.pi / 2.0,
|
||||
r_br,
|
||||
(x + width, y),
|
||||
)
|
||||
emit_corner(
|
||||
x + r_bl,
|
||||
y + r_bl,
|
||||
-math.pi / 2.0,
|
||||
-math.pi,
|
||||
r_bl,
|
||||
(x, y),
|
||||
)
|
||||
|
||||
if outline and outline[0] != outline[-1]:
|
||||
outline.append(outline[0])
|
||||
return outline
|
||||
|
||||
|
||||
def _rounded_rect_mesh(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
radius: Union[tuple[Union[str, float], ...], str, float],
|
||||
crop: Tuple[float, float, float, float],
|
||||
segments: int = SEGMENTS_DEFAULT,
|
||||
):
|
||||
if width <= 0.0 or height <= 0.0:
|
||||
return None
|
||||
outline = _rounded_rect_outline(
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
radius,
|
||||
segments=segments,
|
||||
)
|
||||
if not outline:
|
||||
return None
|
||||
loop = outline[:-1] if len(outline) > 1 and outline[0] == outline[-1] else outline
|
||||
if len(loop) < 3:
|
||||
return None
|
||||
crop_u0, crop_v0, crop_u1, crop_v1 = crop
|
||||
u_span = crop_u1 - crop_u0
|
||||
v_span = crop_v1 - crop_v0
|
||||
if u_span == 0.0:
|
||||
u_span = 1.0
|
||||
if v_span == 0.0:
|
||||
v_span = 1.0
|
||||
coords = list(loop)
|
||||
try:
|
||||
inv_width = 1.0 / width
|
||||
except ZeroDivisionError:
|
||||
inv_width = 0.0
|
||||
try:
|
||||
inv_height = 1.0 / height
|
||||
except ZeroDivisionError:
|
||||
inv_height = 0.0
|
||||
uvs = []
|
||||
for vx, vy in coords:
|
||||
rel_x = (vx - x) * inv_width
|
||||
rel_y = (vy - y) * inv_height
|
||||
u = crop_u0 + rel_x * u_span
|
||||
v = crop_v0 + rel_y * v_span
|
||||
uvs.append((u, v))
|
||||
indices = [(0, idx, idx + 1) for idx in range(1, len(coords) - 1)]
|
||||
return coords, uvs, indices
|
||||
|
||||
|
||||
def draw_rounded_rect_with_border(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
radius: Union[tuple[Union[str, float], ...], str, float] = (0.0,),
|
||||
fill_color: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
|
||||
border_color: Optional[Tuple[float, float, float, float]] = None,
|
||||
border_thickness: float = 1.0,
|
||||
):
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
outline = _rounded_rect_outline(x, y, width, height, radius)
|
||||
if not outline:
|
||||
return
|
||||
loop = outline[:-1] if len(outline) > 1 and outline[0] == outline[-1] else outline
|
||||
if len(loop) < 3:
|
||||
return
|
||||
shader = _get_flat_shader_2d()
|
||||
indices = [(0, idx, idx + 1) for idx in range(1, len(loop) - 1)]
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": loop}, indices=indices)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
shader.bind()
|
||||
shader.uniform_float("color", fill_color)
|
||||
batch.draw(shader)
|
||||
if border_color and border_thickness > 0:
|
||||
gpu.state.line_width_set(border_thickness)
|
||||
if outline[0] == outline[-1]:
|
||||
line_points = outline
|
||||
else:
|
||||
line_points = outline + [outline[0]]
|
||||
line_batch = batch_for_shader(shader, "LINE_STRIP", {"pos": line_points})
|
||||
shader.uniform_float("color", border_color)
|
||||
line_batch.draw(shader)
|
||||
gpu.state.line_width_set(1.0)
|
||||
|
||||
|
||||
def draw_strikethrough_line(x_start, x_end, y, color, thickness):
|
||||
if x_end <= x_start:
|
||||
return
|
||||
draw_line2d(x_start, y, x_end, y, thickness, color)
|
||||
|
||||
|
||||
def create_shader_info():
|
||||
@@ -325,80 +695,101 @@ def draw_image_runtime(
|
||||
transparency: Optional[float] = 1.0,
|
||||
crop: Tuple[float, float, float, float] = (0, 0, 1, 1),
|
||||
batch: Optional[gpu.types.GPUBatch] = None,
|
||||
corner_radius: Optional[Union[tuple[Union[str, float], ...], str, float]] = None,
|
||||
corner_segments: int = SEGMENTS_DEFAULT,
|
||||
) -> Optional[gpu.types.GPUBatch]:
|
||||
"""Draws an image at given location with given size.
|
||||
|
||||
Supports optional rounded corner clipping by supplying ``corner_radius``.
|
||||
|
||||
Returns:
|
||||
The batch object if successful, or None if the image is invalid.
|
||||
"""
|
||||
if not image.name or not image.filepath:
|
||||
if width <= 0.0 or height <= 0.0 or not image.name or not image.filepath:
|
||||
return None
|
||||
|
||||
image_shader = create_image_shader()
|
||||
rounded_segments = max(1, int(corner_segments))
|
||||
cache_key = (
|
||||
image.filepath,
|
||||
float(x),
|
||||
float(y),
|
||||
float(width),
|
||||
float(height),
|
||||
tuple(float(component) for component in crop),
|
||||
repr(corner_radius) if corner_radius is not None else None,
|
||||
rounded_segments,
|
||||
)
|
||||
|
||||
texture = None
|
||||
ci = cached_images.get(image.filepath + "GPU_TEXTURE")
|
||||
if ci is not None:
|
||||
if (
|
||||
ci["x"] == x
|
||||
and ci["y"] == y
|
||||
and ci["width"] == width
|
||||
and ci["height"] == height
|
||||
):
|
||||
if batch is None:
|
||||
ci = cached_images.get(cache_key)
|
||||
if ci is not None:
|
||||
batch = ci["batch"]
|
||||
image_shader = ci["image_shader"]
|
||||
texture = ci["texture"]
|
||||
|
||||
if not batch:
|
||||
coords = [(x, y), (x + width, y), (x, y + height), (x + width, y + height)]
|
||||
|
||||
uvs = [
|
||||
(crop[0], crop[1]),
|
||||
(crop[2], crop[1]),
|
||||
(crop[0], crop[3]),
|
||||
(crop[2], crop[3]),
|
||||
]
|
||||
|
||||
indices = [(0, 1, 2), (2, 1, 3)]
|
||||
|
||||
if batch is None:
|
||||
coords = None
|
||||
uvs = None
|
||||
indices = None
|
||||
if corner_radius is not None:
|
||||
mesh_data = _rounded_rect_mesh(
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
corner_radius,
|
||||
crop,
|
||||
rounded_segments,
|
||||
)
|
||||
if mesh_data:
|
||||
coords, uvs, indices = mesh_data
|
||||
if coords is None or uvs is None or indices is None:
|
||||
coords = [(x, y), (x + width, y), (x, y + height), (x + width, y + height)]
|
||||
uvs = [
|
||||
(crop[0], crop[1]),
|
||||
(crop[2], crop[1]),
|
||||
(crop[0], crop[3]),
|
||||
(crop[2], crop[3]),
|
||||
]
|
||||
indices = [(0, 1, 2), (2, 1, 3)]
|
||||
batch = batch_for_shader(
|
||||
image_shader, "TRIS", {"pos": coords, "texCoord": uvs}, indices=indices
|
||||
)
|
||||
|
||||
texture = path_to_gpu_texture(image.filepath)
|
||||
|
||||
# tell shader to use the image that is bound to image unit 0
|
||||
cached_images[image.filepath + "GPU_TEXTURE"] = {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": width,
|
||||
"height": height,
|
||||
cached_images[cache_key] = {
|
||||
"batch": batch,
|
||||
"image_shader": image_shader,
|
||||
"texture": texture,
|
||||
}
|
||||
|
||||
if batch is None:
|
||||
if batch is None or image_shader is None:
|
||||
return None
|
||||
|
||||
if image_shader and texture:
|
||||
color_space_mode = _resolve_color_space_mode()
|
||||
if texture is None:
|
||||
texture = path_to_gpu_texture(image.filepath)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
if texture is None:
|
||||
return None
|
||||
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
color_space_mode = _resolve_color_space_mode()
|
||||
|
||||
# may not be available in simple shader
|
||||
try:
|
||||
# set floats
|
||||
image_shader.uniform_float("transparency", transparency)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
# set color space mode
|
||||
image_shader.uniform_int("color_space_mode", color_space_mode)
|
||||
batch.draw(image_shader)
|
||||
except Exception:
|
||||
pass
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
|
||||
# may not be available in simple shader
|
||||
try:
|
||||
# set floats
|
||||
image_shader.uniform_float("transparency", transparency)
|
||||
|
||||
# set color space mode
|
||||
image_shader.uniform_int("color_space_mode", color_space_mode)
|
||||
batch.draw(image_shader)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return batch
|
||||
|
||||
@@ -427,7 +818,9 @@ def path_to_gpu_texture(path: str) -> Optional[gpu.types.GPUTexture]:
|
||||
return tex
|
||||
|
||||
|
||||
def get_text_size(font_id=0, text="", text_size=16, dpi=72):
|
||||
def get_text_size(
|
||||
font_id: int = 0, text: str = "", text_size: float = 16, dpi: int = 72
|
||||
):
|
||||
if app.version < (4, 0, 0):
|
||||
blf.size(font_id, text_size, dpi)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user